@mmerterden/multi-agent-pipeline 20.0.0 → 20.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +5 -5
  3. package/README.tr.md +5 -5
  4. package/SECURITY.md +3 -3
  5. package/docs/adr/0011-dormant-ci.md +10 -1
  6. package/docs/architecture.md +2 -2
  7. package/docs/ecosystem.md +5 -5
  8. package/docs/facts.json +8 -7
  9. package/install/_codex-agents.mjs +1 -1
  10. package/manifest.json +92 -64
  11. package/package.json +1 -1
  12. package/pipeline/agents/code-reviewer.md +2 -2
  13. package/pipeline/agents/dev-critic.md +5 -5
  14. package/pipeline/agents/security-auditor.md +80 -72
  15. package/pipeline/commands/figma-to-swiftui.md +1 -1
  16. package/pipeline/commands/multi-agent/SKILL.md +1 -1
  17. package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
  18. package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -1
  19. package/pipeline/commands/multi-agent/help/SKILL.md +2 -0
  20. package/pipeline/commands/multi-agent/scan/SKILL.md +2 -2
  21. package/pipeline/commands/multi-agent/security-review/SKILL.md +52 -0
  22. package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
  23. package/pipeline/multi-agent-refs/component-dispatch.md +5 -5
  24. package/pipeline/multi-agent-refs/cross-cli-contract.md +6 -6
  25. package/pipeline/multi-agent-refs/features/security-audit.md +55 -0
  26. package/pipeline/multi-agent-refs/phases/modes.md +1 -1
  27. package/pipeline/multi-agent-refs/phases/phase-3-review.md +9 -15
  28. package/pipeline/multi-agent-refs/phases/phase-5-report.md +1 -1
  29. package/pipeline/multi-agent-refs/threat-model.md +39 -0
  30. package/pipeline/schemas/agent-state.schema.json +23 -0
  31. package/pipeline/schemas/phases.json +1 -2
  32. package/pipeline/schemas/prefs.schema.json +0 -4
  33. package/pipeline/schemas/reviewer-output.schema.json +99 -2
  34. package/pipeline/schemas/security-finding.schema.json +144 -0
  35. package/pipeline/scripts/_stack-routing.mjs +1 -0
  36. package/pipeline/scripts/gc-abandoned.sh +16 -9
  37. package/pipeline/scripts/render-work-summary.sh +7 -4
  38. package/pipeline/skills/.skill-manifest.json +47 -23
  39. package/pipeline/skills/.skills-index.json +75 -9
  40. package/pipeline/skills/shared/README.md +13 -7
  41. package/pipeline/skills/shared/core/multi-agent/SKILL.md +3 -4
  42. package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +2 -2
  43. package/pipeline/skills/shared/core/multi-agent-security-review/SKILL.md +29 -0
  44. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  45. package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
  46. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
  47. package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
  48. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
  49. package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
  50. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
  51. package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
  52. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
  53. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
  54. package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
  55. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
  56. package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
  57. package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
  58. package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
  59. package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
  60. package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
  61. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
  62. package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
  63. package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
  64. package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
  65. package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
  66. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
  67. package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
  68. package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
  69. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
  70. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
  71. package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
  72. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
  73. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
  74. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
  75. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
  76. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
  77. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
  78. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
  79. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
  80. package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
  81. package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
  82. package/pipeline/skills/shared/external/security-review/SKILL.md +64 -0
  83. package/pipeline/skills/shared/external/security-review/references/owasp-mobile-top10-2024.md +53 -0
  84. package/pipeline/skills/shared/external/security-review/references/owasp-web-api-top10-2021.md +56 -0
  85. package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
  86. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
  87. package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
  88. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
  89. package/pipeline/skills/skills-index.md +8 -2
  90. package/pipeline/commands/security-review.md +0 -6
@@ -122,10 +122,6 @@ types. Framework: `IntentPerson`, `IntentFile`. Custom: any `AppEntity` or
122
122
  ### Common initializer patterns
123
123
 
