@mmerterden/multi-agent-pipeline 20.8.0 → 20.8.2

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 (112) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/docs/facts.json +3 -3
  3. package/index.js +1 -0
  4. package/install/_codex-agents.mjs +1 -1
  5. package/install/_common.mjs +486 -53
  6. package/install/_mcp-register.mjs +173 -117
  7. package/install/claude.mjs +281 -220
  8. package/install/codex.mjs +7 -7
  9. package/install/copilot.mjs +13 -11
  10. package/install/index.mjs +92 -27
  11. package/install/templates/claude-hooks.json +9 -9
  12. package/manifest.json +112 -114
  13. package/package.json +5 -2
  14. package/pipeline/commands/multi-agent/update/SKILL.md +28 -17
  15. package/pipeline/lib/confusables.json +79 -33
  16. package/pipeline/lib/extract-conventions.sh +3 -3
  17. package/pipeline/lib/json-file-lock.mjs +27 -7
  18. package/pipeline/lib/normalize-text.mjs +86 -17
  19. package/pipeline/lib/outbound-gate.mjs +13 -4
  20. package/pipeline/lib/redact.mjs +87 -13
  21. package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
  22. package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
  23. package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
  24. package/pipeline/multi-agent-refs/conventions-defaults.md +1 -1
  25. package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
  26. package/pipeline/scripts/agent-guard.py +150 -25
  27. package/pipeline/scripts/audit-log.sh +3 -4
  28. package/pipeline/scripts/autopilot-runner.mjs +14 -5
  29. package/pipeline/scripts/doctor.mjs +8 -2
  30. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  31. package/pipeline/scripts/log-metric.sh +9 -3
  32. package/pipeline/scripts/migrate-prefs.mjs +18 -4
  33. package/pipeline/scripts/pre-commit-check.sh +119 -27
  34. package/pipeline/scripts/scan-agent-config.sh +9 -9
  35. package/pipeline/scripts/unattended_policy.py +12 -3
  36. package/pipeline/scripts/uninstall.mjs +88 -1
  37. package/pipeline/scripts/usage-identity.mjs +1 -1
  38. package/pipeline/scripts/usage-register.mjs +1 -1
  39. package/pipeline/skills/.skill-manifest.json +30 -30
  40. package/pipeline/skills/shared/README.md +64 -64
  41. package/pipeline/skills/shared/external/alarmkit/SKILL.md +2 -2
  42. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +2 -2
  43. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +6 -0
  44. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +3 -0
  45. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +3 -0
  46. package/pipeline/skills/shared/external/app-store-review/SKILL.md +3 -4
  47. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +5 -3
  48. package/pipeline/skills/shared/external/authentication/SKILL.md +29 -17
  49. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +3 -1
  50. package/pipeline/skills/shared/external/background-processing/SKILL.md +10 -8
  51. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +7 -7
  52. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
  53. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
  54. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +4 -2
  55. package/pipeline/skills/shared/external/core-data/SKILL.md +12 -2
  56. package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
  57. package/pipeline/skills/shared/external/coreml/SKILL.md +1 -1
  58. package/pipeline/skills/shared/external/cryptokit/SKILL.md +1 -1
  59. package/pipeline/skills/shared/external/device-integrity/SKILL.md +11 -5
  60. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
  61. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
  62. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +6 -6
  63. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +3 -2
  64. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +20 -18
  65. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +4 -4
  66. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +12 -9
  67. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +201 -0
  68. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +64 -30
  69. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  70. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +20 -12
  71. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -7
  72. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +26 -12
  73. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
  74. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +51 -24
  75. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +19 -9
  76. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +26 -19
  77. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -46
  78. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +37 -63
  79. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +4 -2
  80. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
  81. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
  82. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
  83. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
  84. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
  85. package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
  86. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
  87. package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
  88. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
  89. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
  90. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
  91. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  92. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +6 -1
  93. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +3 -2
  94. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +1 -1
  95. package/pipeline/skills/shared/external/swift-security/SKILL.md +10 -8
  96. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +8 -5
  97. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +13 -9
  98. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +7 -6
  99. package/pipeline/skills/shared/external/swift-testing/SKILL.md +8 -5
  100. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +1 -1
  101. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +3 -2
  102. package/pipeline/skills/shared/external/swiftdata/SKILL.md +5 -3
  103. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +17 -9
  104. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +4 -2
  105. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +13 -6
  106. package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
  107. package/pipeline/skills/shared/external/weatherkit/SKILL.md +8 -5
  108. package/pipeline/skills/shared/external/widgetkit/SKILL.md +15 -7
  109. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +8 -6
  110. package/pipeline/scripts/gen-ref-toc.mjs +0 -279
  111. package/pipeline/scripts/make-manifest.mjs +0 -199
  112. package/pipeline/scripts/scorecard-snapshot.mjs +0 -178
@@ -11,47 +11,16 @@ import os
11
11
  import re
12
12
  import sys
13
13
 
