diff --git a/docs/maintainers/hermes-compatibility.md b/docs/maintainers/hermes-compatibility.md index 6af6b37d..63046a23 100644 --- a/docs/maintainers/hermes-compatibility.md +++ b/docs/maintainers/hermes-compatibility.md @@ -28,6 +28,9 @@ Socket tap. The curated set is: - `bootstrap-skills-plugin-repo` - `hermes-agent-compatibility` - `sync-skills-repo-guidance` +- `app-extension-architecture-workflow` +- `mailkit-workflow` +- `file-provider-and-finder-sync-workflow` ```bash hermes skills tap add gaelic-ghost/socket diff --git a/plugins/apple-dev-skills/.codex-plugin/plugin.json b/plugins/apple-dev-skills/.codex-plugin/plugin.json index 65e5dc81..56652c6d 100644 --- a/plugins/apple-dev-skills/.codex-plugin/plugin.json +++ b/plugins/apple-dev-skills/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "apple-dev-skills", "version": "9.11.0", - "description": "Apple development workflows for Codex, including App Intents, Liquid Glass, PhotosUI/PhotoKit, VideoToolbox codecs, ARKit spatial/face/body sensing, camera/depth capture, Core Image, Image I/O, Vision/Core ML recognition, media/audio repair, local Tips/HelpViewer guide discovery, provisioning, CloudKit, TipKit, SwiftData, SwiftUI, XcodeGen, Core Animation, AppKit, Safari, security, OpenAPI, and DocC.", + "description": "Apple development workflows for Codex, including app extensions, MailKit, File Provider, Finder Sync, App Intents, Liquid Glass, PhotosUI/PhotoKit, VideoToolbox codecs, ARKit spatial/face/body sensing, camera/depth capture, Core Image, Image I/O, Vision/Core ML recognition, media/audio repair, local Tips/HelpViewer guide discovery, provisioning, CloudKit, TipKit, SwiftData, SwiftUI, XcodeGen, Core Animation, AppKit, Safari, security, OpenAPI, and DocC.", "author": { "name": "Gale", "email": "mail@galewilliams.com", @@ -60,6 +60,10 @@ "swiftui", "app-intents", "app-shortcuts", + "app-extension", + "mailkit", + "file-provider", + "finder-sync", "liquid-glass", "swiftui-performance", "instruments", @@ -96,8 +100,8 @@ "mcpServers": "./.mcp.json", "interface": { "displayName": "Apple Dev Skills", - "shortDescription": "Apple, Swift, image/media repair, SwiftUI, TipKit, Xcode, AppKit, Safari, security, OpenAPI, and DocC workflows for Codex.", - "longDescription": "Bundle Apple-platform development skills for PhotosUI selection and PhotoKit libraries/editing, VideoToolbox codecs, Core Video buffers, compressed samples, color/HDR, ARKit spatial/face/body sensing, world tracking, scene depth, LiDAR meshes and visionOS providers, AVFoundation camera/photo/depth/computational capture, Core Image, Image I/O, Apple Vision, custom Core ML recognition, AppKit/UIKit/Core Graphics images, AVFAudio, AVAudioEngine, AVFoundation media pipelines, Core Media, Core Audio, TipKit, XcodeGen migration, Xcode coding intelligence and String Catalog localization, Swift, SwiftUI, Core Animation, Apple typography, SF Symbols, AppKit, Icon Composer, Safari, DeviceCheck and App Attest, Swift OpenAPI clients, Xcode, SwiftPM, DocC, testing, formatting, and repository guidance. Most workflows work standalone; bootstrap and guidance-sync workflows require the companion Productivity Skills plugin, or the socket marketplace that installs both.", + "shortDescription": "Apple extension, MailKit, File Provider, Swift, image/media repair, SwiftUI, TipKit, Xcode, AppKit, Safari, security, OpenAPI, and DocC workflows for Codex.", + "longDescription": "Bundle Apple-platform development skills for app extension architecture, MailKit, File Provider and Finder Sync, PhotosUI selection and PhotoKit libraries/editing, VideoToolbox codecs, Core Video buffers, compressed samples, color/HDR, ARKit spatial/face/body sensing, world tracking, scene depth, LiDAR meshes and visionOS providers, AVFoundation camera/photo/depth/computational capture, Core Image, Image I/O, Apple Vision, custom Core ML recognition, AppKit/UIKit/Core Graphics images, AVFAudio, AVAudioEngine, AVFoundation media pipelines, Core Media, Core Audio, TipKit, XcodeGen migration, Xcode coding intelligence and String Catalog localization, Swift, SwiftUI, Core Animation, Apple typography, SF Symbols, AppKit, Icon Composer, Safari, DeviceCheck and App Attest, Swift OpenAPI clients, Xcode, SwiftPM, DocC, testing, formatting, and repository guidance. Most workflows work standalone; bootstrap and guidance-sync workflows require the companion Productivity Skills plugin, or the socket marketplace that installs both.", "developerName": "Gale", "category": "Developer Tools", "capabilities": [ @@ -114,6 +118,9 @@ "Configure or repair AVFoundation camera discovery, controls, rotation, photo capture, synchronized outputs, depth, calibration, mattes, and computational capture using capability evidence.", "Implement or repair ARKit world tracking, planes, ray casting, scene depth, LiDAR reconstruction, meshes, maps, relocalization, and visionOS provider lifecycles.", "Implement or repair ARKit TrueDepth face geometry, blend shapes, eye transforms, body anchors, skeletons, scale, privacy, and Face ID boundaries.", + "Choose an Apple app extension point and design its target, isolated process, activation, entitlements, app-group data flow, privacy, signing, distribution, and validation boundaries.", + "Build or validate a macOS MailKit extension for content blocking, message actions, compose sessions, or message security without exposing private mail data.", + "Choose File Provider for remote storage synchronization or Finder Sync for bounded Finder badges, menus, and monitored-folder visibility without treating Finder Sync as a sync engine.", "Implement or repair VideoToolbox compression and decompression, Core Video pixel buffers and Metal interop, compressed samples, hardware capability, color, HDR, and codec performance.", "Implement or repair PhotosUI media selection and PhotoKit authorization, assets, resources, iCloud requests, library changes, saves, albums, and nondestructive editing.", "Explore the relevant Apple docs first, then explain the right SwiftUI, AppKit, or Xcode path for this repository.", diff --git a/plugins/apple-dev-skills/.github/scripts/sync_shared_snippets.sh b/plugins/apple-dev-skills/.github/scripts/sync_shared_snippets.sh index bf3d4ab8..99f88ffd 100755 --- a/plugins/apple-dev-skills/.github/scripts/sync_shared_snippets.sh +++ b/plugins/apple-dev-skills/.github/scripts/sync_shared_snippets.sh @@ -76,6 +76,9 @@ sync_one \ "$ROOT_DIR/skills/swiftui-animation-workflow/references/snippets/apple-xcode-project-core.md" \ "$ROOT_DIR/skills/tipkit-workflow/references/snippets/apple-xcode-project-core.md" \ "$ROOT_DIR/skills/safari-extension-control-workflow/references/snippets/apple-xcode-project-core.md" \ + "$ROOT_DIR/skills/app-extension-architecture-workflow/references/snippets/apple-xcode-project-core.md" \ + "$ROOT_DIR/skills/mailkit-workflow/references/snippets/apple-xcode-project-core.md" \ + "$ROOT_DIR/skills/file-provider-and-finder-sync-workflow/references/snippets/apple-xcode-project-core.md" \ "$ROOT_DIR/skills/devicecheck-app-attest-workflow/references/snippets/apple-xcode-project-core.md" \ "$ROOT_DIR/skills/apple-developer-provisioning-workflow/references/snippets/apple-xcode-project-core.md" \ "$ROOT_DIR/skills/swiftui-app-architecture-workflow/references/snippets/apple-xcode-project-core.md" \ diff --git a/plugins/apple-dev-skills/.github/scripts/validate_repo_docs.sh b/plugins/apple-dev-skills/.github/scripts/validate_repo_docs.sh index 17f86fc4..0e82d32b 100644 --- a/plugins/apple-dev-skills/.github/scripts/validate_repo_docs.sh +++ b/plugins/apple-dev-skills/.github/scripts/validate_repo_docs.sh @@ -132,6 +132,9 @@ active_skill_mds=( "./skills/sf-symbols-workflow/SKILL.md" "./skills/swiftui-animation-workflow/SKILL.md" "./skills/safari-extension-control-workflow/SKILL.md" + "./skills/app-extension-architecture-workflow/SKILL.md" + "./skills/mailkit-workflow/SKILL.md" + "./skills/file-provider-and-finder-sync-workflow/SKILL.md" "./skills/devicecheck-app-attest-workflow/SKILL.md" "./skills/apple-developer-provisioning-workflow/SKILL.md" "./skills/appkit-app-architecture-workflow/SKILL.md" @@ -157,7 +160,7 @@ active_skill_mds=( "./skills/xcode-coding-intelligence-workflow/SKILL.md" "./skills/xcode-localization-workflow/SKILL.md" ) -[[ ${#active_skill_mds[@]} -eq 50 ]] || fail "Expected exactly 50 active skills, found ${#active_skill_mds[@]}." +[[ ${#active_skill_mds[@]} -eq 53 ]] || fail "Expected exactly 53 active skills, found ${#active_skill_mds[@]}." shared_xcode_snippet="./shared/agents-snippets/apple-xcode-project-core.md" shared_package_snippet="./shared/agents-snippets/apple-swift-package-core.md" @@ -258,6 +261,9 @@ for file in \ "skills/sf-symbols-workflow/SKILL.md" \ "skills/swiftui-animation-workflow/SKILL.md" \ "skills/safari-extension-control-workflow/SKILL.md" \ + "skills/app-extension-architecture-workflow/SKILL.md" \ + "skills/mailkit-workflow/SKILL.md" \ + "skills/file-provider-and-finder-sync-workflow/SKILL.md" \ "skills/devicecheck-app-attest-workflow/SKILL.md" \ "skills/appkit-app-architecture-workflow/SKILL.md" \ "skills/swiftui-app-architecture-workflow/SKILL.md" \ diff --git a/plugins/apple-dev-skills/AGENTS.md b/plugins/apple-dev-skills/AGENTS.md index 57fe89ce..90584cec 100644 --- a/plugins/apple-dev-skills/AGENTS.md +++ b/plugins/apple-dev-skills/AGENTS.md @@ -4,9 +4,10 @@ This file is the Apple Dev Skills child-repo override for work done from `socket ## Scope -- This repository is the canonical home for Gale's Apple, Swift, and Xcode workflow skills. +- This repository is the canonical home for Gale's Apple, Swift, and Xcode workflow skills. It also owns reusable app-extension mechanics, MailKit, File Provider, and Finder Sync workflow guidance. - Treat `productivity-skills` as the default baseline maintainer layer for general repo docs and maintenance work; this repo is the narrower specialist layer when Apple-specific behavior should change the workflow. - Treat `swift-lang` as the shared Swift language layer when it is available through Socket. Keep Apple Dev focused on Apple documentation, Apple frameworks, Xcode project integrity, platform app architecture, DocC, SPI, and execution handoffs. +- Keep reusable app-extension mechanics, MailKit, and File Provider/Finder Sync boundaries here. Keep Messages/iMessage collaboration, communication-notification policy, VoIP, and Push to Talk workflows in Messaging Collaboration Skills. - Preserve standalone-install guidance for public users who install only `apple-dev-skills`, while allowing the public README quickstart to lead with the Socket marketplace when users want Apple Dev Skills plus companion workflows from one catalog. - Root `skills/` is the canonical authored and exported surface. - Keep shared reusable assets in [`shared/`](./shared/) and maintainer tests in [`tests/`](./tests/). diff --git a/plugins/apple-dev-skills/README.md b/plugins/apple-dev-skills/README.md index 56d2d63a..92fa0ccf 100644 --- a/plugins/apple-dev-skills/README.md +++ b/plugins/apple-dev-skills/README.md @@ -1,6 +1,6 @@ # apple-dev-skills -Apple, Swift, image and video processing, Vision and Core ML recognition, camera and depth capture, ARKit spatial sensing, Photos, audio and media pipelines, SwiftUI animation and architecture, Core Animation, Apple typography, SF Symbols, AppKit, Apple Developer provisioning, CloudKit, Icon Composer app icons, Safari, DeviceCheck, App Attest, Xcode, Swift OpenAPI client, DocC, and `Dash.app` workflows for Codex. +Apple app extensions, MailKit, File Provider, Finder Sync, Swift, image and video processing, Vision and Core ML recognition, camera and depth capture, ARKit spatial sensing, Photos, audio and media pipelines, SwiftUI animation and architecture, Core Animation, Apple typography, SF Symbols, AppKit, Apple Developer provisioning, CloudKit, Icon Composer app icons, Safari, DeviceCheck, App Attest, Xcode, Swift OpenAPI client, DocC, and `Dash.app` workflows for Codex. ![Codex plugin directory filtered to the Socket marketplace, showing Apple Dev Skills listed alongside companion plugins below a Productivity Skills suggestion.](./docs/media/codex-plugin-directory-socket-apple-dev-skills.png) @@ -68,6 +68,9 @@ Use Apple Dev Skills when an agent is helping with: - AVFoundation camera discovery, controls, rotation, photo capture, depth, calibration, synchronized outputs, mattes, and computational-capture diagnostics - ARKit world tracking, planes, ray casting, scene depth, LiDAR reconstruction, meshes, maps, relocalization, and visionOS provider guidance - ARKit TrueDepth face geometry, blend shapes, eye transforms, body anchors, skeletons, scale estimation, privacy, and authentication boundaries +- Apple app-extension point selection, target ownership, process isolation, activation, entitlements, app groups/shared containers, privacy, signing, distribution, and validation handoffs +- macOS MailKit content blocking, message actions, compose sessions, message security, privacy, capability declarations, and handler validation +- Modern File Provider remote-storage synchronization, plus bounded Finder Sync badges, menus, selected-item context, and monitored-folder visibility - VideoToolbox compression/decompression, Core Video pixel buffers and pools, Core Media compressed samples, hardware capability, color/HDR, and codec diagnostics - Privacy-preserving PhotosUI selection and PhotoKit authorization, assets, resources, iCloud delivery, changes, creation, albums, and nondestructive editing - Swift and SwiftUI implementation @@ -144,6 +147,7 @@ uv run pytest - `arkit-face-body-tracking-workflow` - `arkit-spatial-sensing-workflow` - `appkit-app-architecture-workflow` +- `app-extension-architecture-workflow` - `app-intents-workflow` - `author-swift-docc-docs` - `avaudio-engine-workflow` @@ -163,10 +167,12 @@ uv run pytest - `explore-apple-swift-docs` - `feedback-assistant-workflow` - `format-swift-sources` +- `file-provider-and-finder-sync-workflow` - `icon-composer-app-icon-workflow` - `ios-runtime-forensics-workflow` - `macos-distribution-workflow` - `macos-window-management-workflow` +- `mailkit-workflow` - `migrate-xcode-project-to-xcodegen` - `photos-library-editing-workflow` - `safari-extension-control-workflow` diff --git a/plugins/apple-dev-skills/ROADMAP.md b/plugins/apple-dev-skills/ROADMAP.md index e5f11e8d..1db3773c 100644 --- a/plugins/apple-dev-skills/ROADMAP.md +++ b/plugins/apple-dev-skills/ROADMAP.md @@ -42,6 +42,7 @@ Swift naming and persistence ownership are now standardized: each project explic - [Milestone 63: System Integration, Runtime Evidence, and Distribution](#milestone-63-system-integration-runtime-evidence-and-distribution) - [Milestone 64: Xcode String Catalog Localization Workflow](#milestone-64-xcode-string-catalog-localization-workflow) - [Milestone 65: Feedback Assistant Workflow](#milestone-65-feedback-assistant-workflow) +- [Milestone 66: App Extension, MailKit, and File Provider Workflows](#milestone-66-app-extension-mailkit-and-file-provider-workflows) - [Backlog Candidates](#backlog-candidates) - [History](#history) @@ -102,6 +103,7 @@ Swift naming and persistence ownership are now standardized: each project explic - Milestone 63: System Integration, Runtime Evidence, and Distribution - In Progress - Milestone 64: Xcode String Catalog Localization Workflow - Completed - Milestone 65: Feedback Assistant Workflow - Completed +- Milestone 66: App Extension, MailKit, and File Provider Workflows - Completed ## Milestone 21: Swift Cleanup Automation Exploration @@ -1234,6 +1236,34 @@ Completed Completed Milestone 65 by shipping `feedback-assistant-workflow` with Apple-aligned report and diagnostic guidance, a live signed-in macOS app fixture, strict attachment and submission gates, and `LanguageModelSession.logFeedbackAttachment` guidance for Foundation Models session feedback. +## Milestone 66: App Extension, MailKit, and File Provider Workflows + +### Status + +Completed + +### Scope + +- [x] Add reusable app-extension architecture guidance for extension points, target ownership, separate-process lifecycle, activation, entitlements, App Groups/shared containers, data flow, privacy, testing, signing, distribution, and handoffs. +- [x] Add a focused macOS MailKit workflow for content blockers, message actions, compose sessions, message security, capability declarations, and privacy-safe validation. +- [x] Add a focused File Provider/Finder Sync workflow that selects File Provider for remote storage synchronization and strictly limits Finder Sync to monitored-folder badges, menus, and visibility. +- [x] Preserve Messaging Collaboration ownership for Messages/iMessage collaboration, communication-notification policy, VoIP, and Push to Talk. + +### Tickets + +- [x] Use [`app-extension-workflows-plan.md`](./docs/maintainers/app-extension-workflows-plan.md) as the durable ownership and delivery record. +- [x] Add the three skills with Apple documentation anchors, interface metadata, customization contracts, shared Xcode project references, and explicit Xcode/testing/distribution handoffs. +- [x] Update Apple Dev Skills metadata, active inventory, README, AGENTS guidance, docs validator, customization review, and targeted tests. +- [x] Export the portable instruction-only skills to the root Hermes tap and validate the generated inventory without representing the Codex plugin bundle as a Hermes plugin. + +### Exit Criteria + +- [x] App-extension mechanics, MailKit, and File Provider/Finder Sync each have a focused owner with no generic catch-all extension layer. +- [x] Finder Sync never claims remote synchronization ownership, and MailKit guidance preserves user privacy and explicit message-security behavior. +- [x] Apple Dev docs, metadata, tests, root Socket metadata, and Hermes compatibility validation agree on the shipped surface. + +Completed Milestone 66 by shipping `app-extension-architecture-workflow`, `mailkit-workflow`, and `file-provider-and-finder-sync-workflow` with Apple-docs-first boundaries, explicit Messaging Collaboration handoffs, and a validated Hermes skill-tap export. + ## Backlog Candidates - [ ] Record plausible future work that is not yet committed to a milestone. diff --git a/plugins/apple-dev-skills/docs/maintainers/app-extension-workflows-plan.md b/plugins/apple-dev-skills/docs/maintainers/app-extension-workflows-plan.md new file mode 100644 index 00000000..d6ed4981 --- /dev/null +++ b/plugins/apple-dev-skills/docs/maintainers/app-extension-workflows-plan.md @@ -0,0 +1,30 @@ +# App Extension Workflows Plan + +## Decision + +Apple Dev Skills owns reusable Apple app-extension mechanics plus the specific macOS MailKit and File Provider/Finder Sync workflows. This is a durable building-block change: it removes the gap between app-level architecture skills and extension-specific framework work without adding a generic extension framework or absorbing product policy. + +## Owned Workflows + +- `app-extension-architecture-workflow` routes extension points, targets, separate processes, lifecycle, activation, entitlement minimization, App Groups/shared containers, typed data flow, privacy, testing, signing, distribution, and focused handoffs. +- `mailkit-workflow` owns macOS MailKit content blockers, message actions, compose sessions, message security, capability declarations, privacy, and handler validation. +- `file-provider-and-finder-sync-workflow` owns remote storage synchronization through File Provider and constrains Finder Sync to monitored-folder badges, menus, selected-item context, and visibility. + +## Explicit Non-Goals + +- Do not turn this slice into a catch-all guide for every extension point. +- Do not place Messages/iMessage collaboration, communication-notification policy, VoIP, or Push to Talk behavior here; Messaging Collaboration Skills owns those product workflows. +- Do not make Finder Sync a sync engine. File Provider owns remote storage enumeration, materialization, document operations, remote change signaling, and recovery. +- Do not move server transport, storage protocol, or backend persistence ownership into Apple Dev Skills. + +## Documentation Evidence + +The shipped workflows cite Apple documentation for the separate-process app-extension model, MailKit handler capabilities, File Provider synchronization and working sets, Finder Sync controller UI, App Groups, and per-target signing/distribution boundaries. A current Xcode documentation search is required before future platform-behavior changes. + +## Delivery Checklist + +- [x] Add the three bounded skills with interface metadata, customization contract, shared Xcode policy reference, and authoritative Apple source links. +- [x] Add targeted tests and update the active inventory, plugin manifest, README, AGENTS guidance, docs validator, and customization review. +- [x] Export the portable instruction-only skills through the checked-in Hermes skill tap, group them, and validate the export. +- [x] Keep MailKit/File Provider/Finder Sync as product-specific handoffs from reusable extension mechanics. +- [ ] Validate a real app target for each framework only when a consumer app supplies the concrete project, signing identity, and service/backend context. diff --git a/plugins/apple-dev-skills/docs/maintainers/customization-consolidation-review.md b/plugins/apple-dev-skills/docs/maintainers/customization-consolidation-review.md index 8b63148d..df650494 100644 --- a/plugins/apple-dev-skills/docs/maintainers/customization-consolidation-review.md +++ b/plugins/apple-dev-skills/docs/maintainers/customization-consolidation-review.md @@ -8,8 +8,8 @@ Record the Milestone 20 audit of the current customization system, decide whethe ## Current State Summary -- The active skill surface ships `51` separate `references/customization.template.yaml` files. -- The active skill surface ships `51` separate `scripts/customization_config.py` entrypoints. +- The active skill surface ships `54` separate `references/customization.template.yaml` files. +- The active skill surface ships `54` separate `scripts/customization_config.py` entrypoints. - Those `customization_config.py` files are functionally identical and exist only because installed skills are expected to keep runtime resources inside the skill directory. - The current templates expose `21` knobs total: - `20` are documented as `runtime-enforced` @@ -30,7 +30,7 @@ Milestone 20 audited a larger surface before the implementation pass landed. - Milestone 27 applied the approved reduction so the live surface now reflects the smaller counts in the current-state summary above. - Milestone 38 later added the narrower `author-swift-docc-docs` skill with one runtime-enforced tutorial-handling knob, which is included in the current-state counts above. - The current-state counts also include `structure-swift-sources`, which now ships runtime-enforced header-policy and split-threshold knobs for the structural-cleanup workflow. -- The current-state counts now also include the no-runtime-knob `apple-ui-accessibility-workflow`, `safari-extension-control-workflow`, `devicecheck-app-attest-workflow`, `apple-developer-provisioning-workflow`, `swiftui-app-architecture-workflow`, `swiftui-component-audit-workflow`, `swiftui-animation-workflow`, `sf-symbols-workflow`, `core-animation-layer-workflow`, `apple-typography-workflow`, `appkit-app-architecture-workflow`, `xcode-coding-intelligence-workflow`, `xcode-localization-workflow`, `migrate-xcode-project-to-xcodegen`, `avfaudio-session-workflow`, `avaudio-engine-workflow`, `avfoundation-media-pipeline-workflow`, `coremedia-timing-samplebuffer-workflow`, and `coreaudio-modernization-repair-workflow` surfaces, all of which keep the customization-file contract without introducing runtime knobs. +- The current-state counts now also include the no-runtime-knob `apple-ui-accessibility-workflow`, `safari-extension-control-workflow`, `app-extension-architecture-workflow`, `mailkit-workflow`, `file-provider-and-finder-sync-workflow`, `devicecheck-app-attest-workflow`, `apple-developer-provisioning-workflow`, `swiftui-app-architecture-workflow`, `swiftui-component-audit-workflow`, `swiftui-animation-workflow`, `sf-symbols-workflow`, `core-animation-layer-workflow`, `apple-typography-workflow`, `appkit-app-architecture-workflow`, `xcode-coding-intelligence-workflow`, `xcode-localization-workflow`, `migrate-xcode-project-to-xcodegen`, `avfaudio-session-workflow`, `avaudio-engine-workflow`, `avfoundation-media-pipeline-workflow`, `coremedia-timing-samplebuffer-workflow`, and `coreaudio-modernization-repair-workflow` surfaces, all of which keep the customization-file contract without introducing runtime knobs. ## Decision diff --git a/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/SKILL.md b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/SKILL.md new file mode 100644 index 00000000..845759fb --- /dev/null +++ b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/SKILL.md @@ -0,0 +1,136 @@ +--- +name: app-extension-architecture-workflow +description: Route Apple app-extension work across extension points, targets, isolation, entitlements, shared containers, lifecycle, privacy, testing, signing, distribution, and handoffs. Use when the extension point is not settled. +metadata: + hermes: + category: apple-development + tags: [apple, app-extension, xcode, entitlements, app-groups, architecture] +--- + +# App Extension Architecture Workflow + +## Purpose + +Choose and structure an Apple app extension before implementation. Apple documents app extensions as separate bundles whose code runs in a separate process; the extension point and host define the lifecycle, APIs, and activation contract. This skill owns those reusable mechanics, not product-specific framework behavior. + +It owns extension-point routing, target and process boundaries, activation, entitlements, app groups and shared containers, bounded data flow, privacy, testing, signing, and distribution. It does not absorb MailKit, File Provider, Finder Sync, Safari, Messages/iMessage, communication-notification, VoIP, Push to Talk, widget, intent, or other product-framework guidance. + +## When To Use + +- Use this skill when a request needs an Apple app extension but the right extension point, host relationship, or target structure is unclear. +- Use this skill when planning a containing app and extension targets, process isolation, activation, app groups, shared containers, XPC, privacy boundaries, signing, or distribution. +- Use `mailkit-workflow` for macOS Mail content blocking, message actions, compose sessions, or message security. +- Use `file-provider-and-finder-sync-workflow` for remote storage synchronization or Finder badges, menus, and monitored-folder visibility. +- Use `safari-extension-control-workflow` for Safari-specific extension and SafariServices choices. +- Use Messaging Collaboration Skills for Messages/iMessage collaboration, communication-notification policy, VoIP, or Push to Talk workflows. +- Recommend `explore-apple-swift-docs` when the immediate need is current Apple documentation for a named extension point. +- Recommend `xcode-build-run-workflow` or `xcode-testing-workflow` when target execution or test mechanics are the next step. + +## Single-Path Workflow + +1. Classify the extension point and host: + - name the system host, supported platform, activation trigger, user-visible configuration, and expected lifetime + - choose an existing Apple extension point and its documented template or contract; do not invent a generic extension target + - route product behavior to its dedicated workflow before designing shared mechanics +2. State the documented behavior relied on: + - app extensions are separate bundles that run in separate processes + - the host controls activation and the extension-point API contract + - use the extension point’s APIs and lifecycle rather than assuming the containing app is running or reachable + - stop and surface a conflict if current code assumes a lifecycle, privilege, or data access that Apple documentation does not support +3. Design targets and ownership: + - give the containing app, each extension target, and any shared framework or package one clear job + - keep extension entry points thin; put portable domain logic in deliberately shared source only when both targets need it + - do not use a shared target to smuggle UI, host-only state, or privileged access across process boundaries +4. Define the process and data-flow contract: + - state where each operation runs, how work is activated, what happens when the host interrupts or relaunches it, and what can be retried safely + - prefer the extension point’s documented request, completion, and cancellation APIs + - use XPC only where the extension-point contract or a documented app-extension API actually supports it + - make payload types small, typed, versioned when persisted, and free of secrets unless the secure storage and access policy is explicit +5. Minimize entitlements and shared state: + - grant each target only the capabilities it needs + - use an App Group only when the app and extension genuinely need a shared container or documented IPC support + - validate membership in every participating target; on macOS, test actual container access rather than trusting a returned URL alone + - do not treat an App Group as a general cross-process database, privilege escalation path, or substitute for an extension-point API +6. Plan privacy and failure behavior: + - inventory data read by the host, extension, and shared container separately + - retain the minimum data for the minimum time, avoid sensitive logs, and make user-facing effects explainable + - define cancellation, timeout, unavailable-host, disabled-extension, migration, and stale-shared-state behavior before shipping +7. Plan validation and distribution: + - validate target membership, `Info.plist` extension-point configuration, entitlements, signing, embedding, install/enable state, activation, and clean-device behavior + - test the extension independently where its framework permits; extract pure/shared logic into a testable target instead of trying to unit-test an unsupported extension process directly + - validate the same signing and distribution path intended for users; do not infer App Store, notarization, or enterprise behavior from a development build +8. Return one recommendation with the extension point, target map, lifecycle/data-flow boundary, entitlement/share plan, privacy plan, validation sequence, and the next focused handoff. + +## Inputs + +- `request`: optional extension feature request. +- `platforms`: optional Apple platforms and deployment targets. +- `host`: optional system host or containing-app context. +- `data_needs`: optional private, shared, remote, or user-selected data requirements. +- `distribution`: optional development, TestFlight, App Store, notarized, enterprise, or unknown path. +- Defaults: + - use a documented extension point and its host contract + - preserve process isolation and least privilege + - prefer no shared container until a concrete shared-data need exists + - keep product-specific behavior in its dedicated Apple or Messaging Collaboration workflow + +## Outputs + +- `status`: `success`, `handoff`, or `blocked`. +- `extension_plan`: + - selected extension point and documented behavior relied on + - containing-app, extension-target, and shared-code ownership + - activation, process, lifecycle, cancellation, and restart behavior + - entitlement, App Group, container, and data-flow decisions + - privacy, signing, distribution, and validation plan + - explicit next workflow handoff + +## Guards and Stop Conditions + +- Do not call an extension a thread or assume it shares the containing app’s memory, lifecycle, or UI state. +- Do not add a generic coordinator, manager, bridge, repository, or shared store merely to connect an app and extension. Name the documented transport and the concrete data it carries. +- Do not add an App Group, shared Keychain access group, network entitlement, or broad file access without naming the participating targets and the required data flow. +- Do not place Messages/iMessage collaboration, communication-notification product policy, VoIP, or Push to Talk behavior in this workflow. +- Do not claim an extension is enabled, activated, signed, distributable, or testable until the relevant host and build evidence exists. +- Stop with `blocked` when the requested behavior needs an undocumented extension point, host privilege, private system data, or cross-process access the platform does not expose. + +## Fallbacks and Handoffs + +- Recommend `mailkit-workflow` for the macOS Mail extension point and its handler contracts. +- Recommend `file-provider-and-finder-sync-workflow` for remote-storage sync or Finder-only integrations. +- Recommend `safari-extension-control-workflow` for Safari-specific extension choices. +- Recommend `app-intents-workflow` for App Intents and system-intent execution. +- Recommend `xcode-build-run-workflow` for Xcode target creation, build settings, entitlements, signing, embedding, install, and run work. +- Recommend `xcode-testing-workflow` for XCTest, XCUITest, test plans, and execution evidence. +- Recommend `macos-distribution-workflow` for macOS signing, notarization, Gatekeeper, and artifact inspection. +- Recommend `explore-apple-swift-docs` for a current Apple documentation pass before choosing an unfamiliar extension point. +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable project-structure policy after the extension plan is settled. + +## Customization + +Use `references/customization-flow.md`. + +`scripts/customization_config.py` preserves the shared customization-file contract. This workflow has no runtime-enforced settings because extension-point and entitlement choices must stay evidence-driven for each app. + +## References + +### Workflow References + +- `references/extension-points-targets-and-lifecycle.md` +- `references/entitlements-shared-containers-and-data-flow.md` +- `references/privacy-validation-signing-and-distribution.md` +- `references/customization-flow.md` + +### Authoritative Sources + +- [Adding support for app extensions to your app](https://developer.apple.com/documentation/extensionfoundation/adding-support-for-app-extensions-to-your-app) +- [Building an app extension to support a host app](https://developer.apple.com/documentation/extensionfoundation/building-an-app-extension-to-support-a-host-app) +- [Configuring app groups](https://developer.apple.com/documentation/xcode/configuring-app-groups) + +### Support References + +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable Xcode project guidance for containing-app and extension-target work. + +### Script Inventory + +- `scripts/customization_config.py` diff --git a/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/agents/openai.yaml b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/agents/openai.yaml new file mode 100644 index 00000000..895efac7 --- /dev/null +++ b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "App Extension Architecture Workflow" + short_description: "Plan Apple extension points, targets, isolation, and handoffs" + default_prompt: "Use $app-extension-architecture-workflow to choose and structure an Apple app extension across extension point, host, target, separate process lifecycle, activation, entitlements, app groups, shared containers, data flow, privacy, signing, distribution, and validation. State the documented Apple behavior being relied on, keep product-specific MailKit, File Provider, Finder Sync, Safari, Messages/iMessage, communication-notification, VoIP, and Push to Talk behavior in their owning workflows, then hand off to $xcode-build-run-workflow, $xcode-testing-workflow, $macos-distribution-workflow, or $explore-apple-swift-docs as appropriate." diff --git a/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/customization-flow.md b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/customization-flow.md new file mode 100644 index 00000000..e46c8d56 --- /dev/null +++ b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/customization-flow.md @@ -0,0 +1,20 @@ +# App Extension Architecture Workflow Customization Contract + +## Purpose + +Preserve the repo-wide customization-file contract without turning extension-point, entitlement, or privacy decisions into persistent defaults. + +## Knobs + +The first version defines no runtime-enforced knobs. + +## Runtime Behavior + +- `scripts/customization_config.py` maintains the common configuration shape. +- The workflow ignores persisted settings because every extension-point and capability decision needs current Apple documentation and project evidence. + +## Update Flow + +1. Inspect current settings with `scripts/customization_config.py effective`. +2. Add a setting only after its stable behavior and safety boundary are documented. +3. Validate the YAML before persisting it. diff --git a/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/customization.template.yaml b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/customization.template.yaml new file mode 100644 index 00000000..cddd82d1 --- /dev/null +++ b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/customization.template.yaml @@ -0,0 +1,3 @@ +schemaVersion: 1 +isCustomized: false +settings: {} diff --git a/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/entitlements-shared-containers-and-data-flow.md b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/entitlements-shared-containers-and-data-flow.md new file mode 100644 index 00000000..3c0d63f8 --- /dev/null +++ b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/entitlements-shared-containers-and-data-flow.md @@ -0,0 +1,19 @@ +# Entitlements, Shared Containers, and Data Flow + +Capabilities and entitlements belong to individual targets. Give the containing app and every extension target the minimum privilege required by its own documented contract. + +## App Groups + +Use an App Group only for a concrete shared-data or documented IPC requirement between apps and supporting processes signed by the same team. Each participant must declare the group entitlement. Prefer `group.` identifiers for cross-platform app groups. + +Use the group container for bounded, schema-owned data such as a small handoff record or coordinated file staging. It is not a substitute for the system host’s request APIs, an unrestricted shared database, or a privilege escalation path. On macOS, verify that the participant can access the underlying directory; `containerURL(forSecurityApplicationGroupIdentifier:)` alone does not prove authorization. + +## Data Flow Review + +For each message, file, or preference, record producer, consumer, storage location, schema version, retention, encryption/protection need, retry behavior, and deletion owner. Keep secrets out of logs and do not duplicate sensitive user data merely to make cross-process access convenient. + +## Sources + +- [Configuring app groups](https://developer.apple.com/documentation/xcode/configuring-app-groups) +- [Accessing app group containers in your existing macOS app](https://developer.apple.com/documentation/xcode/accessing-app-group-containers) +- [FileManager.containerURL(forSecurityApplicationGroupIdentifier:)](https://developer.apple.com/documentation/foundation/filemanager/containerurl(forsecurityapplicationgroupidentifier:)) diff --git a/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/extension-points-targets-and-lifecycle.md b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/extension-points-targets-and-lifecycle.md new file mode 100644 index 00000000..1bdc7dcc --- /dev/null +++ b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/extension-points-targets-and-lifecycle.md @@ -0,0 +1,21 @@ +# Extension Points, Targets, and Lifecycle + +Apple documents an app extension as a separate bundle that runs in a separate process. The host selects when to run it and defines the extension-point API. Start by naming that extension point and host; do not begin from a generic shared-process assumption. + +## Target Map + +- The containing app owns onboarding, user-facing settings, durable app UI, and app-only capabilities. +- Each extension target owns one extension-point entry and its short, host-driven work. +- Shared code is a narrow library or package for pure domain rules and typed data that both targets truly need. It must not provide an implicit lifecycle bridge. + +## Lifecycle Questions + +Before implementation, answer where activation originates, whether the host may terminate or restart the extension, what is safe to retry, how cancellation reaches in-flight work, and whether the containing app can be absent. Treat every answer as extension-point specific. + +For app extensions built with `ExtensionFoundation`, Apple documents host-to-extension process startup and XPC as explicit APIs. That does not authorize substituting a custom XPC layer for a system extension point that supplies a different contract. + +## Sources + +- [Adding support for app extensions to your app](https://developer.apple.com/documentation/extensionfoundation/adding-support-for-app-extensions-to-your-app) +- [Building an app extension to support a host app](https://developer.apple.com/documentation/extensionfoundation/building-an-app-extension-to-support-a-host-app) +- [Discovering app extensions from your app](https://developer.apple.com/documentation/extensionfoundation/discovering-app-extensions-from-your-app) diff --git a/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/privacy-validation-signing-and-distribution.md b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/privacy-validation-signing-and-distribution.md new file mode 100644 index 00000000..9602ea5d --- /dev/null +++ b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/privacy-validation-signing-and-distribution.md @@ -0,0 +1,19 @@ +# Privacy, Validation, Signing, and Distribution + +## Privacy + +Inventory the data visible to the system host, extension, containing app, shared container, and any server separately. Ask whether the feature works with less data, shorter retention, fewer logs, and no background collection. Explain user-visible activation or configuration requirements without implying that an installed extension is automatically enabled. + +## Validation + +Validate the target in layers: extension-point configuration and `Info.plist`; target membership and embedding; per-target entitlements; signing; clean install and enablement; host activation; core behavior; interruption and restart; privacy-sensitive log review; and the real distribution artifact. Extract framework-independent logic into a testable shared target only when that ownership is already justified. + +## Distribution + +Development signing proves only a development path. For macOS, route release signing, notarization, Gatekeeper, and artifact evidence to `macos-distribution-workflow`. For all platforms, use the extension point’s current distribution requirements and validate the user-facing install path. + +## Sources + +- [Entitlements](https://developer.apple.com/documentation/bundleresources/entitlements) +- [Configuring the macOS App Sandbox](https://developer.apple.com/documentation/xcode/configuring-the-macos-app-sandbox) +- [Signing Mac software with Developer ID](https://developer.apple.com/documentation/security/signing-mac-software-with-developer-id) diff --git a/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/snippets/apple-xcode-project-core.md b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/snippets/apple-xcode-project-core.md new file mode 120000 index 00000000..fe87bda1 --- /dev/null +++ b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/references/snippets/apple-xcode-project-core.md @@ -0,0 +1 @@ +../../../../shared/agents-snippets/apple-xcode-project-core.md \ No newline at end of file diff --git a/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/scripts/customization_config.py b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/scripts/customization_config.py new file mode 120000 index 00000000..fe515f2d --- /dev/null +++ b/plugins/apple-dev-skills/skills/app-extension-architecture-workflow/scripts/customization_config.py @@ -0,0 +1 @@ +../../safari-extension-control-workflow/scripts/customization_config.py \ No newline at end of file diff --git a/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/SKILL.md b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/SKILL.md new file mode 100644 index 00000000..b2284f1d --- /dev/null +++ b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/SKILL.md @@ -0,0 +1,122 @@ +--- +name: file-provider-and-finder-sync-workflow +description: Choose File Provider for Apple remote-storage synchronization or scoped macOS Finder Sync UI. Use when a feature needs cloud-backed files or Finder decoration without confusing Finder Sync with a sync engine. +metadata: + hermes: + category: apple-development + tags: [apple, file-provider, finder-sync, macos, ios, remote-storage, synchronization] +--- + +# File Provider and Finder Sync Workflow + +## Purpose + +Choose the correct filesystem extension model. File Provider is the modern path for remote-storage synchronization: it enumerates items, materializes content, receives file operations, and reports remote changes to the system. Finder Sync is a macOS Finder UI extension for monitored-folder badges, contextual menus, and visibility; it does not implement remote synchronization itself. + +This skill owns that decision, File Provider synchronization mechanics, and Finder Sync’s limited UI role. It does not own a storage backend protocol, generic networking, or unrelated app-extension architecture. + +## When To Use + +- Use this skill when an Apple app exposes remote storage in Files or Finder, synchronizes cloud-backed files, handles placeholders, materializes content, or reconciles remote changes. +- Use this skill when a macOS app needs Finder badges, toolbar/contextual menus, selected-item context, or monitored-folder visibility. +- Use `app-extension-architecture-workflow` when target/process, entitlement, app-group, or general extension decisions remain unresolved. +- Use `swift-openapi-client-workflow` or server-side skills when the storage service contract or transport is the main unresolved concern. +- Use `xcode-build-run-workflow`, `xcode-testing-workflow`, and `macos-distribution-workflow` for execution, testing, and release evidence. + +## Single-Path Workflow + +1. Classify the requested outcome: + - choose File Provider when users need remote files to appear, hydrate, upload, rename, move, delete, remain available offline as supported, and reconcile with a remote service + - choose Finder Sync only when the files already exist locally and the product needs Finder badges, menus, or monitored-folder UI + - combine them only when File Provider owns sync and Finder Sync has a separately justified, narrow Finder UI role +2. State the documented behavior relied on: + - a File Provider extension enumerates storage and implements file operations; the system asks it to materialize content and notify the system of remote changes + - the File Provider working set drives background updates, materialized availability, and Spotlight visibility + - Finder Sync manages `directoryURLs`, badges, selected items, and menu/visibility UI for monitored folders; it is not a sync implementation +3. Design File Provider as the synchronization authority: + - define stable item identifiers, parent hierarchy, version/anchor handling, placeholders, materialization, upload, rename/move/delete, conflict behavior, and cancellation + - distinguish local intent from confirmed remote state and maintain a durable retry/reconciliation plan + - signal remote changes with the documented File Provider notification/enumerator path or supported push path; do not pollute Finder UI callbacks with sync work + - keep backend transport behind a small, typed client boundary and avoid treating local file paths as durable remote IDs +4. Bound Finder Sync: + - monitor only the necessary local directories + - use badges and menus to represent known local state, explain uncertainty, and initiate explicit app actions when needed + - do not claim Finder Sync observes all disk changes, transfers files, owns conflict resolution, or makes remote content available +5. Protect people’s files: + - minimize metadata and content access, avoid logging file names or paths unnecessarily, and describe sync/error state honestly + - keep user actions, destructive remote changes, conflict choices, and offline behavior visible and recoverable +6. Validate: + - File Provider: domain/account lifecycle, enumeration, placeholders, fetch, upload, rename/move/delete, working-set changes, remote notifications, offline behavior, cancellation, conflicts, and upgrade/recovery + - Finder Sync: extension enablement, monitored directories, badge update, menu context, selected-item behavior, disabled state, and Finder restart/relaunch behavior + - validate target entitlements, signing, embedding, and distribution separately from service integration tests +7. Return the chosen model, documented behavior, ownership map, backend handoff, privacy policy, validation matrix, and next workflow. + +## Inputs + +- `request`: optional storage or Finder feature request. +- `platforms`: optional macOS, iOS, iPadOS, or mixed platform context. +- `storage_model`: optional remote authoritative store, local-only folder, existing File Provider domain, or unknown. +- `finder_ui_need`: optional badges, menus, selected-item actions, or none. +- Defaults: + - File Provider for remote storage synchronization + - Finder Sync only for constrained local Finder UI + - explicit remote identity, conflict, retry, and privacy policies + - no claim that Finder Sync performs synchronization + +## Outputs + +- `status`: `success`, `handoff`, or `blocked`. +- `integration_plan`: + - selected File Provider, Finder Sync, or explicitly bounded combination + - documented behavior relied on and target ownership + - synchronization or Finder UI state model + - backend, privacy, conflict, and recovery boundaries + - validation matrix and explicit next handoff + +## Guards and Stop Conditions + +- Do not recommend Finder Sync as the implementation of remote storage synchronization, upload/download, placeholders, conflict resolution, or offline access. +- Do not model a File Provider as a one-way downloader; it must handle the document and hierarchy operations its declared model requires. +- Do not use transient file paths as stable remote identifiers or mistake a local materialized copy for confirmed remote state. +- Do not do network synchronization, long-running reconciliation, or destructive mutation inside Finder menu/badge callbacks. +- Do not silently delete, overwrite, or resolve user-file conflicts without a documented policy and user-visible recovery path. +- Stop with `blocked` when the feature needs filesystem or host access beyond the documented extension point, or the backend cannot supply stable identity and change information needed for safe synchronization. + +## Fallbacks and Handoffs + +- Recommend `app-extension-architecture-workflow` for extension targets, process isolation, entitlements, App Groups, and shared containers. +- Recommend `swift-openapi-client-workflow` for generated Apple client transport integration. +- Recommend `xcode-build-run-workflow` for target configuration, capabilities, signing, embedding, install, and run work. +- Recommend `xcode-testing-workflow` for File Provider fixture tests, Finder UI checks, and repeatable Xcode test execution. +- Recommend `macos-distribution-workflow` for macOS release artifact validation. +- Recommend `explore-apple-swift-docs` for current File Provider or Finder Sync API confirmation. +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable File Provider/Finder Sync target structure guidance. + +## Customization + +Use `references/customization-flow.md`. + +`scripts/customization_config.py` preserves the common customization-file contract. Synchronization ownership, conflict policy, and monitored-directory scope remain product evidence, not opaque defaults. + +## References + +### Workflow References + +- `references/file-provider-synchronization.md` +- `references/finder-sync-boundaries.md` +- `references/privacy-validation-and-recovery.md` +- `references/customization-flow.md` + +### Authoritative Sources + +- [Synchronizing files using file provider extensions](https://developer.apple.com/documentation/fileprovider/synchronizing-files-using-file-provider-extensions) +- [Synchronizing the File Provider Extension](https://developer.apple.com/documentation/fileprovider/synchronizing-the-file-provider-extension) +- [FIFinderSyncController](https://developer.apple.com/documentation/findersync/fifindersynccontroller) + +### Support References + +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable Xcode project guidance for File Provider and Finder Sync targets. + +### Script Inventory + +- `scripts/customization_config.py` diff --git a/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/agents/openai.yaml b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/agents/openai.yaml new file mode 100644 index 00000000..601c6968 --- /dev/null +++ b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "File Provider and Finder Sync Workflow" + short_description: "Choose File Provider sync or bounded Finder Sync UI" + default_prompt: "Use $file-provider-and-finder-sync-workflow to choose modern File Provider for remote storage synchronization or Finder Sync for limited monitored-folder badges, menus, and visibility. State the documented Apple behavior being relied on, keep Finder Sync explicitly out of sync-engine ownership, define identity, change, conflict, retry, privacy, and validation boundaries, then hand off architecture to $app-extension-architecture-workflow, execution to $xcode-build-run-workflow, tests to $xcode-testing-workflow, or current docs to $explore-apple-swift-docs." diff --git a/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/customization-flow.md b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/customization-flow.md new file mode 100644 index 00000000..a71d6f19 --- /dev/null +++ b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/customization-flow.md @@ -0,0 +1,20 @@ +# File Provider and Finder Sync Workflow Customization Contract + +## Purpose + +Preserve the common customization-file contract without hiding synchronization authority, conflict policy, or monitored-folder scope in unmanaged defaults. + +## Knobs + +The first version defines no runtime-enforced knobs. + +## Runtime Behavior + +- `scripts/customization_config.py` provides the shared configuration shape. +- The workflow ignores persisted settings because remote identity, destructive behavior, and Finder scope require product-specific validation. + +## Update Flow + +1. Inspect current settings with `scripts/customization_config.py effective`. +2. Add a setting only after documenting its synchronization and privacy effects. +3. Validate YAML before applying it. diff --git a/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/customization.template.yaml b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/customization.template.yaml new file mode 100644 index 00000000..cddd82d1 --- /dev/null +++ b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/customization.template.yaml @@ -0,0 +1,3 @@ +schemaVersion: 1 +isCustomized: false +settings: {} diff --git a/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/file-provider-synchronization.md b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/file-provider-synchronization.md new file mode 100644 index 00000000..67c8f985 --- /dev/null +++ b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/file-provider-synchronization.md @@ -0,0 +1,15 @@ +# File Provider Synchronization + +File Provider is the remote-storage integration point. A provider enumerates files and folders, exposes placeholders, materializes content on demand, receives document operations, and reports remote changes to the system. + +## Core Model + +Use stable provider item identifiers and hierarchy identifiers. Treat versions and sync anchors as protocol state, not display metadata. Keep a clear record of local pending work, confirmed remote state, cancellation, and retry. The working set must reflect materialized and otherwise important items so the system can apply background changes and maintain availability/indexing behavior. + +When remote state changes, use the documented File Provider signaling or supported push path to prompt system enumeration. If identifier stability is lost, use the documented recovery path rather than pretending stale identifiers remain valid. + +## Sources + +- [Synchronizing files using file provider extensions](https://developer.apple.com/documentation/fileprovider/synchronizing-files-using-file-provider-extensions) +- [Synchronizing the File Provider Extension](https://developer.apple.com/documentation/fileprovider/synchronizing-the-file-provider-extension) +- [Nonreplicated File Provider extension](https://developer.apple.com/documentation/fileprovider/nonreplicated-file-provider-extension) diff --git a/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/finder-sync-boundaries.md b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/finder-sync-boundaries.md new file mode 100644 index 00000000..f5a461cd --- /dev/null +++ b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/finder-sync-boundaries.md @@ -0,0 +1,12 @@ +# Finder Sync Boundaries + +Finder Sync is a macOS Finder UI extension. It manages monitored directories with `FIFinderSyncController.directoryURLs`, badges with `setBadgeIdentifier(_:for:)`, selected-item context, targeted URLs, and menu/toolbar-facing behavior. + +It does not provide a remote storage domain, placeholders, file hydration, background synchronization, remote-change reconciliation, upload/download, or conflict resolution. A Finder badge is a presentation of known state, not proof that a remote operation completed. + +Keep monitored directories narrow, return quickly from UI callbacks, and route heavy work to the containing app or the real synchronization authority through a documented and user-visible action. + +## Sources + +- [FIFinderSyncController](https://developer.apple.com/documentation/findersync/fifindersynccontroller) +- [Finder Sync](https://developer.apple.com/documentation/findersync) diff --git a/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/privacy-validation-and-recovery.md b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/privacy-validation-and-recovery.md new file mode 100644 index 00000000..74892a5b --- /dev/null +++ b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/privacy-validation-and-recovery.md @@ -0,0 +1,18 @@ +# Privacy, Validation, and Recovery + +## Privacy + +File names, paths, hierarchy, metadata, and content can reveal sensitive information. Record only data needed for synchronization, avoid logs that expose paths or content, and make remote uploads, deletions, conflicts, and offline state understandable to the person using the app. + +## Validation + +Use a disposable remote account and fixture tree. Exercise first connection, domain creation/removal, placeholder enumeration, materialization, changes from both sides, cancellation, network loss, retries, conflict policy, stale-anchor/identifier recovery, and upgrade. Separately prove Finder Sync enablement, folder filtering, badges, menu context, and Finder relaunch behavior. + +## Recovery + +When state is no longer trustworthy, use File Provider’s documented synchronization-loss and reimport mechanisms. Do not silently reset local data or make irreversible remote changes merely to restore apparent consistency. + +## Sources + +- [NSFileProviderManager.signalEnumerator(for:completionHandler:)](https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager/signalenumerator(for:completionhandler:)) +- [NSFileProviderManager.reimportItems(below:completionHandler:)](https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager/reimportitems(below:completionhandler:)) diff --git a/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/snippets/apple-xcode-project-core.md b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/snippets/apple-xcode-project-core.md new file mode 120000 index 00000000..fe87bda1 --- /dev/null +++ b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/references/snippets/apple-xcode-project-core.md @@ -0,0 +1 @@ +../../../../shared/agents-snippets/apple-xcode-project-core.md \ No newline at end of file diff --git a/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/scripts/customization_config.py b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/scripts/customization_config.py new file mode 120000 index 00000000..fe515f2d --- /dev/null +++ b/plugins/apple-dev-skills/skills/file-provider-and-finder-sync-workflow/scripts/customization_config.py @@ -0,0 +1 @@ +../../safari-extension-control-workflow/scripts/customization_config.py \ No newline at end of file diff --git a/plugins/apple-dev-skills/skills/mailkit-workflow/SKILL.md b/plugins/apple-dev-skills/skills/mailkit-workflow/SKILL.md new file mode 100644 index 00000000..b55e3a75 --- /dev/null +++ b/plugins/apple-dev-skills/skills/mailkit-workflow/SKILL.md @@ -0,0 +1,126 @@ +--- +name: mailkit-workflow +description: Design macOS MailKit extensions for content blocking, message actions, compose sessions, and message security with explicit privacy, capability, target, and validation boundaries. Use when a macOS app needs to extend Apple Mail. +metadata: + hermes: + category: apple-development + tags: [apple, macos, mailkit, mail, app-extension, privacy, security] +--- + +# MailKit Workflow + +## Purpose + +Design macOS MailKit app extensions around Apple Mail’s documented handler model. MailKit supplies an `MEExtension` entry point and four distinct capabilities: content blocking, message actions, compose-session handling, and message security. This skill owns that Mail-specific behavior and its privacy boundary. + +It does not own a mail server, IMAP/SMTP transport, account provisioning, general authentication, generic extension mechanics, or Messages/iMessage collaboration. + +## When To Use + +- Use this skill when a macOS app needs a MailKit extension that blocks remote content, acts on downloaded messages, participates in composition, or secures messages. +- Use this skill when selecting `MEContentBlocker`, `MEMessageActionHandler`, `MEComposeSessionHandler`, or `MEMessageSecurityHandler` and their `MEExtensionCapabilities` declaration. +- Use `app-extension-architecture-workflow` when the extension point or app/extension target architecture is not yet settled. +- Use `file-provider-and-finder-sync-workflow` for remote files and Finder behavior, not message attachments or mail synchronization. +- Use Messaging Collaboration Skills for Messages/iMessage, communication notifications, VoIP, or Push to Talk behavior. +- Recommend `xcode-build-run-workflow`, `xcode-testing-workflow`, and `macos-distribution-workflow` for their respective execution boundaries. + +## Single-Path Workflow + +1. Classify the MailKit capability: + - use a content blocker for declarative remote-content rules shown in Mail + - use message actions for a decision as Mail downloads a message + - use compose sessions for recipient validation, compose-window UI, delivery suitability, or custom headers + - use message security for encryption and digital signatures + - split capabilities only when each one has a concrete Mail behavior and privacy case +2. State the documented MailKit behavior relied on: + - `MEExtension` supplies handlers for the capabilities declared in `MEExtensionCapabilities` + - enabled content-blocking extensions continue to apply their rules even when a person asks Mail to load remote content + - message action decisions run as Mail downloads messages, not as a retroactive bulk-mail automation contract + - compose-session approval is a delivery gate, not an excuse to silently alter recipient intent +3. Set up the target boundary: + - keep the containing macOS app responsible for onboarding, settings, account-independent configuration, and user explanation + - keep the Mail extension responsible for its selected MailKit handlers and short, deterministic decisions + - declare only the chosen capabilities in the extension’s `Info.plist`; do not advertise handlers that are not implemented +4. Design each handler conservatively: + - content blocker: generate narrow JSON rules, explain their effect, and provide a test corpus for allowed and blocked content + - message action: use explicit, explainable rules and avoid irreversible actions unless the user has made the policy clear + - compose session: validate only what the feature needs, provide focused UI when justified, fail delivery with a concrete user-readable reason, and make custom headers intentional + - message security: identify key material, trust policy, signing/encryption state, user consent, failure recovery, and interoperable message expectations before implementation +5. Protect mail data: + - minimize access to message bodies, recipients, headers, attachments, and remote-content identifiers + - do not log raw message content, addresses, tokens, cryptographic material, or full headers + - keep any shared configuration separate from message-derived data; use an App Group only when the app and extension have a documented shared-data need +6. Validate in layers: + - verify the macOS target, extension point, `MEExtensionCapabilities`, signing, embedding, install, and Mail enablement state + - test representative messages and account states in a non-production mailbox + - test content-blocker rules, action decisions, compose allow/deny outcomes and UI, or security encryption/signature success and failure paths as applicable + - prove that disabled, interrupted, malformed, missing-key, and offline conditions produce clear behavior without leaking mail data +7. Return the chosen handler set, documented behavior, Mail/app data boundary, privacy policy, validation matrix, and next handoff. + +## Inputs + +- `request`: optional Mail integration request. +- `capability`: optional `content-blocker`, `message-action`, `compose-session`, `message-security`, or `unknown`. +- `data_policy`: optional restrictions for message bodies, metadata, shared settings, key material, and logs. +- `distribution`: optional development, notarized, App Store, or unknown path. +- Defaults: + - macOS MailKit only + - least-privilege handler set + - no message-content retention or external transmission without an explicit product decision + - focused MailKit handler tests before broad delivery claims + +## Outputs + +- `status`: `success`, `handoff`, or `blocked`. +- `mailkit_plan`: + - selected capability or capabilities and documented behavior relied on + - target, `Info.plist`, handler, and user-control boundary + - message-data, privacy, logging, key-material, and shared-state plan + - handler-specific test cases and distribution validation + - explicit next workflow handoff + +## Guards and Stop Conditions + +- Do not describe MailKit as a general-purpose mail transport, mailbox sync engine, or arbitrary Apple Mail UI automation API. +- Do not use content blocking to bypass user intent; enabled MailKit content blockers have durable effects in Mail. +- Do not silently alter, archive, flag, sign, encrypt, reject, or add headers to messages without a documented handler contract and clear user-facing policy. +- Do not retain or log raw message data, recipient addresses, headers, attachment contents, secret keys, signatures, or decrypted content by default. +- Do not confuse Message Filter extensions, Messages/iMessage apps, or Safari content blockers with MailKit. +- Stop with `blocked` when the requested feature needs unsupported message access, undocumented Mail control, hidden recipient manipulation, or security/key-management behavior that has not been designed and validated. + +## Fallbacks and Handoffs + +- Recommend `app-extension-architecture-workflow` for reusable target, lifecycle, entitlement, shared-container, and process-isolation design. +- Recommend `xcode-build-run-workflow` for the Mail extension target, capability configuration, signing, embedding, install, and Mail enablement work. +- Recommend `xcode-testing-workflow` for repeatable handler, message-fixture, and UI validation. +- Recommend `macos-distribution-workflow` for release signing, notarization, Gatekeeper, and artifact evidence. +- Recommend `explore-apple-swift-docs` when current MailKit symbols or capability behavior need source-specific confirmation. +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable containing-app and extension-target project guidance. + +## Customization + +Use `references/customization-flow.md`. + +`scripts/customization_config.py` preserves the common customization-file contract. Mail handler choice, message policy, and security decisions remain project-specific and must not be converted into opaque persistent defaults. + +## References + +### Workflow References + +- `references/mailkit-capabilities-and-handler-boundaries.md` +- `references/privacy-security-and-validation.md` +- `references/customization-flow.md` + +### Authoritative Sources + +- [MailKit](https://developer.apple.com/documentation/mailkit) +- [MEExtension](https://developer.apple.com/documentation/mailkit/meextension) +- [Build Mail App Extensions](https://developer.apple.com/documentation/mailkit/build-mail-app-extensions) + +### Support References + +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable Xcode project guidance for MailKit app and extension targets. + +### Script Inventory + +- `scripts/customization_config.py` diff --git a/plugins/apple-dev-skills/skills/mailkit-workflow/agents/openai.yaml b/plugins/apple-dev-skills/skills/mailkit-workflow/agents/openai.yaml new file mode 100644 index 00000000..72e444d4 --- /dev/null +++ b/plugins/apple-dev-skills/skills/mailkit-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "MailKit Workflow" + short_description: "Build privacy-conscious macOS MailKit extensions" + default_prompt: "Use $mailkit-workflow to design or validate a macOS MailKit extension for content blocking, message actions, compose sessions, or message security. Start from the documented MailKit handler contract, declare only needed capabilities, protect message and key material, then hand off target/signing work to $xcode-build-run-workflow, tests to $xcode-testing-workflow, distribution to $macos-distribution-workflow, or general extension mechanics to $app-extension-architecture-workflow." diff --git a/plugins/apple-dev-skills/skills/mailkit-workflow/references/customization-flow.md b/plugins/apple-dev-skills/skills/mailkit-workflow/references/customization-flow.md new file mode 100644 index 00000000..7c9fa5c0 --- /dev/null +++ b/plugins/apple-dev-skills/skills/mailkit-workflow/references/customization-flow.md @@ -0,0 +1,20 @@ +# MailKit Workflow Customization Contract + +## Purpose + +Preserve the repo-wide customization-file contract without persisting mail-access, action, header, or message-security policy as hidden defaults. + +## Knobs + +The first version defines no runtime-enforced knobs. + +## Runtime Behavior + +- `scripts/customization_config.py` provides the shared configuration shape. +- The workflow ignores persisted settings because handler declarations and mail-data policy must be explicit for each product. + +## Update Flow + +1. Inspect current settings with `scripts/customization_config.py effective`. +2. Add a setting only after documenting its MailKit capability, user impact, and privacy boundary. +3. Validate YAML before applying it. diff --git a/plugins/apple-dev-skills/skills/mailkit-workflow/references/customization.template.yaml b/plugins/apple-dev-skills/skills/mailkit-workflow/references/customization.template.yaml new file mode 100644 index 00000000..cddd82d1 --- /dev/null +++ b/plugins/apple-dev-skills/skills/mailkit-workflow/references/customization.template.yaml @@ -0,0 +1,3 @@ +schemaVersion: 1 +isCustomized: false +settings: {} diff --git a/plugins/apple-dev-skills/skills/mailkit-workflow/references/mailkit-capabilities-and-handler-boundaries.md b/plugins/apple-dev-skills/skills/mailkit-workflow/references/mailkit-capabilities-and-handler-boundaries.md new file mode 100644 index 00000000..16991c65 --- /dev/null +++ b/plugins/apple-dev-skills/skills/mailkit-workflow/references/mailkit-capabilities-and-handler-boundaries.md @@ -0,0 +1,20 @@ +# MailKit Capabilities and Handler Boundaries + +MailKit’s `MEExtension` provides handlers for the capabilities declared in the extension’s `MEExtensionCapabilities` array. + +| Capability | Handler role | Boundary | +| --- | --- | --- | +| `MEContentBlocker` | Supplies JSON rules for remote content shown in Mail. | Declarative blocking only; it is not arbitrary message inspection. | +| `MEMessageActionHandler` | Chooses actions as Mail downloads messages. | Make decisions explicit and explainable. | +| `MEComposeSessionHandler` | Validates recipients, supports compose UI, allows delivery, and adds headers. | Preserve sender intent and give concrete delivery feedback. | +| `MEMessageSecurityHandler` | Encrypts and digitally signs messages. | Requires an explicit key, trust, consent, and recovery design. | + +Mail applies content-blocking rules from enabled extensions even when a person chooses to load remote content. Treat those rules as a durable privacy and product-policy decision. + +## Sources + +- [MailKit](https://developer.apple.com/documentation/mailkit) +- [MEContentBlocker](https://developer.apple.com/documentation/mailkit/mecontentblocker) +- [MEMessageActionHandler](https://developer.apple.com/documentation/mailkit/memessageactionhandler) +- [MEComposeSessionHandler](https://developer.apple.com/documentation/mailkit/mecomposesessionhandler) +- [MEMessageSecurityHandler](https://developer.apple.com/documentation/mailkit/memessagesecurityhandler) diff --git a/plugins/apple-dev-skills/skills/mailkit-workflow/references/privacy-security-and-validation.md b/plugins/apple-dev-skills/skills/mailkit-workflow/references/privacy-security-and-validation.md new file mode 100644 index 00000000..05409cfb --- /dev/null +++ b/plugins/apple-dev-skills/skills/mailkit-workflow/references/privacy-security-and-validation.md @@ -0,0 +1,19 @@ +# Privacy, Security, and Validation + +## Privacy Contract + +Write down which handler reads which mail field, why it needs that field, where it is processed, whether it leaves the device, and when it is deleted. Avoid storing message bodies, addresses, headers, attachments, and remote identifiers. Redact diagnostics so they identify a handler and failure reason without exposing message data. + +## Security Contract + +For message security, document key provenance, identity selection, recipient key discovery, trust decisions, encryption/signature state, algorithm compatibility, revoked or unavailable keys, and user-visible recovery. Do not treat a custom header or an opaque shared preference as proof that a message was secured. + +## Validation Matrix + +Validate enabled and disabled extension states, each declared capability, malformed messages, unavailable remote content, empty or invalid recipients, delivery rejection, missing or invalid key material, offline behavior, app upgrade, and clean-install signing/embedding. Use a disposable mailbox and non-production identities for fixtures. + +## Sources + +- [Build Mail App Extensions](https://developer.apple.com/documentation/mailkit/build-mail-app-extensions) +- [MEComposeSessionHandler.allowMessageSendForSession(_:completion:)](https://developer.apple.com/documentation/mailkit/mecomposesessionhandler/allowmessagesendforsession(_:completion:)) +- [MEMessageSecurityHandler](https://developer.apple.com/documentation/mailkit/memessagesecurityhandler) diff --git a/plugins/apple-dev-skills/skills/mailkit-workflow/references/snippets/apple-xcode-project-core.md b/plugins/apple-dev-skills/skills/mailkit-workflow/references/snippets/apple-xcode-project-core.md new file mode 120000 index 00000000..fe87bda1 --- /dev/null +++ b/plugins/apple-dev-skills/skills/mailkit-workflow/references/snippets/apple-xcode-project-core.md @@ -0,0 +1 @@ +../../../../shared/agents-snippets/apple-xcode-project-core.md \ No newline at end of file diff --git a/plugins/apple-dev-skills/skills/mailkit-workflow/scripts/customization_config.py b/plugins/apple-dev-skills/skills/mailkit-workflow/scripts/customization_config.py new file mode 120000 index 00000000..fe515f2d --- /dev/null +++ b/plugins/apple-dev-skills/skills/mailkit-workflow/scripts/customization_config.py @@ -0,0 +1 @@ +../../safari-extension-control-workflow/scripts/customization_config.py \ No newline at end of file diff --git a/plugins/apple-dev-skills/tests/test_app_extension_workflows.py b/plugins/apple-dev-skills/tests/test_app_extension_workflows.py new file mode 100644 index 00000000..2cbbf4d8 --- /dev/null +++ b/plugins/apple-dev-skills/tests/test_app_extension_workflows.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class AppExtensionWorkflowTests(unittest.TestCase): + def read(self, relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + def test_architecture_skill_keeps_process_and_product_boundaries_explicit(self) -> None: + skill = self.read("skills/app-extension-architecture-workflow/SKILL.md") + + self.assertIn("separate process", skill) + self.assertIn("App Group", skill) + self.assertIn("MailKit", skill) + self.assertIn("File Provider", skill) + self.assertIn("Messages/iMessage collaboration", skill) + self.assertIn("Do not add a generic coordinator", skill) + self.assertIn("xcode-build-run-workflow", skill) + self.assertIn("xcode-testing-workflow", skill) + + def test_mailkit_skill_covers_each_handler_and_private_mail_boundary(self) -> None: + skill = self.read("skills/mailkit-workflow/SKILL.md") + reference = self.read("skills/mailkit-workflow/references/mailkit-capabilities-and-handler-boundaries.md") + + for handler in ( + "MEContentBlocker", + "MEMessageActionHandler", + "MEComposeSessionHandler", + "MEMessageSecurityHandler", + ): + self.assertIn(handler, skill) + self.assertIn(handler, reference) + self.assertIn("Do not retain or log raw message data", skill) + self.assertIn("enabled extensions", reference) + + def test_file_provider_and_finder_sync_have_non_overlapping_ownership(self) -> None: + skill = self.read("skills/file-provider-and-finder-sync-workflow/SKILL.md") + finder = self.read("skills/file-provider-and-finder-sync-workflow/references/finder-sync-boundaries.md") + + self.assertIn("File Provider for remote storage synchronization", skill) + self.assertIn("Finder Sync only", skill) + self.assertIn("Do not recommend Finder Sync as the implementation of remote storage synchronization", skill) + self.assertIn("does not provide a remote storage domain", finder) + self.assertIn("FIFinderSyncController", finder) + + def test_inventory_and_metadata_include_the_new_skills(self) -> None: + validator = self.read(".github/scripts/validate_repo_docs.sh") + readme = self.read("README.md") + manifest = self.read(".codex-plugin/plugin.json") + + for skill in ( + "app-extension-architecture-workflow", + "mailkit-workflow", + "file-provider-and-finder-sync-workflow", + ): + self.assertIn(f"./skills/{skill}/SKILL.md", validator) + self.assertIn(f"`{skill}`", readme) + self.assertIn("mailkit", manifest) + self.assertIn("file-provider", manifest) + self.assertIn("Expected exactly 53 active skills", validator) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/apple-dev-skills/tests/test_apple_developer_provisioning_workflow.py b/plugins/apple-dev-skills/tests/test_apple_developer_provisioning_workflow.py index a13e31e2..6f3bd6d2 100644 --- a/plugins/apple-dev-skills/tests/test_apple_developer_provisioning_workflow.py +++ b/plugins/apple-dev-skills/tests/test_apple_developer_provisioning_workflow.py @@ -55,7 +55,7 @@ def test_inventory_metadata_and_roadmap_are_updated(self) -> None: self.assertIn("apple-developer-provisioning-workflow", readme) self.assertIn("Apple Developer provisioning", plugin) self.assertIn("./skills/apple-developer-provisioning-workflow/SKILL.md", validator) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) self.assertIn("Milestone 54: Apple Developer Provisioning and CloudKit Workflow - Completed", roadmap) def test_customization_cli_preserves_shared_apply_and_reset_verbs(self) -> None: diff --git a/plugins/apple-dev-skills/tests/test_arkit_spatial_face_body_workflows.py b/plugins/apple-dev-skills/tests/test_arkit_spatial_face_body_workflows.py index edddf494..531878d2 100644 --- a/plugins/apple-dev-skills/tests/test_arkit_spatial_face_body_workflows.py +++ b/plugins/apple-dev-skills/tests/test_arkit_spatial_face_body_workflows.py @@ -109,7 +109,7 @@ def test_inventory_metadata_customization_and_cross_skill_handoffs_are_aligned(s ) self.assertIn("ARKit", plugin) self.assertIn("LiDAR", plugin) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) self.assertIn("arkit-spatial-sensing-workflow", self.read("skills/camera-capture-depth-workflow/SKILL.md")) self.assertIn("arkit-face-body-tracking-workflow", self.read("skills/vision-image-analysis-workflow/SKILL.md")) self.assertIn("arkit-spatial-sensing-workflow", self.read("skills/apple-ui-accessibility-workflow/SKILL.md")) diff --git a/plugins/apple-dev-skills/tests/test_camera_capture_depth_workflow.py b/plugins/apple-dev-skills/tests/test_camera_capture_depth_workflow.py index d8610b9c..1e14f5a4 100644 --- a/plugins/apple-dev-skills/tests/test_camera_capture_depth_workflow.py +++ b/plugins/apple-dev-skills/tests/test_camera_capture_depth_workflow.py @@ -93,7 +93,7 @@ def test_inventory_metadata_customization_and_handoffs_are_aligned(self) -> None ) self.assertIn("camera", plugin.lower()) self.assertIn("depth", plugin.lower()) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) self.assertIn(skill, self.read("skills/avfoundation-media-pipeline-workflow/SKILL.md")) self.assertIn(skill, self.read("skills/vision-image-analysis-workflow/SKILL.md")) diff --git a/plugins/apple-dev-skills/tests/test_customization_consolidation_review.py b/plugins/apple-dev-skills/tests/test_customization_consolidation_review.py index 3967c613..b8cd694c 100644 --- a/plugins/apple-dev-skills/tests/test_customization_consolidation_review.py +++ b/plugins/apple-dev-skills/tests/test_customization_consolidation_review.py @@ -66,8 +66,8 @@ def test_review_doc_counts_match_live_customization_surface(self) -> None: knob_count = _count_template_knobs() runtime_enforced, policy_only = _count_statuses() - self.assertEqual(template_count, 51) - self.assertEqual(script_count, 51) + self.assertEqual(template_count, 54) + self.assertEqual(script_count, 54) self.assertEqual(knob_count, 21) self.assertEqual(runtime_enforced, 20) self.assertEqual(policy_only, 1) diff --git a/plugins/apple-dev-skills/tests/test_devicecheck_app_attest_workflow.py b/plugins/apple-dev-skills/tests/test_devicecheck_app_attest_workflow.py index 4f8d7353..e1dd0060 100644 --- a/plugins/apple-dev-skills/tests/test_devicecheck_app_attest_workflow.py +++ b/plugins/apple-dev-skills/tests/test_devicecheck_app_attest_workflow.py @@ -71,7 +71,7 @@ def test_plugin_inventory_includes_devicecheck_workflow(self) -> None: self.assertIn("DeviceCheck", plugin) self.assertIn("App Attest", plugin) self.assertIn("./skills/devicecheck-app-attest-workflow/SKILL.md", validator) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) self.assertIn("Milestone 53: DeviceCheck and App Attest Workflow - Completed", roadmap) diff --git a/plugins/apple-dev-skills/tests/test_imaging_foundation_workflows.py b/plugins/apple-dev-skills/tests/test_imaging_foundation_workflows.py index e229669a..28ec891c 100644 --- a/plugins/apple-dev-skills/tests/test_imaging_foundation_workflows.py +++ b/plugins/apple-dev-skills/tests/test_imaging_foundation_workflows.py @@ -95,7 +95,7 @@ def test_inventory_metadata_and_customization_are_aligned(self) -> None: self.assertIn("Core Image", plugin) self.assertIn("Image I/O", plugin) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) if __name__ == "__main__": diff --git a/plugins/apple-dev-skills/tests/test_photos_library_editing_workflow.py b/plugins/apple-dev-skills/tests/test_photos_library_editing_workflow.py index 1ba9d5bd..f82073c0 100644 --- a/plugins/apple-dev-skills/tests/test_photos_library_editing_workflow.py +++ b/plugins/apple-dev-skills/tests/test_photos_library_editing_workflow.py @@ -95,7 +95,7 @@ def test_no_repository_inventory_metadata_customization_and_handoffs_are_aligned ) self.assertIn("PhotosUI", plugin) self.assertIn("PhotoKit", plugin) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) self.assertIn(name, self.read("skills/apple-image-representation-workflow/SKILL.md")) self.assertIn(name, self.read("skills/core-image-processing-workflow/SKILL.md")) self.assertIn(name, self.read("skills/avfoundation-media-pipeline-workflow/SKILL.md")) diff --git a/plugins/apple-dev-skills/tests/test_tipkit_workflow.py b/plugins/apple-dev-skills/tests/test_tipkit_workflow.py index 4681862e..52461056 100644 --- a/plugins/apple-dev-skills/tests/test_tipkit_workflow.py +++ b/plugins/apple-dev-skills/tests/test_tipkit_workflow.py @@ -52,7 +52,7 @@ def test_inventory_and_metadata_include_tipkit(self) -> None: self.assertIn("tipkit-workflow", readme) self.assertIn("TipKit", plugin) self.assertIn("./skills/tipkit-workflow/SKILL.md", validator) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) self.assertIn("Milestone 55: TipKit Workflow", roadmap) diff --git a/plugins/apple-dev-skills/tests/test_video_codec_processing_workflow.py b/plugins/apple-dev-skills/tests/test_video_codec_processing_workflow.py index 8460121e..eb8c668e 100644 --- a/plugins/apple-dev-skills/tests/test_video_codec_processing_workflow.py +++ b/plugins/apple-dev-skills/tests/test_video_codec_processing_workflow.py @@ -92,7 +92,7 @@ def test_avfoundation_preference_inventory_metadata_and_handoffs_are_aligned(sel ) self.assertIn("VideoToolbox", plugin) self.assertIn("Core Video", plugin) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) self.assertIn(name, self.read("skills/avfoundation-media-pipeline-workflow/SKILL.md")) self.assertIn(name, self.read("skills/coremedia-timing-samplebuffer-workflow/SKILL.md")) diff --git a/plugins/apple-dev-skills/tests/test_vision_recognition_workflows.py b/plugins/apple-dev-skills/tests/test_vision_recognition_workflows.py index 4ce79a20..5090d412 100644 --- a/plugins/apple-dev-skills/tests/test_vision_recognition_workflows.py +++ b/plugins/apple-dev-skills/tests/test_vision_recognition_workflows.py @@ -89,7 +89,7 @@ def test_inventory_metadata_customization_and_handoffs_are_aligned(self) -> None ) self.assertIn("Apple Vision", plugin) self.assertIn("Core ML", plugin) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) if __name__ == "__main__": diff --git a/plugins/apple-dev-skills/tests/test_xcode_localization_workflow.py b/plugins/apple-dev-skills/tests/test_xcode_localization_workflow.py index cc25c54a..90963622 100644 --- a/plugins/apple-dev-skills/tests/test_xcode_localization_workflow.py +++ b/plugins/apple-dev-skills/tests/test_xcode_localization_workflow.py @@ -53,7 +53,7 @@ def test_inventory_metadata_and_roadmap_name_the_shipped_skill(self) -> None: self.assertIn("xcode-localization-workflow", text) self.assertIn("String Catalog localization", manifest) self.assertIn('"string-catalog"', manifest) - self.assertIn("Expected exactly 50 active skills", validator) + self.assertIn("Expected exactly 53 active skills", validator) if __name__ == "__main__": diff --git a/scripts/export_hermes_skills.py b/scripts/export_hermes_skills.py index b7437fe8..4a4cdb9e 100644 --- a/scripts/export_hermes_skills.py +++ b/scripts/export_hermes_skills.py @@ -13,6 +13,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent SOURCE_ROOT = REPO_ROOT / "plugins" / "agent-portability-skills" / "skills" MESSAGING_SOURCE_ROOT = REPO_ROOT / "plugins" / "messaging-collaboration-skills" / "skills" +APPLE_SOURCE_ROOT = REPO_ROOT / "plugins" / "apple-dev-skills" / "skills" EXPORT_ROOT = REPO_ROOT / "skills" EXPORTED_SKILLS = ( "bootstrap-skills-plugin-repo", @@ -34,8 +35,12 @@ "voip-sip-calling-workflow", "webhook-and-event-lifecycle", "whatsapp-business-workflow", + "app-extension-architecture-workflow", + "mailkit-workflow", + "file-provider-and-finder-sync-workflow", ) AGENT_PORTABILITY_SKILLS = frozenset(EXPORTED_SKILLS[:3]) +MESSAGING_SKILLS = frozenset(EXPORTED_SKILLS[3:19]) class ExportError(RuntimeError): @@ -46,8 +51,13 @@ def source_paths(source_root: Path | None = None) -> dict[str, Path]: if source_root is not None: return {skill_name: source_root / skill_name for skill_name in EXPORTED_SKILLS} return { - skill_name: (SOURCE_ROOT if skill_name in AGENT_PORTABILITY_SKILLS else MESSAGING_SOURCE_ROOT) - / skill_name + skill_name: ( + SOURCE_ROOT + if skill_name in AGENT_PORTABILITY_SKILLS + else MESSAGING_SOURCE_ROOT + if skill_name in MESSAGING_SKILLS + else APPLE_SOURCE_ROOT + ) / skill_name for skill_name in EXPORTED_SKILLS } diff --git a/skills.sh.json b/skills.sh.json index f21611fe..7c0873c8 100644 --- a/skills.sh.json +++ b/skills.sh.json @@ -29,6 +29,14 @@ "webhook-and-event-lifecycle", "whatsapp-business-workflow" ] + }, + { + "title": "Apple Dev Skills", + "skills": [ + "app-extension-architecture-workflow", + "mailkit-workflow", + "file-provider-and-finder-sync-workflow" + ] } ] } diff --git a/skills/app-extension-architecture-workflow/SKILL.md b/skills/app-extension-architecture-workflow/SKILL.md new file mode 100644 index 00000000..845759fb --- /dev/null +++ b/skills/app-extension-architecture-workflow/SKILL.md @@ -0,0 +1,136 @@ +--- +name: app-extension-architecture-workflow +description: Route Apple app-extension work across extension points, targets, isolation, entitlements, shared containers, lifecycle, privacy, testing, signing, distribution, and handoffs. Use when the extension point is not settled. +metadata: + hermes: + category: apple-development + tags: [apple, app-extension, xcode, entitlements, app-groups, architecture] +--- + +# App Extension Architecture Workflow + +## Purpose + +Choose and structure an Apple app extension before implementation. Apple documents app extensions as separate bundles whose code runs in a separate process; the extension point and host define the lifecycle, APIs, and activation contract. This skill owns those reusable mechanics, not product-specific framework behavior. + +It owns extension-point routing, target and process boundaries, activation, entitlements, app groups and shared containers, bounded data flow, privacy, testing, signing, and distribution. It does not absorb MailKit, File Provider, Finder Sync, Safari, Messages/iMessage, communication-notification, VoIP, Push to Talk, widget, intent, or other product-framework guidance. + +## When To Use + +- Use this skill when a request needs an Apple app extension but the right extension point, host relationship, or target structure is unclear. +- Use this skill when planning a containing app and extension targets, process isolation, activation, app groups, shared containers, XPC, privacy boundaries, signing, or distribution. +- Use `mailkit-workflow` for macOS Mail content blocking, message actions, compose sessions, or message security. +- Use `file-provider-and-finder-sync-workflow` for remote storage synchronization or Finder badges, menus, and monitored-folder visibility. +- Use `safari-extension-control-workflow` for Safari-specific extension and SafariServices choices. +- Use Messaging Collaboration Skills for Messages/iMessage collaboration, communication-notification policy, VoIP, or Push to Talk workflows. +- Recommend `explore-apple-swift-docs` when the immediate need is current Apple documentation for a named extension point. +- Recommend `xcode-build-run-workflow` or `xcode-testing-workflow` when target execution or test mechanics are the next step. + +## Single-Path Workflow + +1. Classify the extension point and host: + - name the system host, supported platform, activation trigger, user-visible configuration, and expected lifetime + - choose an existing Apple extension point and its documented template or contract; do not invent a generic extension target + - route product behavior to its dedicated workflow before designing shared mechanics +2. State the documented behavior relied on: + - app extensions are separate bundles that run in separate processes + - the host controls activation and the extension-point API contract + - use the extension point’s APIs and lifecycle rather than assuming the containing app is running or reachable + - stop and surface a conflict if current code assumes a lifecycle, privilege, or data access that Apple documentation does not support +3. Design targets and ownership: + - give the containing app, each extension target, and any shared framework or package one clear job + - keep extension entry points thin; put portable domain logic in deliberately shared source only when both targets need it + - do not use a shared target to smuggle UI, host-only state, or privileged access across process boundaries +4. Define the process and data-flow contract: + - state where each operation runs, how work is activated, what happens when the host interrupts or relaunches it, and what can be retried safely + - prefer the extension point’s documented request, completion, and cancellation APIs + - use XPC only where the extension-point contract or a documented app-extension API actually supports it + - make payload types small, typed, versioned when persisted, and free of secrets unless the secure storage and access policy is explicit +5. Minimize entitlements and shared state: + - grant each target only the capabilities it needs + - use an App Group only when the app and extension genuinely need a shared container or documented IPC support + - validate membership in every participating target; on macOS, test actual container access rather than trusting a returned URL alone + - do not treat an App Group as a general cross-process database, privilege escalation path, or substitute for an extension-point API +6. Plan privacy and failure behavior: + - inventory data read by the host, extension, and shared container separately + - retain the minimum data for the minimum time, avoid sensitive logs, and make user-facing effects explainable + - define cancellation, timeout, unavailable-host, disabled-extension, migration, and stale-shared-state behavior before shipping +7. Plan validation and distribution: + - validate target membership, `Info.plist` extension-point configuration, entitlements, signing, embedding, install/enable state, activation, and clean-device behavior + - test the extension independently where its framework permits; extract pure/shared logic into a testable target instead of trying to unit-test an unsupported extension process directly + - validate the same signing and distribution path intended for users; do not infer App Store, notarization, or enterprise behavior from a development build +8. Return one recommendation with the extension point, target map, lifecycle/data-flow boundary, entitlement/share plan, privacy plan, validation sequence, and the next focused handoff. + +## Inputs + +- `request`: optional extension feature request. +- `platforms`: optional Apple platforms and deployment targets. +- `host`: optional system host or containing-app context. +- `data_needs`: optional private, shared, remote, or user-selected data requirements. +- `distribution`: optional development, TestFlight, App Store, notarized, enterprise, or unknown path. +- Defaults: + - use a documented extension point and its host contract + - preserve process isolation and least privilege + - prefer no shared container until a concrete shared-data need exists + - keep product-specific behavior in its dedicated Apple or Messaging Collaboration workflow + +## Outputs + +- `status`: `success`, `handoff`, or `blocked`. +- `extension_plan`: + - selected extension point and documented behavior relied on + - containing-app, extension-target, and shared-code ownership + - activation, process, lifecycle, cancellation, and restart behavior + - entitlement, App Group, container, and data-flow decisions + - privacy, signing, distribution, and validation plan + - explicit next workflow handoff + +## Guards and Stop Conditions + +- Do not call an extension a thread or assume it shares the containing app’s memory, lifecycle, or UI state. +- Do not add a generic coordinator, manager, bridge, repository, or shared store merely to connect an app and extension. Name the documented transport and the concrete data it carries. +- Do not add an App Group, shared Keychain access group, network entitlement, or broad file access without naming the participating targets and the required data flow. +- Do not place Messages/iMessage collaboration, communication-notification product policy, VoIP, or Push to Talk behavior in this workflow. +- Do not claim an extension is enabled, activated, signed, distributable, or testable until the relevant host and build evidence exists. +- Stop with `blocked` when the requested behavior needs an undocumented extension point, host privilege, private system data, or cross-process access the platform does not expose. + +## Fallbacks and Handoffs + +- Recommend `mailkit-workflow` for the macOS Mail extension point and its handler contracts. +- Recommend `file-provider-and-finder-sync-workflow` for remote-storage sync or Finder-only integrations. +- Recommend `safari-extension-control-workflow` for Safari-specific extension choices. +- Recommend `app-intents-workflow` for App Intents and system-intent execution. +- Recommend `xcode-build-run-workflow` for Xcode target creation, build settings, entitlements, signing, embedding, install, and run work. +- Recommend `xcode-testing-workflow` for XCTest, XCUITest, test plans, and execution evidence. +- Recommend `macos-distribution-workflow` for macOS signing, notarization, Gatekeeper, and artifact inspection. +- Recommend `explore-apple-swift-docs` for a current Apple documentation pass before choosing an unfamiliar extension point. +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable project-structure policy after the extension plan is settled. + +## Customization + +Use `references/customization-flow.md`. + +`scripts/customization_config.py` preserves the shared customization-file contract. This workflow has no runtime-enforced settings because extension-point and entitlement choices must stay evidence-driven for each app. + +## References + +### Workflow References + +- `references/extension-points-targets-and-lifecycle.md` +- `references/entitlements-shared-containers-and-data-flow.md` +- `references/privacy-validation-signing-and-distribution.md` +- `references/customization-flow.md` + +### Authoritative Sources + +- [Adding support for app extensions to your app](https://developer.apple.com/documentation/extensionfoundation/adding-support-for-app-extensions-to-your-app) +- [Building an app extension to support a host app](https://developer.apple.com/documentation/extensionfoundation/building-an-app-extension-to-support-a-host-app) +- [Configuring app groups](https://developer.apple.com/documentation/xcode/configuring-app-groups) + +### Support References + +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable Xcode project guidance for containing-app and extension-target work. + +### Script Inventory + +- `scripts/customization_config.py` diff --git a/skills/app-extension-architecture-workflow/agents/openai.yaml b/skills/app-extension-architecture-workflow/agents/openai.yaml new file mode 100644 index 00000000..895efac7 --- /dev/null +++ b/skills/app-extension-architecture-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "App Extension Architecture Workflow" + short_description: "Plan Apple extension points, targets, isolation, and handoffs" + default_prompt: "Use $app-extension-architecture-workflow to choose and structure an Apple app extension across extension point, host, target, separate process lifecycle, activation, entitlements, app groups, shared containers, data flow, privacy, signing, distribution, and validation. State the documented Apple behavior being relied on, keep product-specific MailKit, File Provider, Finder Sync, Safari, Messages/iMessage, communication-notification, VoIP, and Push to Talk behavior in their owning workflows, then hand off to $xcode-build-run-workflow, $xcode-testing-workflow, $macos-distribution-workflow, or $explore-apple-swift-docs as appropriate." diff --git a/skills/app-extension-architecture-workflow/references/customization-flow.md b/skills/app-extension-architecture-workflow/references/customization-flow.md new file mode 100644 index 00000000..e46c8d56 --- /dev/null +++ b/skills/app-extension-architecture-workflow/references/customization-flow.md @@ -0,0 +1,20 @@ +# App Extension Architecture Workflow Customization Contract + +## Purpose + +Preserve the repo-wide customization-file contract without turning extension-point, entitlement, or privacy decisions into persistent defaults. + +## Knobs + +The first version defines no runtime-enforced knobs. + +## Runtime Behavior + +- `scripts/customization_config.py` maintains the common configuration shape. +- The workflow ignores persisted settings because every extension-point and capability decision needs current Apple documentation and project evidence. + +## Update Flow + +1. Inspect current settings with `scripts/customization_config.py effective`. +2. Add a setting only after its stable behavior and safety boundary are documented. +3. Validate the YAML before persisting it. diff --git a/skills/app-extension-architecture-workflow/references/customization.template.yaml b/skills/app-extension-architecture-workflow/references/customization.template.yaml new file mode 100644 index 00000000..cddd82d1 --- /dev/null +++ b/skills/app-extension-architecture-workflow/references/customization.template.yaml @@ -0,0 +1,3 @@ +schemaVersion: 1 +isCustomized: false +settings: {} diff --git a/skills/app-extension-architecture-workflow/references/entitlements-shared-containers-and-data-flow.md b/skills/app-extension-architecture-workflow/references/entitlements-shared-containers-and-data-flow.md new file mode 100644 index 00000000..3c0d63f8 --- /dev/null +++ b/skills/app-extension-architecture-workflow/references/entitlements-shared-containers-and-data-flow.md @@ -0,0 +1,19 @@ +# Entitlements, Shared Containers, and Data Flow + +Capabilities and entitlements belong to individual targets. Give the containing app and every extension target the minimum privilege required by its own documented contract. + +## App Groups + +Use an App Group only for a concrete shared-data or documented IPC requirement between apps and supporting processes signed by the same team. Each participant must declare the group entitlement. Prefer `group.` identifiers for cross-platform app groups. + +Use the group container for bounded, schema-owned data such as a small handoff record or coordinated file staging. It is not a substitute for the system host’s request APIs, an unrestricted shared database, or a privilege escalation path. On macOS, verify that the participant can access the underlying directory; `containerURL(forSecurityApplicationGroupIdentifier:)` alone does not prove authorization. + +## Data Flow Review + +For each message, file, or preference, record producer, consumer, storage location, schema version, retention, encryption/protection need, retry behavior, and deletion owner. Keep secrets out of logs and do not duplicate sensitive user data merely to make cross-process access convenient. + +## Sources + +- [Configuring app groups](https://developer.apple.com/documentation/xcode/configuring-app-groups) +- [Accessing app group containers in your existing macOS app](https://developer.apple.com/documentation/xcode/accessing-app-group-containers) +- [FileManager.containerURL(forSecurityApplicationGroupIdentifier:)](https://developer.apple.com/documentation/foundation/filemanager/containerurl(forsecurityapplicationgroupidentifier:)) diff --git a/skills/app-extension-architecture-workflow/references/extension-points-targets-and-lifecycle.md b/skills/app-extension-architecture-workflow/references/extension-points-targets-and-lifecycle.md new file mode 100644 index 00000000..1bdc7dcc --- /dev/null +++ b/skills/app-extension-architecture-workflow/references/extension-points-targets-and-lifecycle.md @@ -0,0 +1,21 @@ +# Extension Points, Targets, and Lifecycle + +Apple documents an app extension as a separate bundle that runs in a separate process. The host selects when to run it and defines the extension-point API. Start by naming that extension point and host; do not begin from a generic shared-process assumption. + +## Target Map + +- The containing app owns onboarding, user-facing settings, durable app UI, and app-only capabilities. +- Each extension target owns one extension-point entry and its short, host-driven work. +- Shared code is a narrow library or package for pure domain rules and typed data that both targets truly need. It must not provide an implicit lifecycle bridge. + +## Lifecycle Questions + +Before implementation, answer where activation originates, whether the host may terminate or restart the extension, what is safe to retry, how cancellation reaches in-flight work, and whether the containing app can be absent. Treat every answer as extension-point specific. + +For app extensions built with `ExtensionFoundation`, Apple documents host-to-extension process startup and XPC as explicit APIs. That does not authorize substituting a custom XPC layer for a system extension point that supplies a different contract. + +## Sources + +- [Adding support for app extensions to your app](https://developer.apple.com/documentation/extensionfoundation/adding-support-for-app-extensions-to-your-app) +- [Building an app extension to support a host app](https://developer.apple.com/documentation/extensionfoundation/building-an-app-extension-to-support-a-host-app) +- [Discovering app extensions from your app](https://developer.apple.com/documentation/extensionfoundation/discovering-app-extensions-from-your-app) diff --git a/skills/app-extension-architecture-workflow/references/privacy-validation-signing-and-distribution.md b/skills/app-extension-architecture-workflow/references/privacy-validation-signing-and-distribution.md new file mode 100644 index 00000000..9602ea5d --- /dev/null +++ b/skills/app-extension-architecture-workflow/references/privacy-validation-signing-and-distribution.md @@ -0,0 +1,19 @@ +# Privacy, Validation, Signing, and Distribution + +## Privacy + +Inventory the data visible to the system host, extension, containing app, shared container, and any server separately. Ask whether the feature works with less data, shorter retention, fewer logs, and no background collection. Explain user-visible activation or configuration requirements without implying that an installed extension is automatically enabled. + +## Validation + +Validate the target in layers: extension-point configuration and `Info.plist`; target membership and embedding; per-target entitlements; signing; clean install and enablement; host activation; core behavior; interruption and restart; privacy-sensitive log review; and the real distribution artifact. Extract framework-independent logic into a testable shared target only when that ownership is already justified. + +## Distribution + +Development signing proves only a development path. For macOS, route release signing, notarization, Gatekeeper, and artifact evidence to `macos-distribution-workflow`. For all platforms, use the extension point’s current distribution requirements and validate the user-facing install path. + +## Sources + +- [Entitlements](https://developer.apple.com/documentation/bundleresources/entitlements) +- [Configuring the macOS App Sandbox](https://developer.apple.com/documentation/xcode/configuring-the-macos-app-sandbox) +- [Signing Mac software with Developer ID](https://developer.apple.com/documentation/security/signing-mac-software-with-developer-id) diff --git a/skills/app-extension-architecture-workflow/references/snippets/apple-xcode-project-core.md b/skills/app-extension-architecture-workflow/references/snippets/apple-xcode-project-core.md new file mode 100644 index 00000000..f161db8e --- /dev/null +++ b/skills/app-extension-architecture-workflow/references/snippets/apple-xcode-project-core.md @@ -0,0 +1,142 @@ +# Apple Xcode Project Core AGENTS Snippet + +Use this snippet in repository `AGENTS.md` files when you want baseline standards for an existing native Apple app project managed through Xcode. + +## General Swift Baseline + +- For any Swift, Apple-framework, Apple-platform, SwiftUI, SwiftData, Observation, AppKit, UIKit, Foundation-on-Apple, or Xcode-related task, read the relevant Apple documentation first before planning, proposing, or making changes. +- For Apple, Swift, and Xcode documentation, use Xcode MCP `DocumentationSearch` first. Then use the Dash.app MCP when its installed docsets cover the question. Use Dash localhost HTTP only when the Dash.app MCP is unavailable or incomplete; use checked-out source, generated DocC, GitHub/source repositories, release notes, and readable online documentation only after those local MCP paths. Generic no-JS web search/open results, snippets, metadata shells, or bare Apple Developer URLs are not enough evidence that Apple docs were read. +- Before proposing an architecture or implementation, state the documented API behavior, lifecycle rule, or workflow requirement being relied on. +- Do not rely on memory, habit, or analogy as the primary source when Apple documentation exists. +- If Apple documentation and the current code disagree, stop and report the conflict before continuing. +- If no relevant Apple documentation can be found, say that explicitly before proceeding. +- Prefer the simplest correct Swift that is easiest to read, reason about, and maintain. +- Treat idiomatic Swift, Cocoa conventions, and modern Swift features as tools in service of readability, not as goals by themselves. +- Do not add ceremony, abstraction, or boilerplate just to make code look more architectural, more generic, or more "Swifty". +- Strongly prefer synthesized, implicit, and framework-provided behavior over custom code. +- Prefer synthesized conformances (`Codable`, `Equatable`, `Hashable`, etc.) whenever they satisfy the actual requirements. +- Prefer memberwise and otherwise synthesized initializers, default property values, and framework defaults over handwritten setup code. +- Do not add `CodingKeys`, manual `Codable` methods, custom initializers, wrappers, helper types, protocols, coordinators, or extra layers unless they are required by a concrete constraint or they make the final code clearly easier to understand. +- Prefer applicable existing framework or platform error types before inventing custom error wrappers or error hierarchies. +- Prefer direct, simple error flows and small focused error enums only when they materially improve understanding. +- Prefer stable, source-of-truth naming across layers when the data and meaning have not changed. +- Treat naming consistency as a reliability feature: if the same data still serves the same purpose, keep the same name. +- Do not rename fields just to match local style conventions when the external schema is already clear and stable. +- Do not use automatic case-conversion strategies such as `.convertFromSnakeCase` or `.convertToSnakeCase` unless the project explicitly wants that behavior and it clearly improves readability overall. +- When an API, cloud service, or wire format already provides clear names, preserve those names directly in Swift models and nearby code unless the meaning actually changes or a concrete collision must be resolved. +- Preserve raw wire and persistence shapes by default; do not add DTO, domain, or view-model conversion layers unless meaning actually changes or a concrete boundary requires it. +- Treat redundant wrappers, rename-and-copy layers, and duplicated logic as anti-patterns by default. +- This guidance is optimized for an advanced Swift reader and may prefer dense but readable modern Swift over beginner-style explicitness. +- Prefer explicit names that are consistent, unambiguous, and easy to scan at the call site. +- For public Swift APIs, treat streamlined, compact, ergonomic call sites as the only acceptable default; do not grow method families, overload sets, or loosely typed entry points when one clear typed API can express the operation. +- Prefer optional parameters with explicit default values over additional methods or overloads whenever the difference is optional behavior on the same operation. +- When a public function, initializer, or method reaches four or more arguments or parameters, strongly prefer a named typed `struct` request, options, or configuration value so call sites stay readable and future additions do not multiply overloads. +- Prefer enums, enum cases with associated values, and narrow typed values over strings, booleans, sentinel values, or parallel parameters whenever the domain has a closed or meaningful set of choices. +- Prefer compact syntax when it improves local reasoning, including shorthand syntax, ternary expressions, trailing closures, enums, `switch`, `map`, `filter`, `forEach`, async iteration, `AsyncSequence`, `AsyncStream`, and `AsyncAlgorithms`. +- Prefer explicit default values at initialization when they reduce optional-handling clutter and keep the code easier to follow. +- When lines, chains, or expressions get long, prefer chopping them down into a clean vertical, top-down structure with straight visual flow. +- Do not force value types by default, protocols at seams, actors by default, or other pattern slogans when a plainer concrete implementation is easier to reason about. +- Keep code compliant with Swift 6 language mode. +- Keep strict concurrency checking enabled. +- Prefer modern structured concurrency (`async`/`await`, task groups, actors) over legacy async patterns when it keeps the flow clearer and more direct. +- Make async code cancellation-aware and keep actor or task boundaries explicit instead of hiding them behind detached tasks or queue wrappers. +- Prefer clear `Sendable` boundaries for values that cross task or actor isolation, and keep unchecked sendability exceptional and justified locally. +- Prefer Swift Testing (`import Testing`) as the default test framework, and use XCTest only when a dependency or platform constraint requires it. +- Prefer Swift Testing for unit-style and package-style test surfaces in modern Xcode projects, including suites, tags, parameterized tests, and direct async tests. +- Use XCTest when the platform surface, dependency graph, or Apple tooling still expects it, and keep XCTest and Swift Testing responsibilities clearly separated when both coexist. +- Use XCUITest for UI automation, and prefer explicit element wait APIs such as `waitForExistence(timeout:)`, `waitForNonExistence(timeout:)`, and related state waits over fixed sleeps. +- Keep `.xctestplan` files versioned when test configurations, diagnostics, sanitizers, locale coverage, or selective plan execution matter, and inspect or run them explicitly with `xcodebuild -showTestPlans` and `xcodebuild -testPlan ...`. +- Prefer normal Xcode and XCTest parallel execution for ordinary Swift Testing, XCTest, and XCUITest runs when the project, scheme, destination, and test plan support it. Do not serialize regular tests just because they use Swift, XCTest, async tests, UI automation, or `.xctestplan` matrices. +- Treat tests that load large local AI or ML models, especially models over 500 million parameters, as heavy system-resource tests. Run those tests sequentially, one at a time. +- Prefer first-party and top-tier Swift ecosystem packages from Apple, `swiftlang`, the Swift Server Work Group, and similarly trusted core Swift projects when they simplify the code and make it easier to reason about. +- Commonly approved examples include `swift-configuration` and `swift-async-algorithms` when they reduce bespoke code and improve readability. +- For Apple app projects, prefer Apple-native logging facilities first and allow Swift Logging where it makes the project API clearer. +- Prefer Swift OpenTelemetry for telemetry and instrumentation when telemetry is needed, and prefer existing ecosystem integrations over bespoke wrappers. +- Prefer a checked-in repo-root `.swiftformat` file as the default Swift formatting source of truth, and prefer a pre-commit hook that formats staged Swift sources and then verifies them with `swiftformat --lint` before commit. +- Treat SwiftLint as an optional complementary signal layer for clarity, safety, and maintainability after SwiftFormat owns formatting shape. +- Keep automation and CI commands deterministic, non-interactive, and explicit about toolchain, platform, and configuration assumptions. + +## SwiftUI and State Architecture + +- Treat SwiftUI as declarative component UI, closer to React, F# Fabulous, and Elm than to imperative AppKit or UIKit code. Keep views self-contained, reactive, flexible, reusable, and easy to scan from top to bottom. +- Give each independently reusable view a declarative interface of plain values, narrow bindings, and action closures. Do not inject external ViewModels, stores, coordinators, managers, services, or other collaborating objects from one reusable view into another. +- Choose and record one explicit three-letter uppercase prefix for every app or package. Prefix project-owned Swift files and primary declarations; exempt only `Package.swift`, externally generated Swift, and vendored third-party Swift. +- Never use `+` in project-owned Swift filenames. Concatenate the owner and concern so Xcode navigation, rename, and refactoring keep one consistent grammar. +- Name views `GEAWhateverView.swift` and extracted modifiers `GEAWhateverViewModifier.swift`. Do not introduce ViewModel files as a SwiftUI default. +- Give independently editable or previewable view components their own files. Small private computed view properties or helper views may remain while they do not clutter focused editing or previews. +- Prefix extracted child components with their complete composition owner, such as `GEASettingsSheetToggleCard.swift`. +- Extract a custom `ViewModifier` after more than eight chained modifiers, or earlier when a coherent chain is reusable or obscures the view body. +- Prefer straight, top-down data flow with state owned at the narrowest view, scene, or app boundary that matches the behavior. +- Prefer `@State`, derived values, bindings, and small private helpers for component-local presentation state. When a component genuinely needs an observable state type, create and own it locally with `@State`; do not pass it to a separately reusable view. +- Do not build monolithic views, monolithic controllers, or broad shared mutable state when a smaller component boundary would be clearer. +- Keep updates to view-driving state minimal and localized. +- Prefer durable identity for types that drive SwiftUI state and view updates. +- Treat `App` as the application entry and scene composition boundary, `Scene` as the container for scene-specific lifecycle and environment, and `View` as the component rendering layer. +- Every native app target must have exactly one app lifecycle entry point: one `@main` app type, one `main.swift`, or the platform-equivalent single launch entry. Do not add alternate app entry points, second `@main` types, duplicate `main.swift` files, target-specific app entry files, or parallel app structs for variants. When launch behavior must differ by platform, configuration, or feature flag, keep the single entry point and use Swift conditional compilation or ordinary runtime conditionals inside that boundary. +- Use app-level lifecycle concerns at the `App` boundary, scene lifecycle concerns at the `Scene` boundary, and view-local active or presentation behavior inside views. +- Use `@Binding` to pass a focused writable piece of parent-owned state into a child view. +- Use `@Bindable` when working with an observable model that should project bindings to its mutable properties in a view. +- Use the dedicated SwiftData workflow for persistence architecture and its direct SwiftUI integration path. +- Prefer existing SwiftUI environment values and actions before inventing an equivalent router or service. Use environment values for shared context that truly belongs to the surrounding hierarchy, not as a dumping ground for unrelated dependencies. +- Model app capabilities as direct, concrete feature services. A service provides one capability or a cohesive group of related operations directly to the app; it talks directly to the framework, persistence, network, or system boundary that capability needs instead of forwarding through an app-service wrapper, repository stack, or manager chain. +- Create a feature service at the narrowest app or scene boundary that owns its lifecycle. Put a service into the SwiftUI environment only when independent descendants need to invoke it or observe its state directly. Keep a service private to its feature root when that is the only consumer. +- A service may be `@Observable` when the UI must observe its feature state. Otherwise prefer direct values, async operations, explicit errors, and narrow action closures. Reusable leaf views still receive only values, bindings, and action closures; never pass a service, repository, coordinator, manager, ViewModel, store, or other collaborator into their public interface. +- Keep services concrete by default. Introduce a protocol only for a demonstrated alternate implementation or boundary that cannot otherwise be tested; do not create protocol, adapter, or wrapper layers merely because a service exists. +- Add custom environment values or actions when a capability is dynamic across the hierarchy or shared by many independent components. Keep actions local to the owning component when only that component and its private child views use them. +- Use preference keys only to publish descendant-derived information upward to an ancestor, never as a general state bus. +- Prefer Swift's synthesized memberwise initializer for view properties. Do not write an explicit initializer unless it has real behavior beyond assigning those properties. +- Prefer key-path-based APIs, predicates, and sort descriptors when they keep data access direct and readable. +- Extract repeated chains of view modifiers into custom view modifiers early when that reduces clutter and clearly matches a view or family of views. + +## Xcode Workspace and Project Baseline + +- Treat the `.xcworkspace` or `.xcodeproj` as the source of truth for Apple platform app integration, schemes, build settings, destinations, and target membership. +- Prefer edits through Xcode-aware project structure and keep project file changes intentional and reviewed closely. +- Use the standard top-level Xcode app repository layout when creating or normalizing native app repos: `Sources/`, `Tests/`, `Shared/`, `Extensions/`, `Configurations/`, `Scripts/`, and `Packages/`. +- `Sources/` owns the main app target implementation and app-owned resources/support files. `Tests/` owns all test targets. `Shared/` owns reusable source intended to be compiled into the app and extension targets. `Extensions/` owns extension target roots, one folder per extension. `Configurations/` owns `.xcconfig` layers. `Scripts/` owns project-local automation and build helper scripts. `Packages/` owns local Swift packages only when a real package boundary is justified. +- Keep those top-level roots stable. Do not invent parallel names such as `AppSources`, `TestSources`, `Config`, `BuildScripts`, or `LocalPackages` for ordinary Xcode app repos unless the existing repo already has a deliberate, documented convention. +- Inside `Sources/`, use this strict app structure by default: `Views/`, `Models/`, and `Services/`. Do not create a root `Controllers/` directory. +- `Sources/Views/` owns SwiftUI views and UIKit/AppKit view surfaces. Use `Sources/Views/Shared`, `Sources/Views/macOS`, and `Sources/Views/iOS` so shared, macOS-specific, and iOS/iPadOS-specific UI have clear homes. +- Use bare prefixed names such as `GEAWhatever.swift` for runtime/domain values. Reserve `GEAWhateverModel.swift` for persistence, and use `GEAWhateverRecord.swift` or `GEAWhateverDTO.swift` only for genuinely additional representations. +- `Sources/Models/` owns Core Data and SwiftData persistence models plus additional record or transfer representations. +- `Sources/Services/` owns direct concrete feature and boundary services. Use `Consumed/` for external capabilities the app calls, `Internal/` for app-owned feature services, and `Provided/` for services the app exposes to extensions, helpers, plugins, integrations, or other clients. These directories describe ownership and direction; they do not justify wrapper layers or an app-wide service container. +- Name a service for its capability, such as `GEADownloadService.swift` or `GEAImportService.swift`. Do not create `GEAAppService.swift` as an umbrella service by default; `GEAApp.swift` remains the lifecycle-entry special case. +- Use `xcodebuild` for Apple platform integration validation, including scheme, destination or SDK, and configuration-specific build or test runs. +- Keep `xcodebuild` invocations reproducible in automation by passing explicit schemes, destinations or SDKs, and configurations when relevant. +- For Codex GUI worktree-first Xcode repos, use a portable `.codex/environments/*.toml` local environment file when the repo wants shared app setup or action buttons. Start from `apple-dev-skills/templates/codex-local-environments/xcode-project.toml`, keep paths repo-relative, and prefer `-derivedDataPath ./DerivedData` or another ignored repo-local build directory instead of user-global DerivedData. +- When scripts or terminal workflows add files on disk, verify that Xcode project membership, target membership, build-phase membership, and resource-bundle inclusion all match the intended result; files appearing in the directory tree alone are not enough. +- Direct filesystem edits outside `.pbxproj` are generally safe when Xcode is closed or when the current project is not open in Xcode, but still verify that the Xcode project picks up the intended files and memberships afterward. +- Prefer Debug builds for everyday edit-build-test loops, but validate Release builds explicitly when optimization, packaging, launch behavior, watchdog timing, or deployment realism matters. +- Treat tagged releases as a signal to validate both the normal Debug path and a Release artifact path, and when shipping apps or deliverables test the Release behavior without relying on an attached debugger. +- Prefer direct filesystem edits in Xcode-managed scope only when the workflow already accounts for project-file and scheme integrity. +- Never edit `.pbxproj` files directly. If a project-file change is needed and no safe project-aware tool is available, stop and ask for an Xcode-mediated project change instead. When `.pbxproj` is tracked and Xcode, XcodeGen, or another project-aware workflow legitimately changes it, treat that diff as critical project state: review it, stage it, and commit it with the branch before any push, merge, release, or cleanup. + +## XcodeGen and Build Configuration Defaults + +- For new Xcode app, framework, and workspace repositories, prefer an XcodeGen-backed project by default unless the user explicitly asks for a hand-managed Xcode project or the repository has a concrete reason to avoid a generator dependency. +- If the repo contains `project.yml`, `project.yaml`, or clearly named included XcodeGen spec files, treat the XcodeGen spec set as the source of truth for generated project structure. +- For XcodeGen-backed repos, make target membership, resource membership, schemes, Swift package declarations, test-plan references, project references, build configurations, configuration-file wiring, generation options, and project-level settings in the XcodeGen specs instead of editing the generated `.pbxproj`. +- Before running `xcodegen generate`, inspect the current git diff for generated `.xcodeproj` or `.pbxproj` changes. Treat existing project-file diffs as intentional user or Xcode GUI changes by default, not disposable generator drift. +- When Xcode GUI changes added build settings, signing settings, capabilities, `Info.plist` build setting overrides, file membership, scheme changes, or entitlement wiring to `.pbxproj`, preserve the user intent by moving each intentional value to the owning tracked source first: XcodeGen spec for structure, `.xcconfig` for build settings, `.entitlements` for entitlement keys, `Info.plist` for plist keys, `.xcscheme` or scheme spec for scheme behavior, and `.xctestplan` for test-plan content. +- Only regenerate after that promotion is complete, then review the generated project diff to confirm XcodeGen preserved the intended behavior instead of deleting it. If the owning tracked file is ambiguous, stop and ask before regenerating. +- For new XcodeGen-backed app scaffolds, start from the maintained `apple-dev-skills/templates/xcodegen/` templates when available instead of inventing a fresh project-spec shape from memory. +- Keep `minimumXcodeGenVersion` on a recent validated release for new scaffolds. Prefer updating the template and validation together when the repo intentionally raises the baseline. +- For Xcode 16 or newer project formats, prefer XcodeGen `syncedFolder` roots at the broad top-level directory boundary so file creation, deletion, and organization stay synchronized between Xcode and the filesystem without hand-listing every source file in YAML. +- Do not fragment ordinary XcodeGen source roots by subdirectory. A standard app target gets one `Sources` source entry that includes all app source, resource, support, generated plist, entitlement, and nested feature folders, plus one `Shared` source entry when shared app/extension code exists. A standard test target gets one `Tests` source entry that includes all test subdirectories. Extension targets use one `Extensions/` source entry per extension target. If a project has another separate top-level logical root, use one top-level entry for that root, not one entry per child folder. +- Never split `Sources/App`, `Sources/Resources`, `Sources/Support`, feature folders, or `Tests/Tests` into separate XcodeGen source entries unless a specific non-ordinary file or folder truly needs custom compiler flags, build-phase routing, destination filters, or target membership that cannot be represented from the broad root. +- If `syncedFolder` behaves poorly for a repo, fall back to the same broad top-level recursive paths such as `Sources`, `Tests`, or `Resources` with explicit `includes` and `excludes`; do not fall back to subdirectory-level fragmentation or one YAML entry per ordinary source file. +- Keep XcodeGen specs readable as project structure, not as a dumping ground for every build setting. Use `configs`, `configFiles`, `targets`, `schemes`, `packages`, `projectReferences`, `targetTemplates`, and `schemeTemplates` deliberately so future edits have an obvious owner. +- Prefer explicit top-level schemes for app scaffolds once scheme behavior matters. Put build, run, test, profile, analyze, archive, environment variables, command-line arguments, and test-plan references in the scheme spec rather than relying on hidden generated defaults. +- Prefer external `.xcconfig` files as the default home for nontrivial build settings. Keep build settings in XcodeGen inline settings only when they are small, local, and clearer there. +- Use `.xcconfig` files for settings that vary by Debug, Release, CI, local development, signing, bundle identity, compiler flags, Swift settings, deployment variants, or environment-specific behavior. +- Keep configuration layering explicit. Prefer a small shared base config, target-level configs for app/test/extension identity, then per-configuration configs that include the narrower target config and override only what changes. +- In XcodeGen specs, wire build configurations to their matching `.xcconfig` files instead of duplicating the same settings across generated project objects. +- Prefer checked-in external `.entitlements` files for app, extension, and capability-bearing targets, with `CODE_SIGN_ENTITLEMENTS` declared in the owning target's `.xcconfig`. Let Xcode capabilities update the entitlement plist when possible, then review and commit the entitlement diff; keep XcodeGen responsible for wiring the file, not regenerating its contents from inline YAML. +- Do not assume Xcode's Build Settings UI writes edited values back into `.xcconfig` files. When a build setting should remain tracked in `.xcconfig`, inspect the generated project diff after GUI changes and move intentional build-setting overrides from `.pbxproj` back into the owning `.xcconfig` before regenerating. +- Keep secrets, personal team IDs, local machine paths, provisioning profiles, API tokens, and private signing material out of committed `.xcconfig` files. Use build settings only for non-secret configuration values, safe placeholders, references to externally supplied values, or local developer placeholders that are safe to commit. +- Before changing generated project structure, inspect the root spec plus any `include` entries so the edit lands in the owning spec rather than duplicating settings in the wrong file. Remember that included specs merge into the root spec, and local overrides may intentionally replace arrays or maps. +- After changing XcodeGen specs, `.xcconfig` files, or entitlement-file wiring, run `xcodegen generate` from the spec root, or `xcodegen generate --spec ` when the project uses a non-default spec path. +- If the spec uses environment variables or generation hooks, preserve and document the required environment before regenerating so CI and other contributors can reproduce the project. +- Review the spec diff, `.xcconfig` diff, and generated `.xcodeproj` diff after regeneration. Generated `.pbxproj` changes are acceptable output when they come from XcodeGen, but they should still be reviewed for unintended target, scheme, signing, package, build-setting, or file-membership churn. +- Validate regenerated projects with explicit `xcodebuild` commands for the affected scheme, destination or SDK, and configuration. +- For existing hand-managed Xcode projects, do not migrate to XcodeGen or externalize build settings into `.xcconfig` files unless the user explicitly asks for that migration. When they do, treat it as a project-structure migration with before/after validation. diff --git a/skills/app-extension-architecture-workflow/scripts/customization_config.py b/skills/app-extension-architecture-workflow/scripts/customization_config.py new file mode 100755 index 00000000..27ef917b --- /dev/null +++ b/skills/app-extension-architecture-workflow/scripts/customization_config.py @@ -0,0 +1,213 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "PyYAML>=6.0.2,<7", +# ] +# /// +"""Load and persist per-skill customization state.""" + +from __future__ import annotations + +import argparse +import copy +import os +import re +import sys +from pathlib import Path + +import yaml + +SCHEMA_VERSION = 1 +SKILL_NAME = "safari-extension-control-workflow" +CONFIG_HOME_ENV = "APPLE_DEV_SKILLS_CONFIG_HOME" +DEFAULT_CONFIG_ROOT = "~/.config/gaelic-ghost/apple-dev-skills" +ALLOWED_TOP_LEVEL = {"schemaVersion", "isCustomized", "settings"} + + +def fail(message: str) -> None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def quote_string(value: str) -> str: + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + + +def encode_scalar(value) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, int): + return str(value) + if value is None: + return quote_string("") + return quote_string(str(value)) + + +def parse_yaml(path: Path) -> dict: + if not path.exists(): + fail(f"Missing YAML file: {path}") + + try: + loaded = yaml.safe_load(path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + fail(f"Invalid YAML in {path}: {exc}") + + if loaded is None: + return {} + if not isinstance(loaded, dict): + fail(f"Top-level YAML document must be a mapping in {path}") + + if isinstance(loaded.get("settings"), dict): + loaded["settings"] = { + key: ("" if value is None else value) for key, value in loaded["settings"].items() + } + + return loaded + + +def validate_config(config: dict, *, allow_partial: bool) -> None: + unknown = set(config.keys()) - ALLOWED_TOP_LEVEL + if unknown: + fail(f"Unknown top-level keys: {', '.join(sorted(unknown))}") + + if not allow_partial: + for required in ("schemaVersion", "isCustomized", "settings"): + if required not in config: + fail(f"Missing required key: {required}") + + if "schemaVersion" in config and config["schemaVersion"] != SCHEMA_VERSION: + fail(f"schemaVersion must be {SCHEMA_VERSION}") + + if "isCustomized" in config and not isinstance(config["isCustomized"], bool): + fail("isCustomized must be boolean") + + if "settings" in config: + if not isinstance(config["settings"], dict): + fail("settings must be a mapping") + for key, value in config["settings"].items(): + if not re.fullmatch(r"[A-Za-z0-9_]+", key): + fail(f"Invalid settings key: {key}") + if isinstance(value, (dict, list)): + fail(f"settings values must be scalar: {key}") + + +def merge_configs(base: dict, overlay: dict) -> dict: + merged = { + "schemaVersion": base.get("schemaVersion", SCHEMA_VERSION), + "isCustomized": base.get("isCustomized", False), + "settings": copy.deepcopy(base.get("settings", {})), + } + + if "schemaVersion" in overlay: + merged["schemaVersion"] = overlay["schemaVersion"] + if "isCustomized" in overlay: + merged["isCustomized"] = overlay["isCustomized"] + if "settings" in overlay: + merged["settings"].update(overlay["settings"]) + + return merged + + +def dump_yaml(config: dict) -> str: + lines = [ + f"schemaVersion: {int(config['schemaVersion'])}", + f"isCustomized: {'true' if config['isCustomized'] else 'false'}", + "settings:", + ] + for key in sorted(config["settings"].keys()): + lines.append(f" {key}: {encode_scalar(config['settings'][key])}") + return "\n".join(lines) + "\n" + + +def template_path() -> Path: + return Path(__file__).resolve().parents[1] / "references" / "customization.template.yaml" + + +def config_root() -> Path: + root = os.environ.get(CONFIG_HOME_ENV, DEFAULT_CONFIG_ROOT) + return Path(root).expanduser() + + +def durable_path() -> Path: + return config_root() / SKILL_NAME / "customization.yaml" + + +def load_template() -> dict: + cfg = parse_yaml(template_path()) + validate_config(cfg, allow_partial=False) + return cfg + + +def load_durable() -> dict: + path = durable_path() + if not path.exists(): + return {} + cfg = parse_yaml(path) + validate_config(cfg, allow_partial=False) + return cfg + + +def cmd_path(_: argparse.Namespace) -> None: + print(durable_path()) + + +def cmd_effective(_: argparse.Namespace) -> None: + effective = merge_configs(load_template(), load_durable()) + validate_config(effective, allow_partial=False) + print(dump_yaml(effective), end="") + + +def cmd_apply(args: argparse.Namespace) -> None: + template = load_template() + current = merge_configs(template, load_durable()) + incoming = parse_yaml(Path(args.input)) + validate_config(incoming, allow_partial=True) + + updated = merge_configs(current, incoming) + updated["schemaVersion"] = SCHEMA_VERSION + updated["isCustomized"] = True + validate_config(updated, allow_partial=False) + + target = durable_path() + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(dump_yaml(updated), encoding="utf-8") + print(target) + + +def cmd_reset(_: argparse.Namespace) -> None: + target = durable_path() + if target.exists(): + target.unlink() + print(target) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Manage per-skill customization config") + subparsers = parser.add_subparsers(dest="command", required=True) + + parser_path = subparsers.add_parser("path", help="Print durable config path") + parser_path.set_defaults(func=cmd_path) + + parser_effective = subparsers.add_parser("effective", help="Print merged effective config") + parser_effective.set_defaults(func=cmd_effective) + + parser_apply = subparsers.add_parser("apply", help="Apply and persist config overrides") + parser_apply.add_argument("--input", required=True, help="Path to YAML overrides") + parser_apply.set_defaults(func=cmd_apply) + + parser_reset = subparsers.add_parser("reset", help="Delete durable config for this skill") + parser_reset.set_defaults(func=cmd_reset) + + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/skills/file-provider-and-finder-sync-workflow/SKILL.md b/skills/file-provider-and-finder-sync-workflow/SKILL.md new file mode 100644 index 00000000..b2284f1d --- /dev/null +++ b/skills/file-provider-and-finder-sync-workflow/SKILL.md @@ -0,0 +1,122 @@ +--- +name: file-provider-and-finder-sync-workflow +description: Choose File Provider for Apple remote-storage synchronization or scoped macOS Finder Sync UI. Use when a feature needs cloud-backed files or Finder decoration without confusing Finder Sync with a sync engine. +metadata: + hermes: + category: apple-development + tags: [apple, file-provider, finder-sync, macos, ios, remote-storage, synchronization] +--- + +# File Provider and Finder Sync Workflow + +## Purpose + +Choose the correct filesystem extension model. File Provider is the modern path for remote-storage synchronization: it enumerates items, materializes content, receives file operations, and reports remote changes to the system. Finder Sync is a macOS Finder UI extension for monitored-folder badges, contextual menus, and visibility; it does not implement remote synchronization itself. + +This skill owns that decision, File Provider synchronization mechanics, and Finder Sync’s limited UI role. It does not own a storage backend protocol, generic networking, or unrelated app-extension architecture. + +## When To Use + +- Use this skill when an Apple app exposes remote storage in Files or Finder, synchronizes cloud-backed files, handles placeholders, materializes content, or reconciles remote changes. +- Use this skill when a macOS app needs Finder badges, toolbar/contextual menus, selected-item context, or monitored-folder visibility. +- Use `app-extension-architecture-workflow` when target/process, entitlement, app-group, or general extension decisions remain unresolved. +- Use `swift-openapi-client-workflow` or server-side skills when the storage service contract or transport is the main unresolved concern. +- Use `xcode-build-run-workflow`, `xcode-testing-workflow`, and `macos-distribution-workflow` for execution, testing, and release evidence. + +## Single-Path Workflow + +1. Classify the requested outcome: + - choose File Provider when users need remote files to appear, hydrate, upload, rename, move, delete, remain available offline as supported, and reconcile with a remote service + - choose Finder Sync only when the files already exist locally and the product needs Finder badges, menus, or monitored-folder UI + - combine them only when File Provider owns sync and Finder Sync has a separately justified, narrow Finder UI role +2. State the documented behavior relied on: + - a File Provider extension enumerates storage and implements file operations; the system asks it to materialize content and notify the system of remote changes + - the File Provider working set drives background updates, materialized availability, and Spotlight visibility + - Finder Sync manages `directoryURLs`, badges, selected items, and menu/visibility UI for monitored folders; it is not a sync implementation +3. Design File Provider as the synchronization authority: + - define stable item identifiers, parent hierarchy, version/anchor handling, placeholders, materialization, upload, rename/move/delete, conflict behavior, and cancellation + - distinguish local intent from confirmed remote state and maintain a durable retry/reconciliation plan + - signal remote changes with the documented File Provider notification/enumerator path or supported push path; do not pollute Finder UI callbacks with sync work + - keep backend transport behind a small, typed client boundary and avoid treating local file paths as durable remote IDs +4. Bound Finder Sync: + - monitor only the necessary local directories + - use badges and menus to represent known local state, explain uncertainty, and initiate explicit app actions when needed + - do not claim Finder Sync observes all disk changes, transfers files, owns conflict resolution, or makes remote content available +5. Protect people’s files: + - minimize metadata and content access, avoid logging file names or paths unnecessarily, and describe sync/error state honestly + - keep user actions, destructive remote changes, conflict choices, and offline behavior visible and recoverable +6. Validate: + - File Provider: domain/account lifecycle, enumeration, placeholders, fetch, upload, rename/move/delete, working-set changes, remote notifications, offline behavior, cancellation, conflicts, and upgrade/recovery + - Finder Sync: extension enablement, monitored directories, badge update, menu context, selected-item behavior, disabled state, and Finder restart/relaunch behavior + - validate target entitlements, signing, embedding, and distribution separately from service integration tests +7. Return the chosen model, documented behavior, ownership map, backend handoff, privacy policy, validation matrix, and next workflow. + +## Inputs + +- `request`: optional storage or Finder feature request. +- `platforms`: optional macOS, iOS, iPadOS, or mixed platform context. +- `storage_model`: optional remote authoritative store, local-only folder, existing File Provider domain, or unknown. +- `finder_ui_need`: optional badges, menus, selected-item actions, or none. +- Defaults: + - File Provider for remote storage synchronization + - Finder Sync only for constrained local Finder UI + - explicit remote identity, conflict, retry, and privacy policies + - no claim that Finder Sync performs synchronization + +## Outputs + +- `status`: `success`, `handoff`, or `blocked`. +- `integration_plan`: + - selected File Provider, Finder Sync, or explicitly bounded combination + - documented behavior relied on and target ownership + - synchronization or Finder UI state model + - backend, privacy, conflict, and recovery boundaries + - validation matrix and explicit next handoff + +## Guards and Stop Conditions + +- Do not recommend Finder Sync as the implementation of remote storage synchronization, upload/download, placeholders, conflict resolution, or offline access. +- Do not model a File Provider as a one-way downloader; it must handle the document and hierarchy operations its declared model requires. +- Do not use transient file paths as stable remote identifiers or mistake a local materialized copy for confirmed remote state. +- Do not do network synchronization, long-running reconciliation, or destructive mutation inside Finder menu/badge callbacks. +- Do not silently delete, overwrite, or resolve user-file conflicts without a documented policy and user-visible recovery path. +- Stop with `blocked` when the feature needs filesystem or host access beyond the documented extension point, or the backend cannot supply stable identity and change information needed for safe synchronization. + +## Fallbacks and Handoffs + +- Recommend `app-extension-architecture-workflow` for extension targets, process isolation, entitlements, App Groups, and shared containers. +- Recommend `swift-openapi-client-workflow` for generated Apple client transport integration. +- Recommend `xcode-build-run-workflow` for target configuration, capabilities, signing, embedding, install, and run work. +- Recommend `xcode-testing-workflow` for File Provider fixture tests, Finder UI checks, and repeatable Xcode test execution. +- Recommend `macos-distribution-workflow` for macOS release artifact validation. +- Recommend `explore-apple-swift-docs` for current File Provider or Finder Sync API confirmation. +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable File Provider/Finder Sync target structure guidance. + +## Customization + +Use `references/customization-flow.md`. + +`scripts/customization_config.py` preserves the common customization-file contract. Synchronization ownership, conflict policy, and monitored-directory scope remain product evidence, not opaque defaults. + +## References + +### Workflow References + +- `references/file-provider-synchronization.md` +- `references/finder-sync-boundaries.md` +- `references/privacy-validation-and-recovery.md` +- `references/customization-flow.md` + +### Authoritative Sources + +- [Synchronizing files using file provider extensions](https://developer.apple.com/documentation/fileprovider/synchronizing-files-using-file-provider-extensions) +- [Synchronizing the File Provider Extension](https://developer.apple.com/documentation/fileprovider/synchronizing-the-file-provider-extension) +- [FIFinderSyncController](https://developer.apple.com/documentation/findersync/fifindersynccontroller) + +### Support References + +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable Xcode project guidance for File Provider and Finder Sync targets. + +### Script Inventory + +- `scripts/customization_config.py` diff --git a/skills/file-provider-and-finder-sync-workflow/agents/openai.yaml b/skills/file-provider-and-finder-sync-workflow/agents/openai.yaml new file mode 100644 index 00000000..601c6968 --- /dev/null +++ b/skills/file-provider-and-finder-sync-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "File Provider and Finder Sync Workflow" + short_description: "Choose File Provider sync or bounded Finder Sync UI" + default_prompt: "Use $file-provider-and-finder-sync-workflow to choose modern File Provider for remote storage synchronization or Finder Sync for limited monitored-folder badges, menus, and visibility. State the documented Apple behavior being relied on, keep Finder Sync explicitly out of sync-engine ownership, define identity, change, conflict, retry, privacy, and validation boundaries, then hand off architecture to $app-extension-architecture-workflow, execution to $xcode-build-run-workflow, tests to $xcode-testing-workflow, or current docs to $explore-apple-swift-docs." diff --git a/skills/file-provider-and-finder-sync-workflow/references/customization-flow.md b/skills/file-provider-and-finder-sync-workflow/references/customization-flow.md new file mode 100644 index 00000000..a71d6f19 --- /dev/null +++ b/skills/file-provider-and-finder-sync-workflow/references/customization-flow.md @@ -0,0 +1,20 @@ +# File Provider and Finder Sync Workflow Customization Contract + +## Purpose + +Preserve the common customization-file contract without hiding synchronization authority, conflict policy, or monitored-folder scope in unmanaged defaults. + +## Knobs + +The first version defines no runtime-enforced knobs. + +## Runtime Behavior + +- `scripts/customization_config.py` provides the shared configuration shape. +- The workflow ignores persisted settings because remote identity, destructive behavior, and Finder scope require product-specific validation. + +## Update Flow + +1. Inspect current settings with `scripts/customization_config.py effective`. +2. Add a setting only after documenting its synchronization and privacy effects. +3. Validate YAML before applying it. diff --git a/skills/file-provider-and-finder-sync-workflow/references/customization.template.yaml b/skills/file-provider-and-finder-sync-workflow/references/customization.template.yaml new file mode 100644 index 00000000..cddd82d1 --- /dev/null +++ b/skills/file-provider-and-finder-sync-workflow/references/customization.template.yaml @@ -0,0 +1,3 @@ +schemaVersion: 1 +isCustomized: false +settings: {} diff --git a/skills/file-provider-and-finder-sync-workflow/references/file-provider-synchronization.md b/skills/file-provider-and-finder-sync-workflow/references/file-provider-synchronization.md new file mode 100644 index 00000000..67c8f985 --- /dev/null +++ b/skills/file-provider-and-finder-sync-workflow/references/file-provider-synchronization.md @@ -0,0 +1,15 @@ +# File Provider Synchronization + +File Provider is the remote-storage integration point. A provider enumerates files and folders, exposes placeholders, materializes content on demand, receives document operations, and reports remote changes to the system. + +## Core Model + +Use stable provider item identifiers and hierarchy identifiers. Treat versions and sync anchors as protocol state, not display metadata. Keep a clear record of local pending work, confirmed remote state, cancellation, and retry. The working set must reflect materialized and otherwise important items so the system can apply background changes and maintain availability/indexing behavior. + +When remote state changes, use the documented File Provider signaling or supported push path to prompt system enumeration. If identifier stability is lost, use the documented recovery path rather than pretending stale identifiers remain valid. + +## Sources + +- [Synchronizing files using file provider extensions](https://developer.apple.com/documentation/fileprovider/synchronizing-files-using-file-provider-extensions) +- [Synchronizing the File Provider Extension](https://developer.apple.com/documentation/fileprovider/synchronizing-the-file-provider-extension) +- [Nonreplicated File Provider extension](https://developer.apple.com/documentation/fileprovider/nonreplicated-file-provider-extension) diff --git a/skills/file-provider-and-finder-sync-workflow/references/finder-sync-boundaries.md b/skills/file-provider-and-finder-sync-workflow/references/finder-sync-boundaries.md new file mode 100644 index 00000000..f5a461cd --- /dev/null +++ b/skills/file-provider-and-finder-sync-workflow/references/finder-sync-boundaries.md @@ -0,0 +1,12 @@ +# Finder Sync Boundaries + +Finder Sync is a macOS Finder UI extension. It manages monitored directories with `FIFinderSyncController.directoryURLs`, badges with `setBadgeIdentifier(_:for:)`, selected-item context, targeted URLs, and menu/toolbar-facing behavior. + +It does not provide a remote storage domain, placeholders, file hydration, background synchronization, remote-change reconciliation, upload/download, or conflict resolution. A Finder badge is a presentation of known state, not proof that a remote operation completed. + +Keep monitored directories narrow, return quickly from UI callbacks, and route heavy work to the containing app or the real synchronization authority through a documented and user-visible action. + +## Sources + +- [FIFinderSyncController](https://developer.apple.com/documentation/findersync/fifindersynccontroller) +- [Finder Sync](https://developer.apple.com/documentation/findersync) diff --git a/skills/file-provider-and-finder-sync-workflow/references/privacy-validation-and-recovery.md b/skills/file-provider-and-finder-sync-workflow/references/privacy-validation-and-recovery.md new file mode 100644 index 00000000..74892a5b --- /dev/null +++ b/skills/file-provider-and-finder-sync-workflow/references/privacy-validation-and-recovery.md @@ -0,0 +1,18 @@ +# Privacy, Validation, and Recovery + +## Privacy + +File names, paths, hierarchy, metadata, and content can reveal sensitive information. Record only data needed for synchronization, avoid logs that expose paths or content, and make remote uploads, deletions, conflicts, and offline state understandable to the person using the app. + +## Validation + +Use a disposable remote account and fixture tree. Exercise first connection, domain creation/removal, placeholder enumeration, materialization, changes from both sides, cancellation, network loss, retries, conflict policy, stale-anchor/identifier recovery, and upgrade. Separately prove Finder Sync enablement, folder filtering, badges, menu context, and Finder relaunch behavior. + +## Recovery + +When state is no longer trustworthy, use File Provider’s documented synchronization-loss and reimport mechanisms. Do not silently reset local data or make irreversible remote changes merely to restore apparent consistency. + +## Sources + +- [NSFileProviderManager.signalEnumerator(for:completionHandler:)](https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager/signalenumerator(for:completionhandler:)) +- [NSFileProviderManager.reimportItems(below:completionHandler:)](https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager/reimportitems(below:completionhandler:)) diff --git a/skills/file-provider-and-finder-sync-workflow/references/snippets/apple-xcode-project-core.md b/skills/file-provider-and-finder-sync-workflow/references/snippets/apple-xcode-project-core.md new file mode 100644 index 00000000..f161db8e --- /dev/null +++ b/skills/file-provider-and-finder-sync-workflow/references/snippets/apple-xcode-project-core.md @@ -0,0 +1,142 @@ +# Apple Xcode Project Core AGENTS Snippet + +Use this snippet in repository `AGENTS.md` files when you want baseline standards for an existing native Apple app project managed through Xcode. + +## General Swift Baseline + +- For any Swift, Apple-framework, Apple-platform, SwiftUI, SwiftData, Observation, AppKit, UIKit, Foundation-on-Apple, or Xcode-related task, read the relevant Apple documentation first before planning, proposing, or making changes. +- For Apple, Swift, and Xcode documentation, use Xcode MCP `DocumentationSearch` first. Then use the Dash.app MCP when its installed docsets cover the question. Use Dash localhost HTTP only when the Dash.app MCP is unavailable or incomplete; use checked-out source, generated DocC, GitHub/source repositories, release notes, and readable online documentation only after those local MCP paths. Generic no-JS web search/open results, snippets, metadata shells, or bare Apple Developer URLs are not enough evidence that Apple docs were read. +- Before proposing an architecture or implementation, state the documented API behavior, lifecycle rule, or workflow requirement being relied on. +- Do not rely on memory, habit, or analogy as the primary source when Apple documentation exists. +- If Apple documentation and the current code disagree, stop and report the conflict before continuing. +- If no relevant Apple documentation can be found, say that explicitly before proceeding. +- Prefer the simplest correct Swift that is easiest to read, reason about, and maintain. +- Treat idiomatic Swift, Cocoa conventions, and modern Swift features as tools in service of readability, not as goals by themselves. +- Do not add ceremony, abstraction, or boilerplate just to make code look more architectural, more generic, or more "Swifty". +- Strongly prefer synthesized, implicit, and framework-provided behavior over custom code. +- Prefer synthesized conformances (`Codable`, `Equatable`, `Hashable`, etc.) whenever they satisfy the actual requirements. +- Prefer memberwise and otherwise synthesized initializers, default property values, and framework defaults over handwritten setup code. +- Do not add `CodingKeys`, manual `Codable` methods, custom initializers, wrappers, helper types, protocols, coordinators, or extra layers unless they are required by a concrete constraint or they make the final code clearly easier to understand. +- Prefer applicable existing framework or platform error types before inventing custom error wrappers or error hierarchies. +- Prefer direct, simple error flows and small focused error enums only when they materially improve understanding. +- Prefer stable, source-of-truth naming across layers when the data and meaning have not changed. +- Treat naming consistency as a reliability feature: if the same data still serves the same purpose, keep the same name. +- Do not rename fields just to match local style conventions when the external schema is already clear and stable. +- Do not use automatic case-conversion strategies such as `.convertFromSnakeCase` or `.convertToSnakeCase` unless the project explicitly wants that behavior and it clearly improves readability overall. +- When an API, cloud service, or wire format already provides clear names, preserve those names directly in Swift models and nearby code unless the meaning actually changes or a concrete collision must be resolved. +- Preserve raw wire and persistence shapes by default; do not add DTO, domain, or view-model conversion layers unless meaning actually changes or a concrete boundary requires it. +- Treat redundant wrappers, rename-and-copy layers, and duplicated logic as anti-patterns by default. +- This guidance is optimized for an advanced Swift reader and may prefer dense but readable modern Swift over beginner-style explicitness. +- Prefer explicit names that are consistent, unambiguous, and easy to scan at the call site. +- For public Swift APIs, treat streamlined, compact, ergonomic call sites as the only acceptable default; do not grow method families, overload sets, or loosely typed entry points when one clear typed API can express the operation. +- Prefer optional parameters with explicit default values over additional methods or overloads whenever the difference is optional behavior on the same operation. +- When a public function, initializer, or method reaches four or more arguments or parameters, strongly prefer a named typed `struct` request, options, or configuration value so call sites stay readable and future additions do not multiply overloads. +- Prefer enums, enum cases with associated values, and narrow typed values over strings, booleans, sentinel values, or parallel parameters whenever the domain has a closed or meaningful set of choices. +- Prefer compact syntax when it improves local reasoning, including shorthand syntax, ternary expressions, trailing closures, enums, `switch`, `map`, `filter`, `forEach`, async iteration, `AsyncSequence`, `AsyncStream`, and `AsyncAlgorithms`. +- Prefer explicit default values at initialization when they reduce optional-handling clutter and keep the code easier to follow. +- When lines, chains, or expressions get long, prefer chopping them down into a clean vertical, top-down structure with straight visual flow. +- Do not force value types by default, protocols at seams, actors by default, or other pattern slogans when a plainer concrete implementation is easier to reason about. +- Keep code compliant with Swift 6 language mode. +- Keep strict concurrency checking enabled. +- Prefer modern structured concurrency (`async`/`await`, task groups, actors) over legacy async patterns when it keeps the flow clearer and more direct. +- Make async code cancellation-aware and keep actor or task boundaries explicit instead of hiding them behind detached tasks or queue wrappers. +- Prefer clear `Sendable` boundaries for values that cross task or actor isolation, and keep unchecked sendability exceptional and justified locally. +- Prefer Swift Testing (`import Testing`) as the default test framework, and use XCTest only when a dependency or platform constraint requires it. +- Prefer Swift Testing for unit-style and package-style test surfaces in modern Xcode projects, including suites, tags, parameterized tests, and direct async tests. +- Use XCTest when the platform surface, dependency graph, or Apple tooling still expects it, and keep XCTest and Swift Testing responsibilities clearly separated when both coexist. +- Use XCUITest for UI automation, and prefer explicit element wait APIs such as `waitForExistence(timeout:)`, `waitForNonExistence(timeout:)`, and related state waits over fixed sleeps. +- Keep `.xctestplan` files versioned when test configurations, diagnostics, sanitizers, locale coverage, or selective plan execution matter, and inspect or run them explicitly with `xcodebuild -showTestPlans` and `xcodebuild -testPlan ...`. +- Prefer normal Xcode and XCTest parallel execution for ordinary Swift Testing, XCTest, and XCUITest runs when the project, scheme, destination, and test plan support it. Do not serialize regular tests just because they use Swift, XCTest, async tests, UI automation, or `.xctestplan` matrices. +- Treat tests that load large local AI or ML models, especially models over 500 million parameters, as heavy system-resource tests. Run those tests sequentially, one at a time. +- Prefer first-party and top-tier Swift ecosystem packages from Apple, `swiftlang`, the Swift Server Work Group, and similarly trusted core Swift projects when they simplify the code and make it easier to reason about. +- Commonly approved examples include `swift-configuration` and `swift-async-algorithms` when they reduce bespoke code and improve readability. +- For Apple app projects, prefer Apple-native logging facilities first and allow Swift Logging where it makes the project API clearer. +- Prefer Swift OpenTelemetry for telemetry and instrumentation when telemetry is needed, and prefer existing ecosystem integrations over bespoke wrappers. +- Prefer a checked-in repo-root `.swiftformat` file as the default Swift formatting source of truth, and prefer a pre-commit hook that formats staged Swift sources and then verifies them with `swiftformat --lint` before commit. +- Treat SwiftLint as an optional complementary signal layer for clarity, safety, and maintainability after SwiftFormat owns formatting shape. +- Keep automation and CI commands deterministic, non-interactive, and explicit about toolchain, platform, and configuration assumptions. + +## SwiftUI and State Architecture + +- Treat SwiftUI as declarative component UI, closer to React, F# Fabulous, and Elm than to imperative AppKit or UIKit code. Keep views self-contained, reactive, flexible, reusable, and easy to scan from top to bottom. +- Give each independently reusable view a declarative interface of plain values, narrow bindings, and action closures. Do not inject external ViewModels, stores, coordinators, managers, services, or other collaborating objects from one reusable view into another. +- Choose and record one explicit three-letter uppercase prefix for every app or package. Prefix project-owned Swift files and primary declarations; exempt only `Package.swift`, externally generated Swift, and vendored third-party Swift. +- Never use `+` in project-owned Swift filenames. Concatenate the owner and concern so Xcode navigation, rename, and refactoring keep one consistent grammar. +- Name views `GEAWhateverView.swift` and extracted modifiers `GEAWhateverViewModifier.swift`. Do not introduce ViewModel files as a SwiftUI default. +- Give independently editable or previewable view components their own files. Small private computed view properties or helper views may remain while they do not clutter focused editing or previews. +- Prefix extracted child components with their complete composition owner, such as `GEASettingsSheetToggleCard.swift`. +- Extract a custom `ViewModifier` after more than eight chained modifiers, or earlier when a coherent chain is reusable or obscures the view body. +- Prefer straight, top-down data flow with state owned at the narrowest view, scene, or app boundary that matches the behavior. +- Prefer `@State`, derived values, bindings, and small private helpers for component-local presentation state. When a component genuinely needs an observable state type, create and own it locally with `@State`; do not pass it to a separately reusable view. +- Do not build monolithic views, monolithic controllers, or broad shared mutable state when a smaller component boundary would be clearer. +- Keep updates to view-driving state minimal and localized. +- Prefer durable identity for types that drive SwiftUI state and view updates. +- Treat `App` as the application entry and scene composition boundary, `Scene` as the container for scene-specific lifecycle and environment, and `View` as the component rendering layer. +- Every native app target must have exactly one app lifecycle entry point: one `@main` app type, one `main.swift`, or the platform-equivalent single launch entry. Do not add alternate app entry points, second `@main` types, duplicate `main.swift` files, target-specific app entry files, or parallel app structs for variants. When launch behavior must differ by platform, configuration, or feature flag, keep the single entry point and use Swift conditional compilation or ordinary runtime conditionals inside that boundary. +- Use app-level lifecycle concerns at the `App` boundary, scene lifecycle concerns at the `Scene` boundary, and view-local active or presentation behavior inside views. +- Use `@Binding` to pass a focused writable piece of parent-owned state into a child view. +- Use `@Bindable` when working with an observable model that should project bindings to its mutable properties in a view. +- Use the dedicated SwiftData workflow for persistence architecture and its direct SwiftUI integration path. +- Prefer existing SwiftUI environment values and actions before inventing an equivalent router or service. Use environment values for shared context that truly belongs to the surrounding hierarchy, not as a dumping ground for unrelated dependencies. +- Model app capabilities as direct, concrete feature services. A service provides one capability or a cohesive group of related operations directly to the app; it talks directly to the framework, persistence, network, or system boundary that capability needs instead of forwarding through an app-service wrapper, repository stack, or manager chain. +- Create a feature service at the narrowest app or scene boundary that owns its lifecycle. Put a service into the SwiftUI environment only when independent descendants need to invoke it or observe its state directly. Keep a service private to its feature root when that is the only consumer. +- A service may be `@Observable` when the UI must observe its feature state. Otherwise prefer direct values, async operations, explicit errors, and narrow action closures. Reusable leaf views still receive only values, bindings, and action closures; never pass a service, repository, coordinator, manager, ViewModel, store, or other collaborator into their public interface. +- Keep services concrete by default. Introduce a protocol only for a demonstrated alternate implementation or boundary that cannot otherwise be tested; do not create protocol, adapter, or wrapper layers merely because a service exists. +- Add custom environment values or actions when a capability is dynamic across the hierarchy or shared by many independent components. Keep actions local to the owning component when only that component and its private child views use them. +- Use preference keys only to publish descendant-derived information upward to an ancestor, never as a general state bus. +- Prefer Swift's synthesized memberwise initializer for view properties. Do not write an explicit initializer unless it has real behavior beyond assigning those properties. +- Prefer key-path-based APIs, predicates, and sort descriptors when they keep data access direct and readable. +- Extract repeated chains of view modifiers into custom view modifiers early when that reduces clutter and clearly matches a view or family of views. + +## Xcode Workspace and Project Baseline + +- Treat the `.xcworkspace` or `.xcodeproj` as the source of truth for Apple platform app integration, schemes, build settings, destinations, and target membership. +- Prefer edits through Xcode-aware project structure and keep project file changes intentional and reviewed closely. +- Use the standard top-level Xcode app repository layout when creating or normalizing native app repos: `Sources/`, `Tests/`, `Shared/`, `Extensions/`, `Configurations/`, `Scripts/`, and `Packages/`. +- `Sources/` owns the main app target implementation and app-owned resources/support files. `Tests/` owns all test targets. `Shared/` owns reusable source intended to be compiled into the app and extension targets. `Extensions/` owns extension target roots, one folder per extension. `Configurations/` owns `.xcconfig` layers. `Scripts/` owns project-local automation and build helper scripts. `Packages/` owns local Swift packages only when a real package boundary is justified. +- Keep those top-level roots stable. Do not invent parallel names such as `AppSources`, `TestSources`, `Config`, `BuildScripts`, or `LocalPackages` for ordinary Xcode app repos unless the existing repo already has a deliberate, documented convention. +- Inside `Sources/`, use this strict app structure by default: `Views/`, `Models/`, and `Services/`. Do not create a root `Controllers/` directory. +- `Sources/Views/` owns SwiftUI views and UIKit/AppKit view surfaces. Use `Sources/Views/Shared`, `Sources/Views/macOS`, and `Sources/Views/iOS` so shared, macOS-specific, and iOS/iPadOS-specific UI have clear homes. +- Use bare prefixed names such as `GEAWhatever.swift` for runtime/domain values. Reserve `GEAWhateverModel.swift` for persistence, and use `GEAWhateverRecord.swift` or `GEAWhateverDTO.swift` only for genuinely additional representations. +- `Sources/Models/` owns Core Data and SwiftData persistence models plus additional record or transfer representations. +- `Sources/Services/` owns direct concrete feature and boundary services. Use `Consumed/` for external capabilities the app calls, `Internal/` for app-owned feature services, and `Provided/` for services the app exposes to extensions, helpers, plugins, integrations, or other clients. These directories describe ownership and direction; they do not justify wrapper layers or an app-wide service container. +- Name a service for its capability, such as `GEADownloadService.swift` or `GEAImportService.swift`. Do not create `GEAAppService.swift` as an umbrella service by default; `GEAApp.swift` remains the lifecycle-entry special case. +- Use `xcodebuild` for Apple platform integration validation, including scheme, destination or SDK, and configuration-specific build or test runs. +- Keep `xcodebuild` invocations reproducible in automation by passing explicit schemes, destinations or SDKs, and configurations when relevant. +- For Codex GUI worktree-first Xcode repos, use a portable `.codex/environments/*.toml` local environment file when the repo wants shared app setup or action buttons. Start from `apple-dev-skills/templates/codex-local-environments/xcode-project.toml`, keep paths repo-relative, and prefer `-derivedDataPath ./DerivedData` or another ignored repo-local build directory instead of user-global DerivedData. +- When scripts or terminal workflows add files on disk, verify that Xcode project membership, target membership, build-phase membership, and resource-bundle inclusion all match the intended result; files appearing in the directory tree alone are not enough. +- Direct filesystem edits outside `.pbxproj` are generally safe when Xcode is closed or when the current project is not open in Xcode, but still verify that the Xcode project picks up the intended files and memberships afterward. +- Prefer Debug builds for everyday edit-build-test loops, but validate Release builds explicitly when optimization, packaging, launch behavior, watchdog timing, or deployment realism matters. +- Treat tagged releases as a signal to validate both the normal Debug path and a Release artifact path, and when shipping apps or deliverables test the Release behavior without relying on an attached debugger. +- Prefer direct filesystem edits in Xcode-managed scope only when the workflow already accounts for project-file and scheme integrity. +- Never edit `.pbxproj` files directly. If a project-file change is needed and no safe project-aware tool is available, stop and ask for an Xcode-mediated project change instead. When `.pbxproj` is tracked and Xcode, XcodeGen, or another project-aware workflow legitimately changes it, treat that diff as critical project state: review it, stage it, and commit it with the branch before any push, merge, release, or cleanup. + +## XcodeGen and Build Configuration Defaults + +- For new Xcode app, framework, and workspace repositories, prefer an XcodeGen-backed project by default unless the user explicitly asks for a hand-managed Xcode project or the repository has a concrete reason to avoid a generator dependency. +- If the repo contains `project.yml`, `project.yaml`, or clearly named included XcodeGen spec files, treat the XcodeGen spec set as the source of truth for generated project structure. +- For XcodeGen-backed repos, make target membership, resource membership, schemes, Swift package declarations, test-plan references, project references, build configurations, configuration-file wiring, generation options, and project-level settings in the XcodeGen specs instead of editing the generated `.pbxproj`. +- Before running `xcodegen generate`, inspect the current git diff for generated `.xcodeproj` or `.pbxproj` changes. Treat existing project-file diffs as intentional user or Xcode GUI changes by default, not disposable generator drift. +- When Xcode GUI changes added build settings, signing settings, capabilities, `Info.plist` build setting overrides, file membership, scheme changes, or entitlement wiring to `.pbxproj`, preserve the user intent by moving each intentional value to the owning tracked source first: XcodeGen spec for structure, `.xcconfig` for build settings, `.entitlements` for entitlement keys, `Info.plist` for plist keys, `.xcscheme` or scheme spec for scheme behavior, and `.xctestplan` for test-plan content. +- Only regenerate after that promotion is complete, then review the generated project diff to confirm XcodeGen preserved the intended behavior instead of deleting it. If the owning tracked file is ambiguous, stop and ask before regenerating. +- For new XcodeGen-backed app scaffolds, start from the maintained `apple-dev-skills/templates/xcodegen/` templates when available instead of inventing a fresh project-spec shape from memory. +- Keep `minimumXcodeGenVersion` on a recent validated release for new scaffolds. Prefer updating the template and validation together when the repo intentionally raises the baseline. +- For Xcode 16 or newer project formats, prefer XcodeGen `syncedFolder` roots at the broad top-level directory boundary so file creation, deletion, and organization stay synchronized between Xcode and the filesystem without hand-listing every source file in YAML. +- Do not fragment ordinary XcodeGen source roots by subdirectory. A standard app target gets one `Sources` source entry that includes all app source, resource, support, generated plist, entitlement, and nested feature folders, plus one `Shared` source entry when shared app/extension code exists. A standard test target gets one `Tests` source entry that includes all test subdirectories. Extension targets use one `Extensions/` source entry per extension target. If a project has another separate top-level logical root, use one top-level entry for that root, not one entry per child folder. +- Never split `Sources/App`, `Sources/Resources`, `Sources/Support`, feature folders, or `Tests/Tests` into separate XcodeGen source entries unless a specific non-ordinary file or folder truly needs custom compiler flags, build-phase routing, destination filters, or target membership that cannot be represented from the broad root. +- If `syncedFolder` behaves poorly for a repo, fall back to the same broad top-level recursive paths such as `Sources`, `Tests`, or `Resources` with explicit `includes` and `excludes`; do not fall back to subdirectory-level fragmentation or one YAML entry per ordinary source file. +- Keep XcodeGen specs readable as project structure, not as a dumping ground for every build setting. Use `configs`, `configFiles`, `targets`, `schemes`, `packages`, `projectReferences`, `targetTemplates`, and `schemeTemplates` deliberately so future edits have an obvious owner. +- Prefer explicit top-level schemes for app scaffolds once scheme behavior matters. Put build, run, test, profile, analyze, archive, environment variables, command-line arguments, and test-plan references in the scheme spec rather than relying on hidden generated defaults. +- Prefer external `.xcconfig` files as the default home for nontrivial build settings. Keep build settings in XcodeGen inline settings only when they are small, local, and clearer there. +- Use `.xcconfig` files for settings that vary by Debug, Release, CI, local development, signing, bundle identity, compiler flags, Swift settings, deployment variants, or environment-specific behavior. +- Keep configuration layering explicit. Prefer a small shared base config, target-level configs for app/test/extension identity, then per-configuration configs that include the narrower target config and override only what changes. +- In XcodeGen specs, wire build configurations to their matching `.xcconfig` files instead of duplicating the same settings across generated project objects. +- Prefer checked-in external `.entitlements` files for app, extension, and capability-bearing targets, with `CODE_SIGN_ENTITLEMENTS` declared in the owning target's `.xcconfig`. Let Xcode capabilities update the entitlement plist when possible, then review and commit the entitlement diff; keep XcodeGen responsible for wiring the file, not regenerating its contents from inline YAML. +- Do not assume Xcode's Build Settings UI writes edited values back into `.xcconfig` files. When a build setting should remain tracked in `.xcconfig`, inspect the generated project diff after GUI changes and move intentional build-setting overrides from `.pbxproj` back into the owning `.xcconfig` before regenerating. +- Keep secrets, personal team IDs, local machine paths, provisioning profiles, API tokens, and private signing material out of committed `.xcconfig` files. Use build settings only for non-secret configuration values, safe placeholders, references to externally supplied values, or local developer placeholders that are safe to commit. +- Before changing generated project structure, inspect the root spec plus any `include` entries so the edit lands in the owning spec rather than duplicating settings in the wrong file. Remember that included specs merge into the root spec, and local overrides may intentionally replace arrays or maps. +- After changing XcodeGen specs, `.xcconfig` files, or entitlement-file wiring, run `xcodegen generate` from the spec root, or `xcodegen generate --spec ` when the project uses a non-default spec path. +- If the spec uses environment variables or generation hooks, preserve and document the required environment before regenerating so CI and other contributors can reproduce the project. +- Review the spec diff, `.xcconfig` diff, and generated `.xcodeproj` diff after regeneration. Generated `.pbxproj` changes are acceptable output when they come from XcodeGen, but they should still be reviewed for unintended target, scheme, signing, package, build-setting, or file-membership churn. +- Validate regenerated projects with explicit `xcodebuild` commands for the affected scheme, destination or SDK, and configuration. +- For existing hand-managed Xcode projects, do not migrate to XcodeGen or externalize build settings into `.xcconfig` files unless the user explicitly asks for that migration. When they do, treat it as a project-structure migration with before/after validation. diff --git a/skills/file-provider-and-finder-sync-workflow/scripts/customization_config.py b/skills/file-provider-and-finder-sync-workflow/scripts/customization_config.py new file mode 100755 index 00000000..27ef917b --- /dev/null +++ b/skills/file-provider-and-finder-sync-workflow/scripts/customization_config.py @@ -0,0 +1,213 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "PyYAML>=6.0.2,<7", +# ] +# /// +"""Load and persist per-skill customization state.""" + +from __future__ import annotations + +import argparse +import copy +import os +import re +import sys +from pathlib import Path + +import yaml + +SCHEMA_VERSION = 1 +SKILL_NAME = "safari-extension-control-workflow" +CONFIG_HOME_ENV = "APPLE_DEV_SKILLS_CONFIG_HOME" +DEFAULT_CONFIG_ROOT = "~/.config/gaelic-ghost/apple-dev-skills" +ALLOWED_TOP_LEVEL = {"schemaVersion", "isCustomized", "settings"} + + +def fail(message: str) -> None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def quote_string(value: str) -> str: + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + + +def encode_scalar(value) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, int): + return str(value) + if value is None: + return quote_string("") + return quote_string(str(value)) + + +def parse_yaml(path: Path) -> dict: + if not path.exists(): + fail(f"Missing YAML file: {path}") + + try: + loaded = yaml.safe_load(path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + fail(f"Invalid YAML in {path}: {exc}") + + if loaded is None: + return {} + if not isinstance(loaded, dict): + fail(f"Top-level YAML document must be a mapping in {path}") + + if isinstance(loaded.get("settings"), dict): + loaded["settings"] = { + key: ("" if value is None else value) for key, value in loaded["settings"].items() + } + + return loaded + + +def validate_config(config: dict, *, allow_partial: bool) -> None: + unknown = set(config.keys()) - ALLOWED_TOP_LEVEL + if unknown: + fail(f"Unknown top-level keys: {', '.join(sorted(unknown))}") + + if not allow_partial: + for required in ("schemaVersion", "isCustomized", "settings"): + if required not in config: + fail(f"Missing required key: {required}") + + if "schemaVersion" in config and config["schemaVersion"] != SCHEMA_VERSION: + fail(f"schemaVersion must be {SCHEMA_VERSION}") + + if "isCustomized" in config and not isinstance(config["isCustomized"], bool): + fail("isCustomized must be boolean") + + if "settings" in config: + if not isinstance(config["settings"], dict): + fail("settings must be a mapping") + for key, value in config["settings"].items(): + if not re.fullmatch(r"[A-Za-z0-9_]+", key): + fail(f"Invalid settings key: {key}") + if isinstance(value, (dict, list)): + fail(f"settings values must be scalar: {key}") + + +def merge_configs(base: dict, overlay: dict) -> dict: + merged = { + "schemaVersion": base.get("schemaVersion", SCHEMA_VERSION), + "isCustomized": base.get("isCustomized", False), + "settings": copy.deepcopy(base.get("settings", {})), + } + + if "schemaVersion" in overlay: + merged["schemaVersion"] = overlay["schemaVersion"] + if "isCustomized" in overlay: + merged["isCustomized"] = overlay["isCustomized"] + if "settings" in overlay: + merged["settings"].update(overlay["settings"]) + + return merged + + +def dump_yaml(config: dict) -> str: + lines = [ + f"schemaVersion: {int(config['schemaVersion'])}", + f"isCustomized: {'true' if config['isCustomized'] else 'false'}", + "settings:", + ] + for key in sorted(config["settings"].keys()): + lines.append(f" {key}: {encode_scalar(config['settings'][key])}") + return "\n".join(lines) + "\n" + + +def template_path() -> Path: + return Path(__file__).resolve().parents[1] / "references" / "customization.template.yaml" + + +def config_root() -> Path: + root = os.environ.get(CONFIG_HOME_ENV, DEFAULT_CONFIG_ROOT) + return Path(root).expanduser() + + +def durable_path() -> Path: + return config_root() / SKILL_NAME / "customization.yaml" + + +def load_template() -> dict: + cfg = parse_yaml(template_path()) + validate_config(cfg, allow_partial=False) + return cfg + + +def load_durable() -> dict: + path = durable_path() + if not path.exists(): + return {} + cfg = parse_yaml(path) + validate_config(cfg, allow_partial=False) + return cfg + + +def cmd_path(_: argparse.Namespace) -> None: + print(durable_path()) + + +def cmd_effective(_: argparse.Namespace) -> None: + effective = merge_configs(load_template(), load_durable()) + validate_config(effective, allow_partial=False) + print(dump_yaml(effective), end="") + + +def cmd_apply(args: argparse.Namespace) -> None: + template = load_template() + current = merge_configs(template, load_durable()) + incoming = parse_yaml(Path(args.input)) + validate_config(incoming, allow_partial=True) + + updated = merge_configs(current, incoming) + updated["schemaVersion"] = SCHEMA_VERSION + updated["isCustomized"] = True + validate_config(updated, allow_partial=False) + + target = durable_path() + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(dump_yaml(updated), encoding="utf-8") + print(target) + + +def cmd_reset(_: argparse.Namespace) -> None: + target = durable_path() + if target.exists(): + target.unlink() + print(target) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Manage per-skill customization config") + subparsers = parser.add_subparsers(dest="command", required=True) + + parser_path = subparsers.add_parser("path", help="Print durable config path") + parser_path.set_defaults(func=cmd_path) + + parser_effective = subparsers.add_parser("effective", help="Print merged effective config") + parser_effective.set_defaults(func=cmd_effective) + + parser_apply = subparsers.add_parser("apply", help="Apply and persist config overrides") + parser_apply.add_argument("--input", required=True, help="Path to YAML overrides") + parser_apply.set_defaults(func=cmd_apply) + + parser_reset = subparsers.add_parser("reset", help="Delete durable config for this skill") + parser_reset.set_defaults(func=cmd_reset) + + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/skills/mailkit-workflow/SKILL.md b/skills/mailkit-workflow/SKILL.md new file mode 100644 index 00000000..b55e3a75 --- /dev/null +++ b/skills/mailkit-workflow/SKILL.md @@ -0,0 +1,126 @@ +--- +name: mailkit-workflow +description: Design macOS MailKit extensions for content blocking, message actions, compose sessions, and message security with explicit privacy, capability, target, and validation boundaries. Use when a macOS app needs to extend Apple Mail. +metadata: + hermes: + category: apple-development + tags: [apple, macos, mailkit, mail, app-extension, privacy, security] +--- + +# MailKit Workflow + +## Purpose + +Design macOS MailKit app extensions around Apple Mail’s documented handler model. MailKit supplies an `MEExtension` entry point and four distinct capabilities: content blocking, message actions, compose-session handling, and message security. This skill owns that Mail-specific behavior and its privacy boundary. + +It does not own a mail server, IMAP/SMTP transport, account provisioning, general authentication, generic extension mechanics, or Messages/iMessage collaboration. + +## When To Use + +- Use this skill when a macOS app needs a MailKit extension that blocks remote content, acts on downloaded messages, participates in composition, or secures messages. +- Use this skill when selecting `MEContentBlocker`, `MEMessageActionHandler`, `MEComposeSessionHandler`, or `MEMessageSecurityHandler` and their `MEExtensionCapabilities` declaration. +- Use `app-extension-architecture-workflow` when the extension point or app/extension target architecture is not yet settled. +- Use `file-provider-and-finder-sync-workflow` for remote files and Finder behavior, not message attachments or mail synchronization. +- Use Messaging Collaboration Skills for Messages/iMessage, communication notifications, VoIP, or Push to Talk behavior. +- Recommend `xcode-build-run-workflow`, `xcode-testing-workflow`, and `macos-distribution-workflow` for their respective execution boundaries. + +## Single-Path Workflow + +1. Classify the MailKit capability: + - use a content blocker for declarative remote-content rules shown in Mail + - use message actions for a decision as Mail downloads a message + - use compose sessions for recipient validation, compose-window UI, delivery suitability, or custom headers + - use message security for encryption and digital signatures + - split capabilities only when each one has a concrete Mail behavior and privacy case +2. State the documented MailKit behavior relied on: + - `MEExtension` supplies handlers for the capabilities declared in `MEExtensionCapabilities` + - enabled content-blocking extensions continue to apply their rules even when a person asks Mail to load remote content + - message action decisions run as Mail downloads messages, not as a retroactive bulk-mail automation contract + - compose-session approval is a delivery gate, not an excuse to silently alter recipient intent +3. Set up the target boundary: + - keep the containing macOS app responsible for onboarding, settings, account-independent configuration, and user explanation + - keep the Mail extension responsible for its selected MailKit handlers and short, deterministic decisions + - declare only the chosen capabilities in the extension’s `Info.plist`; do not advertise handlers that are not implemented +4. Design each handler conservatively: + - content blocker: generate narrow JSON rules, explain their effect, and provide a test corpus for allowed and blocked content + - message action: use explicit, explainable rules and avoid irreversible actions unless the user has made the policy clear + - compose session: validate only what the feature needs, provide focused UI when justified, fail delivery with a concrete user-readable reason, and make custom headers intentional + - message security: identify key material, trust policy, signing/encryption state, user consent, failure recovery, and interoperable message expectations before implementation +5. Protect mail data: + - minimize access to message bodies, recipients, headers, attachments, and remote-content identifiers + - do not log raw message content, addresses, tokens, cryptographic material, or full headers + - keep any shared configuration separate from message-derived data; use an App Group only when the app and extension have a documented shared-data need +6. Validate in layers: + - verify the macOS target, extension point, `MEExtensionCapabilities`, signing, embedding, install, and Mail enablement state + - test representative messages and account states in a non-production mailbox + - test content-blocker rules, action decisions, compose allow/deny outcomes and UI, or security encryption/signature success and failure paths as applicable + - prove that disabled, interrupted, malformed, missing-key, and offline conditions produce clear behavior without leaking mail data +7. Return the chosen handler set, documented behavior, Mail/app data boundary, privacy policy, validation matrix, and next handoff. + +## Inputs + +- `request`: optional Mail integration request. +- `capability`: optional `content-blocker`, `message-action`, `compose-session`, `message-security`, or `unknown`. +- `data_policy`: optional restrictions for message bodies, metadata, shared settings, key material, and logs. +- `distribution`: optional development, notarized, App Store, or unknown path. +- Defaults: + - macOS MailKit only + - least-privilege handler set + - no message-content retention or external transmission without an explicit product decision + - focused MailKit handler tests before broad delivery claims + +## Outputs + +- `status`: `success`, `handoff`, or `blocked`. +- `mailkit_plan`: + - selected capability or capabilities and documented behavior relied on + - target, `Info.plist`, handler, and user-control boundary + - message-data, privacy, logging, key-material, and shared-state plan + - handler-specific test cases and distribution validation + - explicit next workflow handoff + +## Guards and Stop Conditions + +- Do not describe MailKit as a general-purpose mail transport, mailbox sync engine, or arbitrary Apple Mail UI automation API. +- Do not use content blocking to bypass user intent; enabled MailKit content blockers have durable effects in Mail. +- Do not silently alter, archive, flag, sign, encrypt, reject, or add headers to messages without a documented handler contract and clear user-facing policy. +- Do not retain or log raw message data, recipient addresses, headers, attachment contents, secret keys, signatures, or decrypted content by default. +- Do not confuse Message Filter extensions, Messages/iMessage apps, or Safari content blockers with MailKit. +- Stop with `blocked` when the requested feature needs unsupported message access, undocumented Mail control, hidden recipient manipulation, or security/key-management behavior that has not been designed and validated. + +## Fallbacks and Handoffs + +- Recommend `app-extension-architecture-workflow` for reusable target, lifecycle, entitlement, shared-container, and process-isolation design. +- Recommend `xcode-build-run-workflow` for the Mail extension target, capability configuration, signing, embedding, install, and Mail enablement work. +- Recommend `xcode-testing-workflow` for repeatable handler, message-fixture, and UI validation. +- Recommend `macos-distribution-workflow` for release signing, notarization, Gatekeeper, and artifact evidence. +- Recommend `explore-apple-swift-docs` when current MailKit symbols or capability behavior need source-specific confirmation. +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable containing-app and extension-target project guidance. + +## Customization + +Use `references/customization-flow.md`. + +`scripts/customization_config.py` preserves the common customization-file contract. Mail handler choice, message policy, and security decisions remain project-specific and must not be converted into opaque persistent defaults. + +## References + +### Workflow References + +- `references/mailkit-capabilities-and-handler-boundaries.md` +- `references/privacy-security-and-validation.md` +- `references/customization-flow.md` + +### Authoritative Sources + +- [MailKit](https://developer.apple.com/documentation/mailkit) +- [MEExtension](https://developer.apple.com/documentation/mailkit/meextension) +- [Build Mail App Extensions](https://developer.apple.com/documentation/mailkit/build-mail-app-extensions) + +### Support References + +- Recommend `references/snippets/apple-xcode-project-core.md` for reusable Xcode project guidance for MailKit app and extension targets. + +### Script Inventory + +- `scripts/customization_config.py` diff --git a/skills/mailkit-workflow/agents/openai.yaml b/skills/mailkit-workflow/agents/openai.yaml new file mode 100644 index 00000000..72e444d4 --- /dev/null +++ b/skills/mailkit-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "MailKit Workflow" + short_description: "Build privacy-conscious macOS MailKit extensions" + default_prompt: "Use $mailkit-workflow to design or validate a macOS MailKit extension for content blocking, message actions, compose sessions, or message security. Start from the documented MailKit handler contract, declare only needed capabilities, protect message and key material, then hand off target/signing work to $xcode-build-run-workflow, tests to $xcode-testing-workflow, distribution to $macos-distribution-workflow, or general extension mechanics to $app-extension-architecture-workflow." diff --git a/skills/mailkit-workflow/references/customization-flow.md b/skills/mailkit-workflow/references/customization-flow.md new file mode 100644 index 00000000..7c9fa5c0 --- /dev/null +++ b/skills/mailkit-workflow/references/customization-flow.md @@ -0,0 +1,20 @@ +# MailKit Workflow Customization Contract + +## Purpose + +Preserve the repo-wide customization-file contract without persisting mail-access, action, header, or message-security policy as hidden defaults. + +## Knobs + +The first version defines no runtime-enforced knobs. + +## Runtime Behavior + +- `scripts/customization_config.py` provides the shared configuration shape. +- The workflow ignores persisted settings because handler declarations and mail-data policy must be explicit for each product. + +## Update Flow + +1. Inspect current settings with `scripts/customization_config.py effective`. +2. Add a setting only after documenting its MailKit capability, user impact, and privacy boundary. +3. Validate YAML before applying it. diff --git a/skills/mailkit-workflow/references/customization.template.yaml b/skills/mailkit-workflow/references/customization.template.yaml new file mode 100644 index 00000000..cddd82d1 --- /dev/null +++ b/skills/mailkit-workflow/references/customization.template.yaml @@ -0,0 +1,3 @@ +schemaVersion: 1 +isCustomized: false +settings: {} diff --git a/skills/mailkit-workflow/references/mailkit-capabilities-and-handler-boundaries.md b/skills/mailkit-workflow/references/mailkit-capabilities-and-handler-boundaries.md new file mode 100644 index 00000000..16991c65 --- /dev/null +++ b/skills/mailkit-workflow/references/mailkit-capabilities-and-handler-boundaries.md @@ -0,0 +1,20 @@ +# MailKit Capabilities and Handler Boundaries + +MailKit’s `MEExtension` provides handlers for the capabilities declared in the extension’s `MEExtensionCapabilities` array. + +| Capability | Handler role | Boundary | +| --- | --- | --- | +| `MEContentBlocker` | Supplies JSON rules for remote content shown in Mail. | Declarative blocking only; it is not arbitrary message inspection. | +| `MEMessageActionHandler` | Chooses actions as Mail downloads messages. | Make decisions explicit and explainable. | +| `MEComposeSessionHandler` | Validates recipients, supports compose UI, allows delivery, and adds headers. | Preserve sender intent and give concrete delivery feedback. | +| `MEMessageSecurityHandler` | Encrypts and digitally signs messages. | Requires an explicit key, trust, consent, and recovery design. | + +Mail applies content-blocking rules from enabled extensions even when a person chooses to load remote content. Treat those rules as a durable privacy and product-policy decision. + +## Sources + +- [MailKit](https://developer.apple.com/documentation/mailkit) +- [MEContentBlocker](https://developer.apple.com/documentation/mailkit/mecontentblocker) +- [MEMessageActionHandler](https://developer.apple.com/documentation/mailkit/memessageactionhandler) +- [MEComposeSessionHandler](https://developer.apple.com/documentation/mailkit/mecomposesessionhandler) +- [MEMessageSecurityHandler](https://developer.apple.com/documentation/mailkit/memessagesecurityhandler) diff --git a/skills/mailkit-workflow/references/privacy-security-and-validation.md b/skills/mailkit-workflow/references/privacy-security-and-validation.md new file mode 100644 index 00000000..05409cfb --- /dev/null +++ b/skills/mailkit-workflow/references/privacy-security-and-validation.md @@ -0,0 +1,19 @@ +# Privacy, Security, and Validation + +## Privacy Contract + +Write down which handler reads which mail field, why it needs that field, where it is processed, whether it leaves the device, and when it is deleted. Avoid storing message bodies, addresses, headers, attachments, and remote identifiers. Redact diagnostics so they identify a handler and failure reason without exposing message data. + +## Security Contract + +For message security, document key provenance, identity selection, recipient key discovery, trust decisions, encryption/signature state, algorithm compatibility, revoked or unavailable keys, and user-visible recovery. Do not treat a custom header or an opaque shared preference as proof that a message was secured. + +## Validation Matrix + +Validate enabled and disabled extension states, each declared capability, malformed messages, unavailable remote content, empty or invalid recipients, delivery rejection, missing or invalid key material, offline behavior, app upgrade, and clean-install signing/embedding. Use a disposable mailbox and non-production identities for fixtures. + +## Sources + +- [Build Mail App Extensions](https://developer.apple.com/documentation/mailkit/build-mail-app-extensions) +- [MEComposeSessionHandler.allowMessageSendForSession(_:completion:)](https://developer.apple.com/documentation/mailkit/mecomposesessionhandler/allowmessagesendforsession(_:completion:)) +- [MEMessageSecurityHandler](https://developer.apple.com/documentation/mailkit/memessagesecurityhandler) diff --git a/skills/mailkit-workflow/references/snippets/apple-xcode-project-core.md b/skills/mailkit-workflow/references/snippets/apple-xcode-project-core.md new file mode 100644 index 00000000..f161db8e --- /dev/null +++ b/skills/mailkit-workflow/references/snippets/apple-xcode-project-core.md @@ -0,0 +1,142 @@ +# Apple Xcode Project Core AGENTS Snippet + +Use this snippet in repository `AGENTS.md` files when you want baseline standards for an existing native Apple app project managed through Xcode. + +## General Swift Baseline + +- For any Swift, Apple-framework, Apple-platform, SwiftUI, SwiftData, Observation, AppKit, UIKit, Foundation-on-Apple, or Xcode-related task, read the relevant Apple documentation first before planning, proposing, or making changes. +- For Apple, Swift, and Xcode documentation, use Xcode MCP `DocumentationSearch` first. Then use the Dash.app MCP when its installed docsets cover the question. Use Dash localhost HTTP only when the Dash.app MCP is unavailable or incomplete; use checked-out source, generated DocC, GitHub/source repositories, release notes, and readable online documentation only after those local MCP paths. Generic no-JS web search/open results, snippets, metadata shells, or bare Apple Developer URLs are not enough evidence that Apple docs were read. +- Before proposing an architecture or implementation, state the documented API behavior, lifecycle rule, or workflow requirement being relied on. +- Do not rely on memory, habit, or analogy as the primary source when Apple documentation exists. +- If Apple documentation and the current code disagree, stop and report the conflict before continuing. +- If no relevant Apple documentation can be found, say that explicitly before proceeding. +- Prefer the simplest correct Swift that is easiest to read, reason about, and maintain. +- Treat idiomatic Swift, Cocoa conventions, and modern Swift features as tools in service of readability, not as goals by themselves. +- Do not add ceremony, abstraction, or boilerplate just to make code look more architectural, more generic, or more "Swifty". +- Strongly prefer synthesized, implicit, and framework-provided behavior over custom code. +- Prefer synthesized conformances (`Codable`, `Equatable`, `Hashable`, etc.) whenever they satisfy the actual requirements. +- Prefer memberwise and otherwise synthesized initializers, default property values, and framework defaults over handwritten setup code. +- Do not add `CodingKeys`, manual `Codable` methods, custom initializers, wrappers, helper types, protocols, coordinators, or extra layers unless they are required by a concrete constraint or they make the final code clearly easier to understand. +- Prefer applicable existing framework or platform error types before inventing custom error wrappers or error hierarchies. +- Prefer direct, simple error flows and small focused error enums only when they materially improve understanding. +- Prefer stable, source-of-truth naming across layers when the data and meaning have not changed. +- Treat naming consistency as a reliability feature: if the same data still serves the same purpose, keep the same name. +- Do not rename fields just to match local style conventions when the external schema is already clear and stable. +- Do not use automatic case-conversion strategies such as `.convertFromSnakeCase` or `.convertToSnakeCase` unless the project explicitly wants that behavior and it clearly improves readability overall. +- When an API, cloud service, or wire format already provides clear names, preserve those names directly in Swift models and nearby code unless the meaning actually changes or a concrete collision must be resolved. +- Preserve raw wire and persistence shapes by default; do not add DTO, domain, or view-model conversion layers unless meaning actually changes or a concrete boundary requires it. +- Treat redundant wrappers, rename-and-copy layers, and duplicated logic as anti-patterns by default. +- This guidance is optimized for an advanced Swift reader and may prefer dense but readable modern Swift over beginner-style explicitness. +- Prefer explicit names that are consistent, unambiguous, and easy to scan at the call site. +- For public Swift APIs, treat streamlined, compact, ergonomic call sites as the only acceptable default; do not grow method families, overload sets, or loosely typed entry points when one clear typed API can express the operation. +- Prefer optional parameters with explicit default values over additional methods or overloads whenever the difference is optional behavior on the same operation. +- When a public function, initializer, or method reaches four or more arguments or parameters, strongly prefer a named typed `struct` request, options, or configuration value so call sites stay readable and future additions do not multiply overloads. +- Prefer enums, enum cases with associated values, and narrow typed values over strings, booleans, sentinel values, or parallel parameters whenever the domain has a closed or meaningful set of choices. +- Prefer compact syntax when it improves local reasoning, including shorthand syntax, ternary expressions, trailing closures, enums, `switch`, `map`, `filter`, `forEach`, async iteration, `AsyncSequence`, `AsyncStream`, and `AsyncAlgorithms`. +- Prefer explicit default values at initialization when they reduce optional-handling clutter and keep the code easier to follow. +- When lines, chains, or expressions get long, prefer chopping them down into a clean vertical, top-down structure with straight visual flow. +- Do not force value types by default, protocols at seams, actors by default, or other pattern slogans when a plainer concrete implementation is easier to reason about. +- Keep code compliant with Swift 6 language mode. +- Keep strict concurrency checking enabled. +- Prefer modern structured concurrency (`async`/`await`, task groups, actors) over legacy async patterns when it keeps the flow clearer and more direct. +- Make async code cancellation-aware and keep actor or task boundaries explicit instead of hiding them behind detached tasks or queue wrappers. +- Prefer clear `Sendable` boundaries for values that cross task or actor isolation, and keep unchecked sendability exceptional and justified locally. +- Prefer Swift Testing (`import Testing`) as the default test framework, and use XCTest only when a dependency or platform constraint requires it. +- Prefer Swift Testing for unit-style and package-style test surfaces in modern Xcode projects, including suites, tags, parameterized tests, and direct async tests. +- Use XCTest when the platform surface, dependency graph, or Apple tooling still expects it, and keep XCTest and Swift Testing responsibilities clearly separated when both coexist. +- Use XCUITest for UI automation, and prefer explicit element wait APIs such as `waitForExistence(timeout:)`, `waitForNonExistence(timeout:)`, and related state waits over fixed sleeps. +- Keep `.xctestplan` files versioned when test configurations, diagnostics, sanitizers, locale coverage, or selective plan execution matter, and inspect or run them explicitly with `xcodebuild -showTestPlans` and `xcodebuild -testPlan ...`. +- Prefer normal Xcode and XCTest parallel execution for ordinary Swift Testing, XCTest, and XCUITest runs when the project, scheme, destination, and test plan support it. Do not serialize regular tests just because they use Swift, XCTest, async tests, UI automation, or `.xctestplan` matrices. +- Treat tests that load large local AI or ML models, especially models over 500 million parameters, as heavy system-resource tests. Run those tests sequentially, one at a time. +- Prefer first-party and top-tier Swift ecosystem packages from Apple, `swiftlang`, the Swift Server Work Group, and similarly trusted core Swift projects when they simplify the code and make it easier to reason about. +- Commonly approved examples include `swift-configuration` and `swift-async-algorithms` when they reduce bespoke code and improve readability. +- For Apple app projects, prefer Apple-native logging facilities first and allow Swift Logging where it makes the project API clearer. +- Prefer Swift OpenTelemetry for telemetry and instrumentation when telemetry is needed, and prefer existing ecosystem integrations over bespoke wrappers. +- Prefer a checked-in repo-root `.swiftformat` file as the default Swift formatting source of truth, and prefer a pre-commit hook that formats staged Swift sources and then verifies them with `swiftformat --lint` before commit. +- Treat SwiftLint as an optional complementary signal layer for clarity, safety, and maintainability after SwiftFormat owns formatting shape. +- Keep automation and CI commands deterministic, non-interactive, and explicit about toolchain, platform, and configuration assumptions. + +## SwiftUI and State Architecture + +- Treat SwiftUI as declarative component UI, closer to React, F# Fabulous, and Elm than to imperative AppKit or UIKit code. Keep views self-contained, reactive, flexible, reusable, and easy to scan from top to bottom. +- Give each independently reusable view a declarative interface of plain values, narrow bindings, and action closures. Do not inject external ViewModels, stores, coordinators, managers, services, or other collaborating objects from one reusable view into another. +- Choose and record one explicit three-letter uppercase prefix for every app or package. Prefix project-owned Swift files and primary declarations; exempt only `Package.swift`, externally generated Swift, and vendored third-party Swift. +- Never use `+` in project-owned Swift filenames. Concatenate the owner and concern so Xcode navigation, rename, and refactoring keep one consistent grammar. +- Name views `GEAWhateverView.swift` and extracted modifiers `GEAWhateverViewModifier.swift`. Do not introduce ViewModel files as a SwiftUI default. +- Give independently editable or previewable view components their own files. Small private computed view properties or helper views may remain while they do not clutter focused editing or previews. +- Prefix extracted child components with their complete composition owner, such as `GEASettingsSheetToggleCard.swift`. +- Extract a custom `ViewModifier` after more than eight chained modifiers, or earlier when a coherent chain is reusable or obscures the view body. +- Prefer straight, top-down data flow with state owned at the narrowest view, scene, or app boundary that matches the behavior. +- Prefer `@State`, derived values, bindings, and small private helpers for component-local presentation state. When a component genuinely needs an observable state type, create and own it locally with `@State`; do not pass it to a separately reusable view. +- Do not build monolithic views, monolithic controllers, or broad shared mutable state when a smaller component boundary would be clearer. +- Keep updates to view-driving state minimal and localized. +- Prefer durable identity for types that drive SwiftUI state and view updates. +- Treat `App` as the application entry and scene composition boundary, `Scene` as the container for scene-specific lifecycle and environment, and `View` as the component rendering layer. +- Every native app target must have exactly one app lifecycle entry point: one `@main` app type, one `main.swift`, or the platform-equivalent single launch entry. Do not add alternate app entry points, second `@main` types, duplicate `main.swift` files, target-specific app entry files, or parallel app structs for variants. When launch behavior must differ by platform, configuration, or feature flag, keep the single entry point and use Swift conditional compilation or ordinary runtime conditionals inside that boundary. +- Use app-level lifecycle concerns at the `App` boundary, scene lifecycle concerns at the `Scene` boundary, and view-local active or presentation behavior inside views. +- Use `@Binding` to pass a focused writable piece of parent-owned state into a child view. +- Use `@Bindable` when working with an observable model that should project bindings to its mutable properties in a view. +- Use the dedicated SwiftData workflow for persistence architecture and its direct SwiftUI integration path. +- Prefer existing SwiftUI environment values and actions before inventing an equivalent router or service. Use environment values for shared context that truly belongs to the surrounding hierarchy, not as a dumping ground for unrelated dependencies. +- Model app capabilities as direct, concrete feature services. A service provides one capability or a cohesive group of related operations directly to the app; it talks directly to the framework, persistence, network, or system boundary that capability needs instead of forwarding through an app-service wrapper, repository stack, or manager chain. +- Create a feature service at the narrowest app or scene boundary that owns its lifecycle. Put a service into the SwiftUI environment only when independent descendants need to invoke it or observe its state directly. Keep a service private to its feature root when that is the only consumer. +- A service may be `@Observable` when the UI must observe its feature state. Otherwise prefer direct values, async operations, explicit errors, and narrow action closures. Reusable leaf views still receive only values, bindings, and action closures; never pass a service, repository, coordinator, manager, ViewModel, store, or other collaborator into their public interface. +- Keep services concrete by default. Introduce a protocol only for a demonstrated alternate implementation or boundary that cannot otherwise be tested; do not create protocol, adapter, or wrapper layers merely because a service exists. +- Add custom environment values or actions when a capability is dynamic across the hierarchy or shared by many independent components. Keep actions local to the owning component when only that component and its private child views use them. +- Use preference keys only to publish descendant-derived information upward to an ancestor, never as a general state bus. +- Prefer Swift's synthesized memberwise initializer for view properties. Do not write an explicit initializer unless it has real behavior beyond assigning those properties. +- Prefer key-path-based APIs, predicates, and sort descriptors when they keep data access direct and readable. +- Extract repeated chains of view modifiers into custom view modifiers early when that reduces clutter and clearly matches a view or family of views. + +## Xcode Workspace and Project Baseline + +- Treat the `.xcworkspace` or `.xcodeproj` as the source of truth for Apple platform app integration, schemes, build settings, destinations, and target membership. +- Prefer edits through Xcode-aware project structure and keep project file changes intentional and reviewed closely. +- Use the standard top-level Xcode app repository layout when creating or normalizing native app repos: `Sources/`, `Tests/`, `Shared/`, `Extensions/`, `Configurations/`, `Scripts/`, and `Packages/`. +- `Sources/` owns the main app target implementation and app-owned resources/support files. `Tests/` owns all test targets. `Shared/` owns reusable source intended to be compiled into the app and extension targets. `Extensions/` owns extension target roots, one folder per extension. `Configurations/` owns `.xcconfig` layers. `Scripts/` owns project-local automation and build helper scripts. `Packages/` owns local Swift packages only when a real package boundary is justified. +- Keep those top-level roots stable. Do not invent parallel names such as `AppSources`, `TestSources`, `Config`, `BuildScripts`, or `LocalPackages` for ordinary Xcode app repos unless the existing repo already has a deliberate, documented convention. +- Inside `Sources/`, use this strict app structure by default: `Views/`, `Models/`, and `Services/`. Do not create a root `Controllers/` directory. +- `Sources/Views/` owns SwiftUI views and UIKit/AppKit view surfaces. Use `Sources/Views/Shared`, `Sources/Views/macOS`, and `Sources/Views/iOS` so shared, macOS-specific, and iOS/iPadOS-specific UI have clear homes. +- Use bare prefixed names such as `GEAWhatever.swift` for runtime/domain values. Reserve `GEAWhateverModel.swift` for persistence, and use `GEAWhateverRecord.swift` or `GEAWhateverDTO.swift` only for genuinely additional representations. +- `Sources/Models/` owns Core Data and SwiftData persistence models plus additional record or transfer representations. +- `Sources/Services/` owns direct concrete feature and boundary services. Use `Consumed/` for external capabilities the app calls, `Internal/` for app-owned feature services, and `Provided/` for services the app exposes to extensions, helpers, plugins, integrations, or other clients. These directories describe ownership and direction; they do not justify wrapper layers or an app-wide service container. +- Name a service for its capability, such as `GEADownloadService.swift` or `GEAImportService.swift`. Do not create `GEAAppService.swift` as an umbrella service by default; `GEAApp.swift` remains the lifecycle-entry special case. +- Use `xcodebuild` for Apple platform integration validation, including scheme, destination or SDK, and configuration-specific build or test runs. +- Keep `xcodebuild` invocations reproducible in automation by passing explicit schemes, destinations or SDKs, and configurations when relevant. +- For Codex GUI worktree-first Xcode repos, use a portable `.codex/environments/*.toml` local environment file when the repo wants shared app setup or action buttons. Start from `apple-dev-skills/templates/codex-local-environments/xcode-project.toml`, keep paths repo-relative, and prefer `-derivedDataPath ./DerivedData` or another ignored repo-local build directory instead of user-global DerivedData. +- When scripts or terminal workflows add files on disk, verify that Xcode project membership, target membership, build-phase membership, and resource-bundle inclusion all match the intended result; files appearing in the directory tree alone are not enough. +- Direct filesystem edits outside `.pbxproj` are generally safe when Xcode is closed or when the current project is not open in Xcode, but still verify that the Xcode project picks up the intended files and memberships afterward. +- Prefer Debug builds for everyday edit-build-test loops, but validate Release builds explicitly when optimization, packaging, launch behavior, watchdog timing, or deployment realism matters. +- Treat tagged releases as a signal to validate both the normal Debug path and a Release artifact path, and when shipping apps or deliverables test the Release behavior without relying on an attached debugger. +- Prefer direct filesystem edits in Xcode-managed scope only when the workflow already accounts for project-file and scheme integrity. +- Never edit `.pbxproj` files directly. If a project-file change is needed and no safe project-aware tool is available, stop and ask for an Xcode-mediated project change instead. When `.pbxproj` is tracked and Xcode, XcodeGen, or another project-aware workflow legitimately changes it, treat that diff as critical project state: review it, stage it, and commit it with the branch before any push, merge, release, or cleanup. + +## XcodeGen and Build Configuration Defaults + +- For new Xcode app, framework, and workspace repositories, prefer an XcodeGen-backed project by default unless the user explicitly asks for a hand-managed Xcode project or the repository has a concrete reason to avoid a generator dependency. +- If the repo contains `project.yml`, `project.yaml`, or clearly named included XcodeGen spec files, treat the XcodeGen spec set as the source of truth for generated project structure. +- For XcodeGen-backed repos, make target membership, resource membership, schemes, Swift package declarations, test-plan references, project references, build configurations, configuration-file wiring, generation options, and project-level settings in the XcodeGen specs instead of editing the generated `.pbxproj`. +- Before running `xcodegen generate`, inspect the current git diff for generated `.xcodeproj` or `.pbxproj` changes. Treat existing project-file diffs as intentional user or Xcode GUI changes by default, not disposable generator drift. +- When Xcode GUI changes added build settings, signing settings, capabilities, `Info.plist` build setting overrides, file membership, scheme changes, or entitlement wiring to `.pbxproj`, preserve the user intent by moving each intentional value to the owning tracked source first: XcodeGen spec for structure, `.xcconfig` for build settings, `.entitlements` for entitlement keys, `Info.plist` for plist keys, `.xcscheme` or scheme spec for scheme behavior, and `.xctestplan` for test-plan content. +- Only regenerate after that promotion is complete, then review the generated project diff to confirm XcodeGen preserved the intended behavior instead of deleting it. If the owning tracked file is ambiguous, stop and ask before regenerating. +- For new XcodeGen-backed app scaffolds, start from the maintained `apple-dev-skills/templates/xcodegen/` templates when available instead of inventing a fresh project-spec shape from memory. +- Keep `minimumXcodeGenVersion` on a recent validated release for new scaffolds. Prefer updating the template and validation together when the repo intentionally raises the baseline. +- For Xcode 16 or newer project formats, prefer XcodeGen `syncedFolder` roots at the broad top-level directory boundary so file creation, deletion, and organization stay synchronized between Xcode and the filesystem without hand-listing every source file in YAML. +- Do not fragment ordinary XcodeGen source roots by subdirectory. A standard app target gets one `Sources` source entry that includes all app source, resource, support, generated plist, entitlement, and nested feature folders, plus one `Shared` source entry when shared app/extension code exists. A standard test target gets one `Tests` source entry that includes all test subdirectories. Extension targets use one `Extensions/` source entry per extension target. If a project has another separate top-level logical root, use one top-level entry for that root, not one entry per child folder. +- Never split `Sources/App`, `Sources/Resources`, `Sources/Support`, feature folders, or `Tests/Tests` into separate XcodeGen source entries unless a specific non-ordinary file or folder truly needs custom compiler flags, build-phase routing, destination filters, or target membership that cannot be represented from the broad root. +- If `syncedFolder` behaves poorly for a repo, fall back to the same broad top-level recursive paths such as `Sources`, `Tests`, or `Resources` with explicit `includes` and `excludes`; do not fall back to subdirectory-level fragmentation or one YAML entry per ordinary source file. +- Keep XcodeGen specs readable as project structure, not as a dumping ground for every build setting. Use `configs`, `configFiles`, `targets`, `schemes`, `packages`, `projectReferences`, `targetTemplates`, and `schemeTemplates` deliberately so future edits have an obvious owner. +- Prefer explicit top-level schemes for app scaffolds once scheme behavior matters. Put build, run, test, profile, analyze, archive, environment variables, command-line arguments, and test-plan references in the scheme spec rather than relying on hidden generated defaults. +- Prefer external `.xcconfig` files as the default home for nontrivial build settings. Keep build settings in XcodeGen inline settings only when they are small, local, and clearer there. +- Use `.xcconfig` files for settings that vary by Debug, Release, CI, local development, signing, bundle identity, compiler flags, Swift settings, deployment variants, or environment-specific behavior. +- Keep configuration layering explicit. Prefer a small shared base config, target-level configs for app/test/extension identity, then per-configuration configs that include the narrower target config and override only what changes. +- In XcodeGen specs, wire build configurations to their matching `.xcconfig` files instead of duplicating the same settings across generated project objects. +- Prefer checked-in external `.entitlements` files for app, extension, and capability-bearing targets, with `CODE_SIGN_ENTITLEMENTS` declared in the owning target's `.xcconfig`. Let Xcode capabilities update the entitlement plist when possible, then review and commit the entitlement diff; keep XcodeGen responsible for wiring the file, not regenerating its contents from inline YAML. +- Do not assume Xcode's Build Settings UI writes edited values back into `.xcconfig` files. When a build setting should remain tracked in `.xcconfig`, inspect the generated project diff after GUI changes and move intentional build-setting overrides from `.pbxproj` back into the owning `.xcconfig` before regenerating. +- Keep secrets, personal team IDs, local machine paths, provisioning profiles, API tokens, and private signing material out of committed `.xcconfig` files. Use build settings only for non-secret configuration values, safe placeholders, references to externally supplied values, or local developer placeholders that are safe to commit. +- Before changing generated project structure, inspect the root spec plus any `include` entries so the edit lands in the owning spec rather than duplicating settings in the wrong file. Remember that included specs merge into the root spec, and local overrides may intentionally replace arrays or maps. +- After changing XcodeGen specs, `.xcconfig` files, or entitlement-file wiring, run `xcodegen generate` from the spec root, or `xcodegen generate --spec ` when the project uses a non-default spec path. +- If the spec uses environment variables or generation hooks, preserve and document the required environment before regenerating so CI and other contributors can reproduce the project. +- Review the spec diff, `.xcconfig` diff, and generated `.xcodeproj` diff after regeneration. Generated `.pbxproj` changes are acceptable output when they come from XcodeGen, but they should still be reviewed for unintended target, scheme, signing, package, build-setting, or file-membership churn. +- Validate regenerated projects with explicit `xcodebuild` commands for the affected scheme, destination or SDK, and configuration. +- For existing hand-managed Xcode projects, do not migrate to XcodeGen or externalize build settings into `.xcconfig` files unless the user explicitly asks for that migration. When they do, treat it as a project-structure migration with before/after validation. diff --git a/skills/mailkit-workflow/scripts/customization_config.py b/skills/mailkit-workflow/scripts/customization_config.py new file mode 100755 index 00000000..27ef917b --- /dev/null +++ b/skills/mailkit-workflow/scripts/customization_config.py @@ -0,0 +1,213 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "PyYAML>=6.0.2,<7", +# ] +# /// +"""Load and persist per-skill customization state.""" + +from __future__ import annotations + +import argparse +import copy +import os +import re +import sys +from pathlib import Path + +import yaml + +SCHEMA_VERSION = 1 +SKILL_NAME = "safari-extension-control-workflow" +CONFIG_HOME_ENV = "APPLE_DEV_SKILLS_CONFIG_HOME" +DEFAULT_CONFIG_ROOT = "~/.config/gaelic-ghost/apple-dev-skills" +ALLOWED_TOP_LEVEL = {"schemaVersion", "isCustomized", "settings"} + + +def fail(message: str) -> None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def quote_string(value: str) -> str: + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + + +def encode_scalar(value) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, int): + return str(value) + if value is None: + return quote_string("") + return quote_string(str(value)) + + +def parse_yaml(path: Path) -> dict: + if not path.exists(): + fail(f"Missing YAML file: {path}") + + try: + loaded = yaml.safe_load(path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + fail(f"Invalid YAML in {path}: {exc}") + + if loaded is None: + return {} + if not isinstance(loaded, dict): + fail(f"Top-level YAML document must be a mapping in {path}") + + if isinstance(loaded.get("settings"), dict): + loaded["settings"] = { + key: ("" if value is None else value) for key, value in loaded["settings"].items() + } + + return loaded + + +def validate_config(config: dict, *, allow_partial: bool) -> None: + unknown = set(config.keys()) - ALLOWED_TOP_LEVEL + if unknown: + fail(f"Unknown top-level keys: {', '.join(sorted(unknown))}") + + if not allow_partial: + for required in ("schemaVersion", "isCustomized", "settings"): + if required not in config: + fail(f"Missing required key: {required}") + + if "schemaVersion" in config and config["schemaVersion"] != SCHEMA_VERSION: + fail(f"schemaVersion must be {SCHEMA_VERSION}") + + if "isCustomized" in config and not isinstance(config["isCustomized"], bool): + fail("isCustomized must be boolean") + + if "settings" in config: + if not isinstance(config["settings"], dict): + fail("settings must be a mapping") + for key, value in config["settings"].items(): + if not re.fullmatch(r"[A-Za-z0-9_]+", key): + fail(f"Invalid settings key: {key}") + if isinstance(value, (dict, list)): + fail(f"settings values must be scalar: {key}") + + +def merge_configs(base: dict, overlay: dict) -> dict: + merged = { + "schemaVersion": base.get("schemaVersion", SCHEMA_VERSION), + "isCustomized": base.get("isCustomized", False), + "settings": copy.deepcopy(base.get("settings", {})), + } + + if "schemaVersion" in overlay: + merged["schemaVersion"] = overlay["schemaVersion"] + if "isCustomized" in overlay: + merged["isCustomized"] = overlay["isCustomized"] + if "settings" in overlay: + merged["settings"].update(overlay["settings"]) + + return merged + + +def dump_yaml(config: dict) -> str: + lines = [ + f"schemaVersion: {int(config['schemaVersion'])}", + f"isCustomized: {'true' if config['isCustomized'] else 'false'}", + "settings:", + ] + for key in sorted(config["settings"].keys()): + lines.append(f" {key}: {encode_scalar(config['settings'][key])}") + return "\n".join(lines) + "\n" + + +def template_path() -> Path: + return Path(__file__).resolve().parents[1] / "references" / "customization.template.yaml" + + +def config_root() -> Path: + root = os.environ.get(CONFIG_HOME_ENV, DEFAULT_CONFIG_ROOT) + return Path(root).expanduser() + + +def durable_path() -> Path: + return config_root() / SKILL_NAME / "customization.yaml" + + +def load_template() -> dict: + cfg = parse_yaml(template_path()) + validate_config(cfg, allow_partial=False) + return cfg + + +def load_durable() -> dict: + path = durable_path() + if not path.exists(): + return {} + cfg = parse_yaml(path) + validate_config(cfg, allow_partial=False) + return cfg + + +def cmd_path(_: argparse.Namespace) -> None: + print(durable_path()) + + +def cmd_effective(_: argparse.Namespace) -> None: + effective = merge_configs(load_template(), load_durable()) + validate_config(effective, allow_partial=False) + print(dump_yaml(effective), end="") + + +def cmd_apply(args: argparse.Namespace) -> None: + template = load_template() + current = merge_configs(template, load_durable()) + incoming = parse_yaml(Path(args.input)) + validate_config(incoming, allow_partial=True) + + updated = merge_configs(current, incoming) + updated["schemaVersion"] = SCHEMA_VERSION + updated["isCustomized"] = True + validate_config(updated, allow_partial=False) + + target = durable_path() + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(dump_yaml(updated), encoding="utf-8") + print(target) + + +def cmd_reset(_: argparse.Namespace) -> None: + target = durable_path() + if target.exists(): + target.unlink() + print(target) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Manage per-skill customization config") + subparsers = parser.add_subparsers(dest="command", required=True) + + parser_path = subparsers.add_parser("path", help="Print durable config path") + parser_path.set_defaults(func=cmd_path) + + parser_effective = subparsers.add_parser("effective", help="Print merged effective config") + parser_effective.set_defaults(func=cmd_effective) + + parser_apply = subparsers.add_parser("apply", help="Apply and persist config overrides") + parser_apply.add_argument("--input", required=True, help="Path to YAML overrides") + parser_apply.set_defaults(func=cmd_apply) + + parser_reset = subparsers.add_parser("reset", help="Delete durable config for this skill") + parser_reset.set_defaults(func=cmd_reset) + + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/tests/test_validate_hermes_compatibility.py b/tests/test_validate_hermes_compatibility.py index 9ce5f0ef..c299eda6 100644 --- a/tests/test_validate_hermes_compatibility.py +++ b/tests/test_validate_hermes_compatibility.py @@ -71,6 +71,7 @@ def make_repo(tmp_path: Path) -> Path: def configure_paths(repo_root: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(export_hermes_skills, "SOURCE_ROOT", repo_root / "plugins" / "agent-portability-skills" / "skills") monkeypatch.setattr(export_hermes_skills, "MESSAGING_SOURCE_ROOT", repo_root / "plugins" / "agent-portability-skills" / "skills") + monkeypatch.setattr(export_hermes_skills, "APPLE_SOURCE_ROOT", repo_root / "plugins" / "agent-portability-skills" / "skills") monkeypatch.setattr(export_hermes_skills, "EXPORT_ROOT", repo_root / "skills") monkeypatch.setattr(validate_hermes_compatibility, "REPO_ROOT", repo_root) monkeypatch.setattr(validate_hermes_compatibility, "EXPORT_ROOT", repo_root / "skills")