124
124
  ```swift
125
- // Basic
126
- @Parameter(title: "Name")
127
- var name: String
128
-
129
125
  // With default
130
126
  @Parameter(title: "Count", default: 5)
131
127
  var count: Int
@@ -137,17 +133,11 @@ var volume: Int
137
133
  // Options provider (dynamic list)
138
134
  @Parameter(title: "Category", optionsProvider: CategoryOptionsProvider())
139
135
  var category: Category
140
-
141
- // File with content types
142
- @Parameter(title: "Document", supportedContentTypes: [.pdf, .plainText])
143
- var document: IntentFile
144
-
145
- // Measurement with unit
146
- @Parameter(title: "Distance", defaultUnit: .miles, supportsNegativeNumbers: false)
147
- var distance: Measurement<UnitLength>
148
136
  ```
149
137
 
150
- See [references/appintents-advanced.md](references/appintents-advanced.md) for all initializer variants.
138
+ Options providers, files, measurements, resolvers, disambiguation dialogs, and
139
+ input-connection behavior follow the same shape. See
140
+ [references/appintents-advanced.md](references/appintents-advanced.md) for all initializer variants.
151
141
 
152
142
  ## AppEntity
153
143
 
@@ -195,52 +185,19 @@ struct SoupEntityQuery: EntityQuery {
195
185
 
196
186
  ### 2. EntityStringQuery (free-text search)
197
187
 
198
- ```swift
199
- struct SoupStringQuery: EntityStringQuery {
200
- func entities(matching string: String) async throws -> [SoupEntity] {
201
- SoupStore.shared.search(string).map { SoupEntity(from: $0) }
202
- }
203
- func entities(for identifiers: [String]) async throws -> [SoupEntity] {
204
- SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
205
- }
206
- }
207
- ```
188
+ Add `entities(matching string:)` alongside ID resolution for free-text search.
208
189
 
209
190
  ### 3. EnumerableEntityQuery (finite set)
210
191
 
211
- ```swift
212
- struct AllSoupsQuery: EnumerableEntityQuery {
213
- func allEntities() async throws -> [SoupEntity] {
214
- SoupStore.shared.allSoups.map { SoupEntity(from: $0) }
215
- }
216
- func entities(for identifiers: [String]) async throws -> [SoupEntity] {
217
- SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
218
- }
219
- }
220
- ```
192
+ Implement `allEntities()` when the full set is small and enumerable.
221
193
 
222
194
  ### 4. UniqueAppEntityQuery (singleton, iOS 18+)
223
195
 
224
- Use for single-instance entities like app settings.
196
+ Use for single-instance entities like app settings; implement `uniqueEntity()`.
225
197
 
226
- ```swift
227
- struct AppSettingsEntity: UniqueAppEntity {
228
- static let defaultQuery = AppSettingsQuery()
229
- static var typeDisplayRepresentation: TypeDisplayRepresentation = "Settings"
230
- var displayRepresentation: DisplayRepresentation { "App Settings" }
231
-
232
- var id: String { "app-settings" }
233
- }
234
-
235
- struct AppSettingsQuery: UniqueAppEntityQuery {
236
- func uniqueEntity() async throws -> AppSettingsEntity {
237
- AppSettingsEntity()
238
- }
239
- }
240
- ```
241
-
242
- See [references/appintents-advanced.md](references/appintents-advanced.md) for `EntityPropertyQuery` with
243
- filter/sort support.
198
+ See [references/appintents-advanced.md](references/appintents-advanced.md) for full `EntityStringQuery`,
199
+ `EnumerableEntityQuery`, and `UniqueAppEntityQuery` examples, plus `EntityPropertyQuery` with
200
+ filter and sort support.
244
201
 
245
202
  ## AppEnum
246
203
 
@@ -377,36 +334,17 @@ struct LightControlConfig: ControlConfigurationIntent {
377
334
  static var title: LocalizedStringResource = "Light Control"
378
335
  @Parameter(title: "Light", default: .livingRoom) var light: LightEntity
379
336
  }
380
-
381
- struct ToggleLightIntent: AppIntent {
382
- static var title: LocalizedStringResource = "Toggle Light"
383
- static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
384
-
385
- @Parameter(title: "Light") var light: LightEntity
386
-
387
- func perform() async throws -> some IntentResult {
388
- try await requestConfirmation(
389
- actionName: .toggle,
390
- dialog: "Toggle \(light.name)?"
391
- )
392
- try await LightService.shared.toggle(light.id)
393
- return .result()
394
- }
395
- }
396
-
397
- struct LightControl: ControlWidget {
398
- var body: some ControlWidgetConfiguration {
399
- AppIntentControlConfiguration(kind: "LightControl", intent: LightControlConfig.self) { config in
400
- ControlWidgetToggle(config.light.name, isOn: config.light.isOn, action: ToggleLightIntent(light: config.light))
401
- }
402
- }
403
- }
404
337
  ```
405
338
 
339
+ See [references/appintents-advanced.md](references/appintents-advanced.md) ("Control Center Widget
340
+ Implementation") for the full `ControlWidget` + action-intent wiring with
341
+ confirmation and authentication.
342
+
406
343
  ## Spotlight and IndexedEntity (iOS 18+)
407
344
 
408
345
  Conform to `IndexedEntity` for Spotlight search. On iOS 26+, use `indexingKey`
409
- for structured metadata:
346
+ to map a property to a `CSSearchableItemAttributeSet` key path for structured
347
+ metadata:
410
348
 
411
349
  ```swift
412
350
  struct RecipeEntity: IndexedEntity {
@@ -421,116 +359,43 @@ struct RecipeEntity: IndexedEntity {
421
359
  var displayRepresentation: DisplayRepresentation {
422
360
  DisplayRepresentation(title: "\(name)")
423
361
  }
424
-
425
- var attributeSet: CSSearchableItemAttributeSet {
426
- let attrs = defaultAttributeSet
427
- attrs.keywords = ["recipe"]
428
- return attrs
429
- }
430
- }
431
-
432
- struct RecipeQuery: EntityQuery {
433
- func entities(for identifiers: [RecipeEntity.ID]) async throws -> [RecipeEntity] {
434
- identifiers.compactMap { id in
435
- RecipeStore.shared.recipe(id: id).map(RecipeEntity.init)
436
- }
437
- }
438
- }
439
-
440
- struct OpenRecipeIntent: OpenIntent {
441
- static var title: LocalizedStringResource = "Open Recipe"
442
- @Parameter(title: "Recipe") var target: RecipeEntity
443
362
  }
444
363
  ```
445
364
 
446
365
  `IndexedEntity` describes metadata; still index instances in a named Spotlight
447
- index, e.g. `CSSearchableIndex(name: "...").indexAppEntities(entities)`.
448
- If you customize `attributeSet`, start from `defaultAttributeSet`; returning a
449
- fresh attribute set replaces display representation and property-derived
450
- metadata. Prefer `indexingKey` for metadata already exposed on the entity.
451
- Update and delete changed records in that same named index:
452
-
453
- ```swift
454
- let recipeIndex = CSSearchableIndex(name: "Recipes")
455
- try await recipeIndex.indexAppEntities(changedRecipes)
456
- try await recipeIndex.deleteAppEntities(
457
- identifiedBy: deletedRecipeIDs,
458
- ofType: RecipeEntity.self
459
- )
460
- ```
366
+ index, e.g. `CSSearchableIndex(name: "...").indexAppEntities(entities)`. Provide
367
+ an `OpenIntent` for the type so results open the right content. If you customize
368
+ `attributeSet`, start from `defaultAttributeSet`; returning a fresh attribute set
369
+ replaces display representation and property-derived metadata. Prefer
370
+ `indexingKey` for metadata already exposed on the entity.
461
371
 
462
- For large syncs, use `beginBatch()`, `endBatch(withClientState:)`, and
463
- `fetchLastClientState()` so indexing can resume after a crash or jetsam.
372
+ See [references/appintents-advanced.md](references/appintents-advanced.md) for the full entity plus query,
373
+ the available `@Property` / `@ComputedProperty` indexing keys, updating and
374
+ deleting indexed entities, batch indexing with client state for crash recovery,
375
+ and direct Core Spotlight usage.
464
376
 
465
377
  ## iOS 26 Additions
466
378
 
467
379
  ### SnippetIntent
468
380
 
469
- Display interactive snippets in system UI:
470
-
471
- ```swift
472
- struct OrderStatusSnippet: SnippetIntent {
473
- static var title: LocalizedStringResource = "Order Status"
474
- func perform() async throws -> some IntentResult & ShowsSnippetView {
475
- let status = await OrderTracker.currentStatus()
476
- return .result(view: OrderStatusSnippetView(status: status))
477
- }
478
- }
479
-
480
- struct CheckOrderStatusIntent: AppIntent {
481
- static var title: LocalizedStringResource = "Check Order Status"
482
- func perform() async throws -> some IntentResult & ShowsSnippetIntent {
483
- .result(snippetIntent: OrderStatusSnippet())
484
- }
485
- }
486
- ```
487
-
488
- The system may call `perform()` multiple times, including after snippet button
489
- or toggle actions; keep `SnippetIntent.perform()` side-effect-free and do
490
- mutations in the calling action intent or a separate button/toggle action. A
491
- snippet-only intent is not discoverable in Shortcuts or Spotlight unless
492
- `isDiscoverable` is `true`.
381
+ Display interactive snippets in system UI. The system may call `perform()`
382
+ multiple times, including after snippet button or toggle actions; keep
383
+ `SnippetIntent.perform()` side-effect-free and do mutations in the calling action
384
+ intent or a separate button/toggle action. A snippet-only intent is not
385
+ discoverable in Shortcuts or Spotlight unless `isDiscoverable` is `true`. See
386
+ [references/appintents-advanced.md](references/appintents-advanced.md) ("SnippetIntent Implementation")
387
+ for the `ShowsSnippetView` / `ShowsSnippetIntent` example.
493
388
 
494
389
  ### IntentValueQuery (Visual Intelligence)
495
390
 
496
- ```swift
497
- @available(iOS 26, *)
498
- @UnionValue
499
- enum ShoppingVisualResult {
500
- case product(ProductEntity)
501
- case store(StoreEntity)
502
- }
503
-
504
- @available(iOS 26, *)
505
- struct ShoppingVisualQuery: IntentValueQuery {
506
- func values(for input: SemanticContentDescriptor) async throws -> [ShoppingVisualResult] {
507
- try Task.checkCancellation()
508
- async let productMatches = ProductStore.shared.matches(
509
- labels: input.labels,
510
- pixelBuffer: input.pixelBuffer,
511
- limit: 5
512
- )
513
- async let storeMatches = StoreStore.shared.matches(
514
- labels: input.labels,
515
- pixelBuffer: input.pixelBuffer,
516
- limit: 3
517
- )
518
- let ranked = await rank(productMatches, storeMatches)
519
- return Array(ranked.prefix(8))
520
- }
521
- }
522
- ```
523
-
524
- Only one `IntentValueQuery` can take `SemanticContentDescriptor`; use
525
- `@UnionValue` when one query must return multiple app entity types. Treat
526
- `labels` as high-level English descriptors, not exhaustive synonyms or app
527
- taxonomy; combine them with `pixelBuffer` when available. Return small, ranked,
528
- cancellation-friendly results, and provide an `OpenIntent`, URL representation,
529
- or in-app search handoff for details and more results. Do not implement camera
530
- capture, Vision `VN*` requests, barcode classification, or Spotlight indexing
531
- inside the App Intents query; call an existing bounded app search or image-match
532
- service instead, with explicit result caps and timeouts when work may exceed a
533
- system UI budget.
391
+ Return app entities for a Visual Intelligence `SemanticContentDescriptor`. Only
392
+ one `IntentValueQuery` can take `SemanticContentDescriptor`; use `@UnionValue`
393
+ when one query must return multiple app entity types. Return small, ranked,
394
+ cancellation-friendly results and provide an opening path (`OpenIntent`, URL, or
395
+ in-app search). Do not run camera capture, Vision `VN*` requests, or Spotlight
396
+ indexing inside the query; call a bounded app search or image-match service
397
+ instead. See [references/appintents-advanced.md](references/appintents-advanced.md) ("Visual Intelligence
398
+ IntentValueQuery") for the full `@UnionValue` + query example and guidance.
534
399
 
535
400
  ## Common Mistakes
536
401
 
@@ -8,7 +8,11 @@ URL-representable types, and Spotlight indexing.
8
8
  ## Contents
9
9
 
10
10
  - [`@Parameter Initializer Variants`](#parameter-initializer-variants)
11
+ - [EntityQuery Variants (Full Examples)](#entityquery-variants-full-examples)
11
12
  - [EntityPropertyQuery (Filter and Sort)](#entitypropertyquery-filter-and-sort)
13
+ - [Control Center Widget Implementation](#control-center-widget-implementation)
14
+ - [SnippetIntent Implementation (iOS 26+)](#snippetintent-implementation-ios-26)
15
+ - [Visual Intelligence IntentValueQuery (iOS 26+)](#visual-intelligence-intentvaluequery-ios-26)
12
16
  - [Assistant Schemas (iOS 18+)](#assistant-schemas-ios-18)
13
17
  - [Focus Filter Intents](#focus-filter-intents)
14
18
  - [SiriKit Migration (CustomIntentMigratedAppIntent)](#sirikit-migration-customintentmigratedappintent)
@@ -184,6 +188,57 @@ func perform() async throws -> some IntentResult {
184
188
  }
185
189
  ```
186
190
 
191
+ ## EntityQuery Variants (Full Examples)
192
+
193
+ Full implementations of the query variants summarized in the main skill. Variant
194
+ 1 (base `EntityQuery`) is shown in the skill; variants 2-4 are below.
195
+
196
+ ### EntityStringQuery (free-text search)
197
+
198
+ ```swift
199
+ struct SoupStringQuery: EntityStringQuery {
200
+ func entities(matching string: String) async throws -> [SoupEntity] {
201
+ SoupStore.shared.search(string).map { SoupEntity(from: $0) }
202
+ }
203
+ func entities(for identifiers: [String]) async throws -> [SoupEntity] {
204
+ SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
205
+ }
206
+ }
207
+ ```
208
+
209
+ ### EnumerableEntityQuery (finite set)
210
+
211
+ ```swift
212
+ struct AllSoupsQuery: EnumerableEntityQuery {
213
+ func allEntities() async throws -> [SoupEntity] {
214
+ SoupStore.shared.allSoups.map { SoupEntity(from: $0) }
215
+ }
216
+ func entities(for identifiers: [String]) async throws -> [SoupEntity] {
217
+ SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
218
+ }
219
+ }
220
+ ```
221
+
222
+ ### UniqueAppEntityQuery (singleton, iOS 18+)
223
+
224
+ Use for single-instance entities like app settings.
225
+
226
+ ```swift
227
+ struct AppSettingsEntity: UniqueAppEntity {
228
+ static let defaultQuery = AppSettingsQuery()
229
+ static var typeDisplayRepresentation: TypeDisplayRepresentation = "Settings"
230
+ var displayRepresentation: DisplayRepresentation { "App Settings" }
231
+
232
+ var id: String { "app-settings" }
233
+ }
234
+
235
+ struct AppSettingsQuery: UniqueAppEntityQuery {
236
+ func uniqueEntity() async throws -> AppSettingsEntity {
237
+ AppSettingsEntity()
238
+ }
239
+ }
240
+ ```
241
+
187
242
  ## EntityPropertyQuery (Filter and Sort)
188
243
 
189
244
  The most powerful query variant. Declare filterable properties and sortable
@@ -283,6 +338,114 @@ raw `EntityQueryComparator` objects.
283
338
  | `LessThanOrEqualToComparator` | Comparable properties |
284
339
  | `IsBetweenComparator` | Comparable properties supported by Shortcuts |
285
340
 
341
+ ## Control Center Widget Implementation
342
+
343
+ Full `ControlConfigurationIntent` + `ControlWidget` wiring. The configuration
344
+ intent is a parameter contract; state changes run from a separate action intent
345
+ with an appropriate `authenticationPolicy` and `requestConfirmation`.
346
+
347
+ ```swift
348
+ struct LightControlConfig: ControlConfigurationIntent {
349
+ static var title: LocalizedStringResource = "Light Control"
350
+ @Parameter(title: "Light", default: .livingRoom) var light: LightEntity
351
+ }
352
+
353
+ struct ToggleLightIntent: AppIntent {
354
+ static var title: LocalizedStringResource = "Toggle Light"
355
+ static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
356
+
357
+ @Parameter(title: "Light") var light: LightEntity
358
+
359
+ func perform() async throws -> some IntentResult {
360
+ try await requestConfirmation(
361
+ actionName: .toggle,
362
+ dialog: "Toggle \(light.name)?"
363
+ )
364
+ try await LightService.shared.toggle(light.id)
365
+ return .result()
366
+ }
367
+ }
368
+
369
+ struct LightControl: ControlWidget {
370
+ var body: some ControlWidgetConfiguration {
371
+ AppIntentControlConfiguration(kind: "LightControl", intent: LightControlConfig.self) { config in
372
+ ControlWidgetToggle(config.light.name, isOn: config.light.isOn, action: ToggleLightIntent(light: config.light))
373
+ }
374
+ }
375
+ }
376
+ ```
377
+
378
+ Parameters without defaults must be optional on a `ControlConfigurationIntent`.
379
+
380
+ ## SnippetIntent Implementation (iOS 26+)
381
+
382
+ Display interactive snippets in system UI. The system may call `perform()`
383
+ multiple times, including after snippet button or toggle actions, so keep
384
+ `SnippetIntent.perform()` side-effect-free and do mutations in the calling
385
+ action intent or a separate button/toggle action.
386
+
387
+ ```swift
388
+ struct OrderStatusSnippet: SnippetIntent {
389
+ static var title: LocalizedStringResource = "Order Status"
390
+ func perform() async throws -> some IntentResult & ShowsSnippetView {
391
+ let status = await OrderTracker.currentStatus()
392
+ return .result(view: OrderStatusSnippetView(status: status))
393
+ }
394
+ }
395
+
396
+ struct CheckOrderStatusIntent: AppIntent {
397
+ static var title: LocalizedStringResource = "Check Order Status"
398
+ func perform() async throws -> some IntentResult & ShowsSnippetIntent {
399
+ .result(snippetIntent: OrderStatusSnippet())
400
+ }
401
+ }
402
+ ```
403
+
404
+ A snippet-only intent is not discoverable in Shortcuts or Spotlight unless
405
+ `isDiscoverable` is `true`.
406
+
407
+ ## Visual Intelligence IntentValueQuery (iOS 26+)
408
+
409
+ Only one `IntentValueQuery` can take `SemanticContentDescriptor`; use
410
+ `@UnionValue` when one query must return multiple app entity types.
411
+
412
+ ```swift
413
+ @available(iOS 26, *)
414
+ @UnionValue
415
+ enum ShoppingVisualResult {
416
+ case product(ProductEntity)
417
+ case store(StoreEntity)
418
+ }
419
+
420
+ @available(iOS 26, *)
421
+ struct ShoppingVisualQuery: IntentValueQuery {
422
+ func values(for input: SemanticContentDescriptor) async throws -> [ShoppingVisualResult] {
423
+ try Task.checkCancellation()
424
+ async let productMatches = ProductStore.shared.matches(
425
+ labels: input.labels,
426
+ pixelBuffer: input.pixelBuffer,
427
+ limit: 5
428
+ )
429
+ async let storeMatches = StoreStore.shared.matches(
430
+ labels: input.labels,
431
+ pixelBuffer: input.pixelBuffer,
432
+ limit: 3
433
+ )
434
+ let ranked = await rank(productMatches, storeMatches)
435
+ return Array(ranked.prefix(8))
436
+ }
437
+ }
438
+ ```
439
+
440
+ Treat `labels` as high-level English descriptors, not exhaustive synonyms or app
441
+ taxonomy; combine them with `pixelBuffer` when available. Return small, ranked,
442
+ cancellation-friendly results, and provide an `OpenIntent`, URL representation,
443
+ or in-app search handoff for details and more results. Do not implement camera
444
+ capture, Vision `VN*` requests, barcode classification, or Spotlight indexing
445
+ inside the App Intents query; call an existing bounded app search or image-match
446
+ service instead, with explicit result caps and timeouts when work may exceed a
447
+ system UI budget.
448
+
286
449
  ## Assistant Schemas (iOS 18+)
287
450
 
288
451
  Assistant schemas define domain-specific intents that Apple Intelligence
@@ -750,6 +913,21 @@ If your app already creates `CSSearchableItem` values, call
750
913
  `OpenIntent` for the entity type so Spotlight results can open the right app
751
914
  content.
752
915
 
916
+ ### Updating and Deleting Indexed Entities
917
+
918
+ Update and delete changed records in the same named index. For large syncs, use
919
+ `beginBatch()`, `endBatch(withClientState:)`, and `fetchLastClientState()` so
920
+ indexing can resume after a crash or jetsam.
921
+
922
+ ```swift
923
+ let recipeIndex = CSSearchableIndex(name: "Recipes")
924
+ try await recipeIndex.indexAppEntities(changedRecipes)
925
+ try await recipeIndex.deleteAppEntities(
926
+ identifiedBy: deletedRecipeIDs,
927
+ ofType: RecipeEntity.self
928
+ )
929
+ ```
930
+
753
931
  ### Hide specific entities from Spotlight UI
754
932
 
755
933
  ```swift
@@ -20,6 +20,9 @@ Material 3 (androidx.compose.material3).
20
20
  - [Typography Tokens](#typography-tokens)
21
21
  - [Shape Tokens](#shape-tokens)
22
22
  - [Color Token Mapping](#color-token-mapping)
23
+ - [Two-Layer Design Tokens](#two-layer-design-tokens)
24
+ - [Bridging Tokens to Material 3](#bridging-tokens-to-material-3)
25
+ - [Component Catalog](#component-catalog)
23
26
  - [Modifier Ordering](#modifier-ordering)
24
27
  - [Do's and Don'ts](#dos-and-donts)
25
28
  - [Troubleshooting](#troubleshooting)
@@ -362,6 +365,46 @@ Card(shape = MaterialTheme.shapes.medium) { /* content */ }
362
365
  | `outline` | `colorScheme.outline` | Borders, dividers |
363
366
  | `outlineVariant` | `colorScheme.outlineVariant` | Subtle borders |
364
367
 
368
+ ## Two-Layer Design Tokens
369
+
370
+ Scale token management with two layers. Layer 1 is a machine-generated raw-token
371
+ source: one file, never hand-edited, the single source of truth for every
372
+ primitive color, size, and font value. Layer 2 is a hand-authored semantic
373
+ wrapper: `@Immutable` color and typography data classes that name each role and
374
+ reference the raw tokens by name, never a literal hex or `sp`. Each semantic
375
+ model exposes a `light()` and a `dark()` variant.
376
+
377
+ The codegen pipeline that produces Layer 1 is a separate concern; see the
378
+ `android-design-tokens-codegen` skill. This skill covers the consumption side:
379
+ the semantic wrappers and how components read them.
380
+
381
+ Full `RawTokens`, `@Immutable AppColors`/`AppTypography` with `light()`/`dark()`:
382
+ [references/patterns.md#two-layer-design-tokens](references/patterns.md#two-layer-design-tokens).
383
+
384
+ ## Bridging Tokens to Material 3
385
+
386
+ Bridge the semantic tokens to both worlds. Select an M3 `light`/`darkColorScheme`
387
+ built from the tokens so stock Material components inherit the brand colors, and
388
+ expose the richer token set through `CompositionLocalProvider`
389
+ (`staticCompositionLocalOf`). Read the rich set through a `Theme.colors` /
390
+ `Theme.typography` accessor marked `@ReadOnlyComposable`, so custom components
391
+ reach tokens Material's `colorScheme` cannot express while stock components still
392
+ theme correctly.
393
+
394
+ Full `LocalAppColors`, `toMaterialColorScheme`, `AppTheme`, `Theme` accessor:
395
+ [references/patterns.md#bridging-tokens-to-material-3](references/patterns.md#bridging-tokens-to-material-3).
396
+
397
+ ## Component Catalog
398
+
399
+ Add a component-catalog module (for example Showkase) that renders every
400
+ annotated composable and token in a browsable in-app gallery. Keep the annotation
401
+ cheap and universal so it can sit on previews across every module, but gate the
402
+ KSP processor behind a build flag so the browser codegen costs nothing on normal
403
+ builds; build the catalog on demand with a `-P` flag.
404
+
405
+ Full annotated preview, flag-gated `ksp` wiring, catalog entry point:
406
+ [references/patterns.md#component-catalog](references/patterns.md#component-catalog).
407
+
365
408
  ## Modifier Ordering
366
409
 
367
410
  Modifier order matters. Modifiers are applied outside-in, so the order
@@ -439,3 +482,8 @@ Box(
439
482
  - [ ] Collapsible TopAppBar has `nestedScroll` modifier
440
483
  - [ ] Dynamic color has API 31 guard with static fallback
441
484
  - [ ] No mixing of M2 and M3 imports in the same screen
485
+ - [ ] Raw tokens come from the generated single source; semantic wrappers reference them by name, no literal hex/`sp` in the wrapper
486
+ - [ ] Semantic color/typography models are `@Immutable` with `light()` and `dark()` variants
487
+ - [ ] Rich tokens exposed via `CompositionLocalProvider` and read through a `@ReadOnlyComposable` `Theme.colors`/`Theme.typography` accessor
488
+ - [ ] M3 `light`/`darkColorScheme` derived from the tokens so stock Material components inherit brand colors
489
+ - [ ] Catalog KSP processor gated behind a build flag, annotation kept universal