14
- EXTENSIONS = {".swift", ".kt", ".kts", ".xml", ".ts", ".tsx", ".js", ".jsx", ".vue"}
15
- SKIP_PARTS = ("/.git", "/Generated/")
16
-
17
- WORD = "A-Za-z0-9_"
18
- LSK = re.compile(rf"LocalizationStringKey((?:\.[A-Za-z_][{WORD}]*)+)")
19
- LITERAL_LOCALIZED = re.compile(rf"\"([{WORD}.]+)\"\.localized\b")
20
- WEB_T = re.compile(r"(?<![\w$])t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
21
- WEB_DOLLAR_T = re.compile(r"\$t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
22
- TRAILING_MARKER = re.compile(r"\s*\([^()]*\)\s*$")
23
- COMPONENT_MARKER = re.compile(r"\([^)]*(owned by|bile\u015fen|component)[^)]*\)", re.IGNORECASE)
24
- PLACEHOLDER = re.compile(r"<[^<>]+>")
25
-
26
-
27
- def strip_accessor(chain):
28
- segments = chain.strip(".").split(".")
29
- while segments and segments[-1].startswith("localized"):
30
- segments.pop()
31
- return ".".join(segments)
32
-
33
-
34
- def normalize_key(key):
35
- return ".".join(seg[:1].upper() + seg[1:] for seg in key.split("."))
36
-
37
-
38
- def source_files(root):
39
- if os.path.isfile(root):
40
- yield root
41
- return
42
- for current, dirs, files in os.walk(root):
43
- probe = current.replace(os.sep, "/") + "/"
44
- if any(part in probe for part in SKIP_PARTS):
45
- dirs[:] = []
46
- continue
47
- for name in sorted(files):
48
- if os.path.splitext(name)[1] in EXTENSIONS:
49
- yield os.path.join(current, name)
14
+ from _shared import (
15
+ ACCESSOR_EXTENSIONS, DEFAULT_KEY_TYPES, LITERAL_KEY, WEB_DOLLAR_T, WEB_T, clean_key, is_component, is_dynamic,
16
+ key_type_warning, key_types, lsk_keys, normalize_key, raw_key, source_files,
17
+ )
50
18
 
51
19
 
52
- def scan_code(root):
20
+ def scan_code(root, types=DEFAULT_KEY_TYPES):
53
21
  keys = set()
54
22
  corpus = []
23
+ accessor_texts = []
55
24
  scanned = 0
56
25
  for path in source_files(root):
57
26
  try:
@@ -61,28 +30,16 @@ def scan_code(root):
61
30
  continue
62
31
  scanned += 1
63
32
  corpus.append(text.lower())
64
- chains = (strip_accessor(hit.group(1)) for hit in LSK.finditer(text))
65
- keys.update(chain.lower() for chain in chains if chain)
66
- for pattern, group in ((LITERAL_LOCALIZED, 1), (WEB_T, 2), (WEB_DOLLAR_T, 2)):
33
+ if os.path.splitext(path)[1] in ACCESSOR_EXTENSIONS:
34
+ accessor_texts.append(text)
35
+ keys.update(key.lower() for key in lsk_keys(text, types))
36
+ for pattern, group in ((LITERAL_KEY, 1), (WEB_T, 2), (WEB_DOLLAR_T, 2)):
67
37
  keys.update(hit.group(group).lower() for hit in pattern.finditer(text))
68
- return keys, "\n".join(corpus), scanned
69
-
38
+ return keys, "\n".join(corpus), scanned, key_type_warning(accessor_texts, types, mapping_hint=True)
70
39
 
71
- def is_component(row):
72
- text = " ".join(str(row.get(f) or "") for f in ("newKey", "element", "note"))
73
- return bool(COMPONENT_MARKER.search(text))
74
40
 
75
-
76
- def is_dynamic(row, key):
77
- element = str(row.get("element") or "").lower()
78
- note = str(row.get("note") or "").lower()
79
- return (
80
- key.startswith("(dynamic")
81
- or bool(PLACEHOLDER.search(key))
82
- or "dinamik" in element
83
- or "dynamic" in element
84
- or "enumerate at runtime" in note
85
- )
41
+ def filled(value):
42
+ return bool(str(value or "").strip())
86
43
 
87
44
 
88
45
  def suggested_exists(root, key):
@@ -100,12 +57,26 @@ def main():
100
57
  parser.add_argument("--screen-path", required=True, help="folder or single file of the built screen")
101
58
  parser.add_argument("--resources-root", help="resources checkout or snapshot; turns on the Suggested-file check")
102
59
  parser.add_argument("--out", help="JSON report destination; stdout when omitted")
60
+ parser.add_argument(
61
+ "--key-type", action="append", metavar="NAME",
62
+ help="type whose member chains are keys; repeatable or comma list. Falls back to the mapping's "
63
+ f"keyType, then {', '.join(DEFAULT_KEY_TYPES)}",
64
+ )
103
65
  args = parser.parse_args()
104
66
 
105
67
  with open(args.mapping, encoding="utf-8") as stream:
106
68
  mapping = json.load(stream)
107
69
  rows = mapping.get("rows") or []
108
- code_keys, corpus, scanned = scan_code(args.screen_path)
70
+ from_mapping = mapping.get("keyType") or []
71
+ if isinstance(from_mapping, str):
72
+ from_mapping = [from_mapping]
73
+ elif not (isinstance(from_mapping, list) and all(isinstance(name, str) for name in from_mapping)):
74
+ parser.error(f"keyType in the mapping must be a string or a list of strings, got {from_mapping!r}")
75
+ try:
76
+ types = key_types(args.key_type or from_mapping)
77
+ except ValueError as exc:
78
+ parser.error(str(exc))
79
+ code_keys, corpus, scanned, type_warning = scan_code(args.screen_path, types)
109
80
 
110
81
  verified = 0
111
82
  exempt = 0
@@ -114,8 +85,7 @@ def main():
114
85
  map_keys = set()
115
86
 
116
87
  for row in rows:
117
- raw_key = str(row.get("newKey") or "").strip()
118
- key = raw_key if raw_key.startswith("(dynamic") else TRAILING_MARKER.sub("", raw_key)
88
+ key = clean_key(raw_key(row))
119
89
  component = is_component(row)
120
90
  dynamic = is_dynamic(row, key)
121
91
  if key:
@@ -136,11 +106,11 @@ def main():
136
106
  verdict = row.get("verdict")
137
107
  new = row.get("new") or {}
138
108
  legacy = row.get("legacy") or {}
139
- if verdict != "new" and not new.get("tr") and not dynamic:
109
+ if verdict != "new" and not filled(new.get("tr")) and not dynamic:
140
110
  gaps.append({"key": key, "issue": "new.tr empty"})
141
- if verdict in ("reuse", "review") and not (legacy.get("en") or legacy.get("tr")):
111
+ if verdict in ("reuse", "review") and not (filled(legacy.get("en")) or filled(legacy.get("tr"))):
142
112
  gaps.append({"key": key, "issue": f"verdict={verdict} but no legacy value"})
143
- if new.get("tr") and new.get("tr") == new.get("en"):
113
+ if filled(new.get("tr")) and new.get("tr") == new.get("en"):
144
114
  untranslated.append(key)
145
115
  if args.resources_root and not component and not dynamic and key and not suggested_exists(args.resources_root, key):
146
116
  gaps.append({"key": key, "issue": f"no Suggested/{normalize_key(key)}.json"})
@@ -160,6 +130,8 @@ def main():
160
130
  "valueGaps": gaps,
161
131
  "untranslated": untranslated,
162
132
  }
133
+ if type_warning:
134
+ report["keyTypeWarning"] = type_warning
163
135
  text = json.dumps(report, indent=2, ensure_ascii=False) + "\n"
164
136
  if args.out:
165
137
  with open(args.out, "w", encoding="utf-8") as sink:
@@ -175,6 +147,8 @@ def main():
175
147
  f"value gaps {len(gaps)} ({len(no_legacy)} hard), untranslated {len(untranslated)}",
176
148
  file=sys.stderr,
177
149
  )
150
+ if type_warning:
151
+ print(f"# {type_warning}", file=sys.stderr)
178
152
  for item in unverified[:12]:
179
153
  print(f"# UNVERIFIED {item['key']} ({item['element']})", file=sys.stderr)
180
154
  for key in not_in_map[:12]:
@@ -182,7 +156,7 @@ def main():
182
156
  if hard:
183
157
  print(f"# FAIL: {hard} hard issue(s); fix them or record a conscious waiver before publishing", file=sys.stderr)
184
158
  sys.exit(2)
185
- print("# PASS", file=sys.stderr)
159
+ print("# PASS, but see the key type WARNING above" if type_warning else "# PASS", file=sys.stderr)
186
160
 
187
161
 
188
162
  if __name__ == "__main__":
@@ -309,9 +309,11 @@ func lookUp(_ text: String) async throws -> MKMapItem? {
309
309
  Reverse lookup is `MKReverseGeocodingRequest(location:)`. Formatting and
310
310
  batch lookups are in [Geocoding and Places on iOS 26](references/mapkit-patterns.md#geocoding-and-places-on-ios-26).
311
311
 
312
- ### CLGeocoder (iOS 5 to iOS 25, deprecated in iOS 26)
312
+ ### CLGeocoder (available since iOS 5, deprecated in iOS 26)
313
313
 
314
- Use it only while the deployment target is below iOS 26. Every `CLPlacemark`
314
+ Use it only while the deployment target is below iOS 26. The SDK's deprecation
315
+ messages point reverse lookups to `MKReverseGeocodingRequest` and forward
316
+ lookups to `MKGeocodingRequest`. Every `CLPlacemark`
315
317
  field is optional. `CLGeocoder` is not `Sendable`, so Swift 6 rejects one
316
318
  shared global instance; make a geocoder for each lookup instead.
317
319
 
@@ -14,10 +14,11 @@ charges.
14
14
  Examples assume Swift 6.3 and iOS 26.
15
15
 
16
16
  One rule shapes advanced checkouts: a single `PKPaymentRequest` may carry at
17
- most one optional advanced type. The candidates are a recurring request, an
18
- automatic-reload request, a deferred request, the Apple Pay Later setting, and
19
- multi-token contexts. When a flow
20
- needs two of them, build two requests.
17
+ most one of `multiTokenContexts`, `recurringPaymentRequest`,
18
+ `automaticReloadPaymentRequest` and `deferredPaymentRequest`. When a flow
19
+ needs two of them, build two requests. `applePayLaterAvailability` (iOS 17+)
20
+ is not part of that rule; it is a separate opt-out of Apple Pay Later that
21
+ defaults to enabled.
21
22
 
22
23
  ## Setup
23
24
 
@@ -10,10 +10,11 @@ Each `PKPaymentRequest` may set only one of these:
10
10
  - `recurringPaymentRequest`
11
11
  - `automaticReloadPaymentRequest`
12
12
  - `deferredPaymentRequest`
13
- - the Apple Pay Later availability setting
14
13
  - `multiTokenContexts`
15
14
 
16
- If a checkout needs two, split it into two requests.
15
+ If a checkout needs two, split it into two requests. `applePayLaterAvailability`
16
+ (iOS 17+) sits outside this rule: it opts the request out of Apple Pay Later,
17
+ and defaults to enabled; the exclusion covers only the four above.
17
18
 
18
19
  ## Recurring payments
19
20
 
@@ -133,8 +133,8 @@ drawing throws there. `PKContentVersion` names each step:
133
133
  |---|---|
134
134
  | `.version1` | Original inks: pen, pencil, marker (iPadOS 14 era) |
135
135
  | `.version2` | Inks added in iPadOS 17: `.monoline`, `.fountainPen`, `.watercolor`, `.crayon` |
136
- | `.version3` | Barrel-roll angle data on strokes |
137
- | `.version4` | Reed pen |
136
+ | `.version3` | The new fountain pen ink (iOS 17.5, macOS 14.5) |
137
+ | `.version4` | The new reed pen ink (iOS 26, macOS 26, visionOS 26) |
138
138
 
139
139
  When reviewing compatibility, lay out this whole table before recommending a
140
140
  cap, and if the fix is a curated picker, say that `PKToolPicker(toolItems:)`
@@ -19,7 +19,7 @@
19
19
  "id": 1,
20
20
  "name": "older-ipad-sync",
21
21
  "prompt": "Teachers annotate worksheets on new iPads and students open them on iPads still running iPadOS 16. Students report that some worksheets fail to open. Our plan is to set maximumSupportedContentVersion to .version2 on the canvas. Review the plan.",
22
- "expected_output": "Explains that inks newer than the recipient supports make loading fail, gives the full content version map (version1 pen/pencil/marker, version2 the iPadOS 17 inks, version3 barrel-roll data, version4 reed pen), and says .version2 does not help iPadOS 16, which needs .version1 or a verified fallback drawing. The cap must be set on both canvas and tool picker. Notes .reed needs 26 and curated picker items need iOS 18 or visionOS 2.",
22
+ "expected_output": "Explains that inks newer than the recipient supports make loading fail, gives the full content version map (version1 pen/pencil/marker, version2 the iPadOS 17 inks, version3 the new fountain pen from iOS 17.5, version4 the new reed pen), and says .version2 does not help iPadOS 16, which needs .version1 or a verified fallback drawing. The cap must be set on both canvas and tool picker. Notes .reed needs 26 and curated picker items need iOS 18 or visionOS 2.",
23
23
  "files": [],
24
24
  "expectations": [
25
25
  "States that unsupported inks make older systems fail to load the drawing",
@@ -168,7 +168,7 @@ func describe(_ sketch: PKDrawing) -> String {
168
168
  switch sketch.requiredContentVersion {
169
169
  case .version1: "pen, pencil, marker"
170
170
  case .version2: "adds monoline, fountain pen, watercolor, crayon"
171
- case .version3: "adds barrel-roll angles"
171
+ case .version3: "adds the new fountain pen (iOS 17.5)"
172
172
  case .version4: "adds reed pen"
173
173
  @unknown default: "unknown future content"
174
174
  }
@@ -215,9 +215,9 @@ func downgradedCopy(of sketch: PKDrawing) -> PKDrawing {
215
215
  }
216
216
  ```
217
217
 
218
- Swapping the ink is not always enough. The copied paths can still carry newer
219
- data such as barrel-roll angles, which is why the result is checked again
220
- before upload.
218
+ Swapping the ink is not always enough. The copies keep every other stroke
219
+ property, and only `requiredContentVersion` says what the result needs, which
220
+ is why the result is checked again before upload.
221
221
 
222
222
  ## A configurable SwiftUI wrapper
223
223
 
@@ -32,10 +32,14 @@ against the Xcode 26 SDK you ship with before relying on it.
32
32
  Add `@preconcurrency import PermissionKit`. The SwiftUI and UIKit/AppKit
33
33
  pieces (`PermissionButton`, `AskCenter.ask(_:in:)`) come in through
34
34
  cross-import overlays, so importing SwiftUI or UIKit next to PermissionKit is
35
- enough. The framework's types carry no `Sendable` annotations, so under Swift 6
36
- a plain import turns every `ask(_:in:)` call from the main actor into a
37
- "sending 'question' risks causing data races" error; `@preconcurrency` is what
38
- lets the examples below compile.
35
+ enough. Most of the framework's types are not `Sendable`: `PermissionQuestion`
36
+ (a class), `PermissionResponse`, `PermissionChoice`, `CommunicationTopic`,
37
+ `AskCenter` and `CommunicationLimits` declare no conformance. The exceptions
38
+ are `CommunicationHandle`, which is `Sendable`, and the SwiftUI views
39
+ `PermissionButton` and `CommunicationLimitsButton`. Because the question is not
40
+ `Sendable`, under Swift 6 a plain import turns every `ask(_:in:)` call from the
41
+ main actor into a "sending 'question' risks causing data races" error;
42
+ `@preconcurrency` is what lets the examples below compile.
39
43
 
40
44
  Do not add made-up entitlement keys for PermissionKit. If a signing
41
45
  requirement exists, it will be in Apple's documentation and in the Xcode
@@ -177,7 +181,11 @@ those.
177
181
  in which the child sends the question; it does not return the parent's
178
182
  answer. Decisions arrive later on `responses(for:)`. If the child backs out of
179
183
  the send sheet, no `PermissionResponse` ever arrives, so a pending request
180
- needs its own expiry or retry.
184
+ needs its own expiry or retry. `PermissionQuestion` has a settable
185
+ `expirationDate: Date?` (nil by default); once that date passes, the parent
186
+ can no longer respond. Set it before asking to bound the window, then move the
187
+ local entry to expired at the same date, since the SDK delivers nothing when a
188
+ question lapses.
181
189
 
182
190
  ```swift
183
191
  @available(iOS 26.2, *)
@@ -233,7 +241,8 @@ examples are in the
233
241
 
234
242
  Track each question by `question.id` (a `UUID`). Since a cancelled send
235
243
  produces nothing, give every pending entry a way out: a retry, an expiry, or
236
- both. When a flow combines known-handle checks with responses, keep the
244
+ both. When the question carries an `expirationDate`, use that date for the
245
+ local expiry so the app and the parent's window close together. When a flow combines known-handle checks with responses, keep the
237
246
  bundle-identifier guard from the section above.
238
247
 
239
248
  ```swift
@@ -186,7 +186,8 @@ print(q.expirationDate as Any) // nil unless set
186
186
  ```
187
187
 
188
188
  A question exposes `id`, `choices` (each with a `title`), `defaultChoice` and
189
- an optional `expirationDate`.
189
+ an optional, settable `expirationDate`; after that date the parent can no
190
+ longer respond.
190
191
 
191
192
  ## Limits-check view model
192
193
 
@@ -425,9 +425,13 @@ func handleChatAction(_ action: String, threadID: String?, typed: String?) async
425
425
  ## Grouping
426
426
 
427
427
  Notifications with the same `threadIdentifier` (local) or `thread-id` (APNs)
428
- stack into one group; each distinct value is its own group. `summaryArgument`
429
- and `summaryArgumentCount` on the content feed the group's summary line, and a
430
- category can shape that line:
428
+ stack into one group; each distinct value is its own group. Grouping comes
429
+ only from that identifier. `summaryArgument` and `summaryArgumentCount` on
430
+ `UNMutableNotificationContent` are deprecated as ignored on iOS 15 and later
431
+ (watchOS 8, tvOS 15); do not set them. A category can still supply a summary
432
+ format through `categorySummaryFormat` (iOS 12+, not deprecated). The argument
433
+ list in that format is optional, and with the per-notification argument ignored
434
+ a count-only format is the one to rely on:
431
435
 
432
436
  ```swift
433
437
  let chat = UNNotificationCategory(
@@ -435,7 +439,7 @@ let chat = UNNotificationCategory(
435
439
  actions: [],
436
440
  intentIdentifiers: [],
437
441
  hiddenPreviewsBodyPlaceholder: nil,
438
- categorySummaryFormat: "%u new messages from %@",
442
+ categorySummaryFormat: "%u new messages",
439
443
  options: []
440
444
  )
441
445
  ```
@@ -385,7 +385,7 @@ enum NotificationCategories {
385
385
  actions: [reply, markRead],
386
386
  intentIdentifiers: [],
387
387
  hiddenPreviewsBodyPlaceholder: nil,
388
- categorySummaryFormat: "%u more from %@",
388
+ categorySummaryFormat: "%u more messages",
389
389
  options: [.customDismissAction]
390
390
  )
391
391
 
@@ -147,16 +147,35 @@ apart.
147
147
  entities that carry `CollisionComponent` shapes. Use it to pick entities and
148
148
  target gestures, never to find planes.
149
149
  - For simple placement on a detected plane, use `AnchorEntity(.plane(...))`.
150
- - To intersect a real surface once, build an ARKit `ARRaycastQuery`, run it
151
- with `ARSession.raycast(_:)`, and anchor with `AnchorEntity(raycastResult:)`.
150
+ This is the placement path inside `RealityView`: `RealityViewCameraContent`
151
+ exposes no `ARSession`, so there is nothing to raycast against there.
152
+ - To intersect a real surface at a tapped point, host an `ARView` (through
153
+ `UIViewRepresentable`), call its `raycast(from:allowing:alignment:)`, and
154
+ anchor with `AnchorEntity(raycastResult:)`. The same view offers
155
+ `makeRaycastQuery(from:allowing:alignment:)` when an `ARRaycastQuery` is
156
+ needed for `arView.session.raycast(_:)`.
152
157
 
153
158
  ```swift
159
+ import RealityKit
160
+ import SwiftUI
161
+
162
+ struct PlaneDropView: View {
163
+ var body: some View {
164
+ RealityView { content in
165
+ content.camera = .spatialTracking
166
+ let floor = AnchorEntity(.plane(.horizontal, classification: .any, minimumBounds: [0.3, 0.3]))
167
+ floor.addChild(ModelEntity(mesh: .generateBox(size: 0.1)))
168
+ content.add(floor)
169
+ }
170
+ }
171
+ }
172
+
154
173
  @MainActor
155
- func placeOnSurface(_ item: Entity, query: ARRaycastQuery, session: ARSession, into content: RealityViewCameraContent) {
156
- guard let hit = session.raycast(query).first else { return }
174
+ func placeOnSurface(_ item: Entity, at point: CGPoint, in arView: ARView) {
175
+ guard let hit = arView.raycast(from: point, allowing: .estimatedPlane, alignment: .any).first else { return }
157
176
  let anchor = AnchorEntity(raycastResult: hit)
158
177
  anchor.addChild(item)
159
- content.add(anchor)
178
+ arView.scene.addAnchor(anchor)
160
179
  }
161
180
  ```
162
181
 
@@ -283,7 +302,7 @@ store it when you plan to cancel it later, or drop it with `_ =` as here.
283
302
  - [ ] Interaction notes explain that `CollisionComponent` provides the hittable shape, paired with `InputTargetComponent`
284
303
  - [ ] A Handoffs line mentions `AccessibilityComponent` and sends detailed SwiftUI and accessibility policy to the sibling skills
285
304
  - [ ] Entity hit tests, `ray(...)` and ARKit surface raycasts are kept distinct
286
- - [ ] Surface placement uses `ARSession.raycast(_:)` with `AnchorEntity(raycastResult:)`
305
+ - [ ] Surface placement inside `RealityView` uses `AnchorEntity(.plane(...))`; a tap raycast uses `ARView.raycast(from:allowing:alignment:)` with `AnchorEntity(raycastResult:)`
287
306
  - [ ] Per-frame logic uses `SceneEvents.Update`, not SwiftUI timers
288
307
  - [ ] Large scenes load with async `ModelEntity(named:)`, not `Entity.load(named:)`
289
308
  - [ ] Anchor targets match the kind of surface the feature needs
@@ -23,7 +23,7 @@
23
23
  "States that hitTest finds collision shapes on virtual entities, not physical surfaces",
24
24
  "States that ray(through:in:to:) is not evidence of a real surface",
25
25
  "Suggests AnchorEntity(.plane(...)) for simple plane placement",
26
- "Uses ARRaycastQuery with ARSession.raycast(_:) and AnchorEntity(raycastResult:) for one-shot placement",
26
+ "Notes that RealityViewCameraContent exposes no ARSession and uses ARView.raycast(from:allowing:alignment:) with AnchorEntity(raycastResult:) for one-shot tap placement",
27
27
  "Keeps collision shapes for entity interaction only"
28
28
  ]
29
29
  },
@@ -1,6 +1,6 @@
1
1
  # Skill templates
2
2
 
3
- Two skeletons. Copy the one that matches the layer, fill it, then delete every line that fails the cut test. The toolkits use these frontmatter keys: `name`, `description`, `allowed-tools`, `user-invocable`, plus an optional `argument-hint`.
3
+ Two skeletons. Copy the one that matches the layer, fill it, then delete every line that fails the cut test. The toolkits use these frontmatter keys: `name`, `description`, `allowed-tools`, `user-invocable`, plus an optional `argument-hint` and a `metadata` block with `version` (semver of the skill text) and `source` (`multi-agent-pipeline` for a first-party skill, otherwise `<upstream> (<license>)` for the upstream it derives from, which a `NOTICE-*.md` beside it must list). For a shared knowledge skill in the pipeline source the block with `source` is required: the pipeline's skill lint fails one that has none. Elsewhere the block is optional; drop it rather than leave a placeholder in it.
4
4
 
5
5
  ## Reference skill
6
6
 
@@ -10,6 +10,9 @@ name: {aspect-noun}
10
10
  description: "{What it covers} for {platform}. {When to reach for it}. Triggers: '{phrase}', '{phrase}', '{phrase}'."
11
11
  user-invocable: true
12
12
  allowed-tools: Bash, Read, Edit, Write, Glob, Grep
13
+ metadata:
14
+ version: {x.y.z}
15
+ source: {multi-agent-pipeline | upstream}
13
16
  ---
14
17
 
15
18
  # {name} - {short title}
@@ -44,6 +47,9 @@ description: "{What it does} for {platform}, with {guardrails}. Use when {situat
44
47
  user-invocable: true
45
48
  argument-hint: "{args}"
46
49
  allowed-tools: Bash, Read
50
+ metadata:
51
+ version: {x.y.z}
52
+ source: {multi-agent-pipeline | upstream}
47
53
  ---
48
54
 
49
55
  # {name} - {short title}
@@ -99,7 +99,7 @@ struct RecipesApp: App {
99
99
  }
100
100
 
101
101
  private static func watchTransactions() -> Task<Void, Never> {
102
- Task.detached(priority: .background) {
102
+ Task(priority: .background) {
103
103
  for await incoming in StoreKit.Transaction.updates {
104
104
  if case .verified(let receipt) = incoming {
105
105
  await EntitlementStore.shared.refresh()
@@ -114,6 +114,11 @@ struct RecipesApp: App {
114
114
  Unverified updates fall through untouched. For each verified one,
115
115
  entitlements are recomputed first and the transaction finished afterwards.
116
116
 
117
+ A plain `Task` is enough here. It inherits the main actor from the `App`, but
118
+ the loop spends its time suspended in `for await` and the entitlement refresh
119
+ runs on the main-actor `EntitlementStore` anyway; `Task.detached` would only add
120
+ a hop and drop the task-local values.
121
+
117
122
  ## Computing Entitlements
118
123
 
119
124
  ```swift
@@ -7,10 +7,11 @@ metadata:
7
7
 
8
8
  # Swift Concurrency
9
9
 
10
- Baseline: Swift 6.3 compilers and later. The aim of every change made with this
10
+ Baseline: Swift 6.2 compilers and later. The aim of every change made with this
11
11
  skill is data-race safety through isolation and `Sendable`, reached with the
12
12
  smallest possible change in behavior. Features newer than Swift 6.0 name the
13
- release or proposal that introduced them.
13
+ release or proposal that introduced them; the few that need Swift 6.3, such as
14
+ `weak let` (SE-0481), are marked as 6.3 only.
14
15
 
15
16
  Not for the smallest fix to a single diagnostic (use `swift-concurrency-expert`)
16
17
  or a file-by-file review pass (use `swift-concurrency-pro`).
@@ -157,7 +157,7 @@ nonisolated struct OffMainBackupBatch {
157
157
 
158
158
  `weak let` declares a weak reference that is never reassigned. Because the
159
159
  binding cannot change, a type storing only `weak let` references to `Sendable`
160
- objects can itself be `Sendable`. Implemented in Swift 6.3.
160
+ objects can itself be `Sendable`. Swift 6.3 only.
161
161
 
162
162
  ```swift
163
163
  final class SessionObserver: Sendable {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: swift-security
3
- description: "Apple client security: Keychain (SecItem, kSecClass, OSStatus, iOS and macOS), Face ID/Touch ID (LAContext), CryptoKit (AES-GCM, ChaChaPoly, ECDSA, ECDH, HPKE, ML-KEM), Secure Enclave, OAuth token and API key storage, SecTrust/SPKI pinning, keychain sharing, moving secrets out of UserDefaults or plists, OWASP MASVS/MASTG. Use when writing, reviewing or migrating code that stores secrets, holds keys or evaluates trust. Not for passkeys, App Attest or App Review."
3
+ description: "Apple client security: Keychain (SecItem, kSecClass, OSStatus, iOS, macOS), Face ID/Touch ID keychain items, CryptoKit (AES-GCM, ChaChaPoly, ECDSA, ECDH, HPKE, ML-KEM), Secure Enclave, OAuth token and API key storage, SecTrust/SPKI pinning, keychain sharing, moving secrets out of UserDefaults or plists, OWASP MASVS/MASTG. Use when writing, reviewing or migrating code that stores secrets, holds keys or evaluates trust. Not for passkeys, App Attest or App Review."
4
4
  metadata:
5
5
  source: multi-agent-pipeline
6
6
  ---
@@ -81,9 +81,9 @@ and should be worded that way.
81
81
  `errSecDuplicateItem`, call `SecItemUpdate`. Deleting and re-adding is
82
82
  reserved for changing an item's access control, which `SecItemUpdate`
83
83
  cannot do.
84
- 6. `SecItem*` calls must not run on the main actor. Isolate them in an actor on
85
- iOS 17 and later,
86
- or a private serial queue for older targets.
84
+ 6. `SecItem*` calls must not run on the main actor. Isolate them in an actor
85
+ (available from iOS 13) or, in completion-handler code, a private serial
86
+ queue.
87
87
  7. On macOS, pass `kSecUseDataProtectionKeychain: true` unless the code is
88
88
  deliberately working with the legacy file-based keychain.
89
89
  8. Never encrypt twice with the same AES-GCM nonce under one key.
@@ -111,9 +111,10 @@ These answers apply across every reference file in this skill.
111
111
  `.privateKeyUsage` in its flags, for example
112
112
  `[.privateKeyUsage, .biometryCurrentSet]`.
113
113
  - Any code path that shows Face ID needs `NSFaceIDUsageDescription` in
114
- `Info.plist`. Apple's LocalAuthentication documentation warns that requests
115
- can fail without it, and that applies to keychain reads that trigger Face ID
116
- as much as to `evaluatePolicy`.
114
+ `Info.plist`. The `LAContext` header asks apps to supply it, shows its string
115
+ the first time Face ID is used, and fails evaluations with
116
+ `LAError.biometryNotAvailable` once the user denies Face ID for the app. That
117
+ applies to keychain reads that trigger Face ID as much as to `evaluatePolicy`.
117
118
  - `arc4random` on Apple platforms is backed by a cryptographically secure
118
119
  generator, not RC4. Its limits are a 32-bit result and the bias of `% n`. For
119
120
  keys, nonces, salts and tokens, use `SecRandomCopyBytes`,
@@ -130,7 +131,8 @@ in hardware, and checks of server trust. Adjacent topics appear here only to
130
131
  point at the right skill:
131
132
 
132
133
  - `authentication`: passkeys, Sign in with Apple, OAuth sign-in flows,
133
- `ASAuthorizationController`, credential state, sign-in UX.
134
+ `ASAuthorizationController`, credential state, sign-in UX, and `LAContext`
135
+ prompts and policy that do not guard a keychain item.
134
136
  - `cryptokit`: CryptoKit questions with no storage, keychain, Secure Enclave,
135
137
  trust or compliance angle.
136
138
  - `device-integrity`: DeviceCheck and App Attest attestations and assertions.
@@ -4,7 +4,8 @@ What is here: the Security types for certificates, trust, identities and policie
4
4
  pinning, whether by leaf, by issuing CA, by public-key digest or declaratively through
5
5
  `NSPinnedDomains`; building your own trust policy; mutual TLS; the points where App
6
6
  Transport Security and trust evaluation overlap; and running a pin set in production.
7
- It applies from iOS 12 through iOS 18 and macOS 10.14 through 15.
7
+ It applies from iOS 12 and macOS 10.14 through the current releases (checked
8
+ against the iOS 26.4 SDK).
8
9
 
9
10
  What is not here: transport concerns other than checking TLS certificates, managing
10
11
  certificates on your servers, and ATS as a configuration topic (the `ios-networking`
@@ -72,7 +73,7 @@ func checkTrustOffMain(_ trust: SecTrust, then report: @escaping @Sendable (Trus
72
73
  `nonisolated(unsafe)`: the caller promises not to touch the trust while it is
73
74
  being evaluated. `report` is `@Sendable` because it runs on that queue.
74
75
 
75
- Apple has not added async/await versions of these calls as of iOS 18. A continuation
76
+ The iOS 26.4 SDK still has no async/await version of these calls. A continuation
76
77
  bridges the gap:
77
78
 
78
79
  ```swift
@@ -477,7 +478,8 @@ Storing certificates and identities in the Keychain is covered in
477
478
  - ATS requires, for every `URLSession` connection: TLS 1.2 or later, RSA keys of at
478
479
  least 2048 bits or ECC of at least 256 bits, SHA-256 or stronger certificate
479
480
  signatures, AES-128 or AES-256, and forward secrecy.
480
- - From iOS 17, ATS also requires HTTPS when connecting to a bare IP address.
481
+ - For connections to IP-address literals, check Apple's `NSAppTransportSecurity`
482
+ documentation for how ATS applies on the deployment target.
481
483
  - Keys that draw App Review scrutiny: `NSAllowsArbitraryLoads`,
482
484
  `NSAllowsArbitraryLoadsForMedia`, `NSAllowsArbitraryLoadsInWebContent`,
483
485
  `NSExceptionAllowsInsecureHTTPLoads`, `NSExceptionMinimumTLSVersion`.
@@ -491,8 +493,9 @@ Storing certificates and identities in the Keychain is covered in
491
493
  | iOS 13 / macOS 10.15 | `SecTrustEvaluateAsyncWithError`; `SecTrustEvaluate` deprecated |
492
494
  | iOS 14 / macOS 11 | `NSPinnedDomains`; `SecTrustCopyKey` replaces `SecTrustCopyPublicKey` |
493
495
  | iOS 15 / macOS 12 | `SecTrustCopyCertificateChain` replaces `SecTrustGetCertificateAtIndex` and `SecTrustGetCertificateCount` |
494
- | iOS 17 / macOS 14 | Connections to literal IPs fall under ATS; TLS 1.3 for EAP |
495
- | iOS 18 / macOS 15 | Trust API unchanged; the Swift 6 language mode makes the closure-based Security calls harder to adopt |
496
+ | iOS 18 / macOS 15 | `SecCertificateCopyNotValidBeforeDate` and `SecCertificateCopyNotValidAfterDate` |
497
+ | iOS 18.4 / macOS 15.4 | `kSecTrustQWACValidation`, `kSecTrustQCStatements` and the `kSecPolicyApple*` server and client policy constants |
498
+ | iOS 26 / macOS 26 | No new trust-evaluation API; still no async/await form of `SecTrustEvaluateAsyncWithError` |
496
499
 
497
500
  ## Threads and performance
498
501