@mmerterden/multi-agent-pipeline 20.8.1 → 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 (75) hide show
  1. package/CHANGELOG.md +36 -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 +75 -78
  13. package/package.json +4 -1
  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 -14
  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/log-metric.sh +9 -3
  31. package/pipeline/scripts/migrate-prefs.mjs +18 -4
  32. package/pipeline/scripts/pre-commit-check.sh +119 -31
  33. package/pipeline/scripts/scan-agent-config.sh +9 -9
  34. package/pipeline/scripts/unattended_policy.py +12 -3
  35. package/pipeline/scripts/uninstall.mjs +88 -1
  36. package/pipeline/scripts/usage-identity.mjs +1 -1
  37. package/pipeline/scripts/usage-register.mjs +1 -1
  38. package/pipeline/skills/.skill-manifest.json +11 -11
  39. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
  40. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
  41. package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
  42. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
  43. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
  44. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  45. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +2 -1
  46. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +14 -13
  47. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  48. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +7 -7
  49. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +105 -4
  50. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +20 -5
  51. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +8 -7
  52. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -8
  53. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +20 -7
  54. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
  55. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +8 -7
  56. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +15 -8
  57. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +17 -9
  58. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -17
  59. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +28 -7
  60. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
  61. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
  62. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
  63. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
  64. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
  65. package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
  66. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
  67. package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
  68. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
  69. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
  70. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
  71. package/pipeline/skills/shared/external/skill-creator/template.md +1 -1
  72. package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
  73. package/pipeline/scripts/gen-ref-toc.mjs +0 -279
  74. package/pipeline/scripts/make-manifest.mjs +0 -199
  75. package/pipeline/scripts/scorecard-snapshot.mjs +0 -178
@@ -13,7 +13,10 @@ import os
13
13
  import re
14
14
  import sys
15
15
 
16
- from _shared import IDENT, WEB_DOLLAR_T, WEB_T, WORD, is_interpolated, lsk_keys, source_files
16
+ from _shared import (
17
+ ACCESSOR_EXTENSIONS, DEFAULT_KEY_TYPES, IDENT, WEB_DOLLAR_T, WEB_T, WORD, is_interpolated, key_type_alternation,
18
+ key_type_warning, key_types, lsk_keys, source_files,
19
+ )
17
20
 
18
21
  ANDROID = {".kt", ".kts", ".xml"}
19
22
  WEB = {".ts", ".tsx", ".js", ".jsx", ".vue"}
@@ -26,7 +29,6 @@ R_STRING = re.compile(rf"\bR\.string\.([{WORD}]+)")
26
29
  XML_STRING = re.compile(rf"@string/([{WORD}.]+)")
27
30
  WEB_T_DYNAMIC = re.compile(r"(?<![\w$])t\(\s*[`$A-Za-z_]")
28
31
  WEB_DOLLAR_T_DYNAMIC = re.compile(r"\$t\(\s*[`$A-Za-z_]")
29
- LSK_DYNAMIC = re.compile(r"LocalizationStringKey\(")
30
32
  FORMAT_DYNAMIC = re.compile(r"String\(format:")
31
33
  GETSTRING_DYNAMIC = re.compile(r"\bgetString\(\s*[A-Za-z_]")
32
34
  INTERPOLATED_LOCALIZED = re.compile(r"\"([^\"\n]*(?:\\\(|\$\{|\$[A-Za-z_])[^\"\n]*)\"\.localized\b")
@@ -42,7 +44,11 @@ def platform_of(path):
42
44
  return "ios"
43
45
 
44
46
 
45
- def hits_in_line(line, ext):
47
+ def dynamic_key_pattern(types):
48
+ return re.compile(rf"({key_type_alternation(types)})\(")
49
+
50
+
51
+ def hits_in_line(line, ext, types=DEFAULT_KEY_TYPES):
46
52
  static, dynamic = [], []
47
53
  if ext == ".xml":
48
54
  static.extend(m.group(1) for m in XML_STRING.finditer(line))
@@ -55,12 +61,13 @@ def hits_in_line(line, ext):
55
61
  if WEB_DOLLAR_T_DYNAMIC.search(line):
56
62
  dynamic.append("(dynamic: $t(...))")
57
63
  return static, dynamic
58
- static.extend(lsk_keys(line))
64
+ static.extend(lsk_keys(line, types))
59
65
  static.extend(m.group(1) for m in LITERAL_LOCALIZED.finditer(line) if not is_interpolated(m.group(1)))
60
66
  static.extend(m.group(1) for m in R_STRING.finditer(line))
61
- if LSK_DYNAMIC.search(line):
62
- dynamic.append("(dynamic: LocalizationStringKey(...))")
63
- mentions_key = any(marker in line for marker in (".localized", "LocalizationStringKey"))
67
+ constructed = dynamic_key_pattern(types).search(line)
68
+ if constructed:
69
+ dynamic.append(f"(dynamic: {constructed.group(1)}(...))")
70
+ mentions_key = any(marker in line for marker in (".localized", *types))
64
71
  if mentions_key and FORMAT_DYNAMIC.search(line):
65
72
  dynamic.append("(dynamic: String(format:...))")
66
73
  if "R.string." not in line and GETSTRING_DYNAMIC.search(line):
@@ -69,26 +76,30 @@ def hits_in_line(line, ext):
69
76
  dynamic.append(f'(dynamic: "{match.group(1)}".localized)')
70
77
  for match in RECEIVER_LOCALIZED.finditer(line):
71
78
  receiver = match.group(1)
72
- if not receiver.startswith("LocalizationStringKey"):
79
+ if not receiver.startswith(tuple(types)):
73
80
  dynamic.append(f"(dynamic: {receiver}.localized)")
74
81
  return static, dynamic
75
82
 
76
83
 
77
- def scan(root):
84
+ def scan(root, types=DEFAULT_KEY_TYPES):
78
85
  keys = []
79
86
  seen = set()
87
+ accessor_texts = []
80
88
  scanned = 0
81
89
  for path in source_files(root):
82
90
  try:
83
91
  with open(path, encoding="utf-8") as stream:
84
- lines = stream.read().splitlines()
92
+ text = stream.read()
85
93
  except (OSError, UnicodeDecodeError):
86
94
  continue
95
+ lines = text.splitlines()
87
96
  scanned += 1
88
97
  ext = os.path.splitext(path)[1]
98
+ if ext in ACCESSOR_EXTENSIONS:
99
+ accessor_texts.append(text)
89
100
  platform = platform_of(path)
90
101
  for number, line in enumerate(lines, 1):
91
- static, dynamic = hits_in_line(line, ext)
102
+ static, dynamic = hits_in_line(line, ext, types)
92
103
  in_error = bool(ERROR_CONTEXT.search(line))
93
104
  found = [(k, "error" if in_error else "static") for k in static] + [(k, "dynamic") for k in dynamic]
94
105
  for key, category in found:
@@ -104,7 +115,7 @@ def scan(root):
104
115
  "line": number,
105
116
  "snippet": line.strip()[:200],
106
117
  })
107
- return scanned, keys
118
+ return scanned, keys, key_type_warning(accessor_texts, types)
108
119
 
109
120
 
110
121
  CATEGORIES = ("static", "error", "dynamic")
@@ -115,22 +126,31 @@ def main():
115
126
  cli.add_argument("--screen-path", required=True, help="folder or single file of the built screen")
116
127
  cli.add_argument("--out", help="JSON destination; stdout when omitted")
117
128
  cli.add_argument("--category", default="all", choices=["all", "error", "dynamic", "static"])
129
+ cli.add_argument(
130
+ "--key-type", action="append", metavar="NAME",
131
+ help="type whose member chains are keys, e.g. the project's generated key enum; repeatable or comma list "
132
+ f"(default {', '.join(DEFAULT_KEY_TYPES)})",
133
+ )
118
134
  opts = cli.parse_args()
119
135
 
120
136
  if not os.path.exists(opts.screen_path):
121
137
  cli.error(f"--screen-path not found: {opts.screen_path}")
138
+ try:
139
+ types = key_types(opts.key_type)
140
+ except ValueError as exc:
141
+ cli.error(str(exc))
122
142
 
123
- scanned, keys = scan(opts.screen_path)
143
+ scanned, keys, type_warning = scan(opts.screen_path, types)
124
144
  if opts.category != "all":
125
145
  keys = [entry for entry in keys if entry["category"] == opts.category]
126
146
  keys.sort(key=lambda entry: (entry["category"], entry["file"], entry["line"]))
127
147
  per_category = [entry["category"] for entry in keys]
128
148
  counts = {name: per_category.count(name) for name in CATEGORIES}
129
149
 
130
- text = json.dumps(
131
- {"screenPath": opts.screen_path, "filesScanned": scanned, "counts": counts, "keys": keys},
132
- indent=2, ensure_ascii=False,
133
- ) + "\n"
150
+ report = {"screenPath": opts.screen_path, "filesScanned": scanned, "counts": counts, "keys": keys}
151
+ if type_warning:
152
+ report["keyTypeWarning"] = type_warning
153
+ text = json.dumps(report, indent=2, ensure_ascii=False) + "\n"
134
154
  if opts.out:
135
155
  with open(opts.out, "w", encoding="utf-8") as sink:
136
156
  sink.write(text)
@@ -138,6 +158,8 @@ def main():
138
158
  sys.stdout.write(text)
139
159
  summary = ", ".join(f"{counts[name]} {name}" for name in CATEGORIES)
140
160
  print(f"# {scanned} file(s) read: {summary}", file=sys.stderr)
161
+ if type_warning:
162
+ print(f"# {type_warning}", file=sys.stderr)
141
163
 
142
164
 
143
165
  if __name__ == "__main__":
@@ -12,14 +12,15 @@ import re
12
12
  import sys
13
13
 
14
14
  from _shared import (
15
- LITERAL_KEY, WEB_DOLLAR_T, WEB_T, clean_key, is_component, is_dynamic, lsk_keys, normalize_key, raw_key,
16
- source_files,
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
17
  )
18
18
 
19
19
 
20
- def scan_code(root):
20
+ def scan_code(root, types=DEFAULT_KEY_TYPES):
21
21
  keys = set()
22
22
  corpus = []
23
+ accessor_texts = []
23
24
  scanned = 0
24
25
  for path in source_files(root):
25
26
  try:
@@ -29,10 +30,12 @@ def scan_code(root):
29
30
  continue
30
31
  scanned += 1
31
32
  corpus.append(text.lower())
32
- keys.update(key.lower() for key in lsk_keys(text))
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))
33
36
  for pattern, group in ((LITERAL_KEY, 1), (WEB_T, 2), (WEB_DOLLAR_T, 2)):
34
37
  keys.update(hit.group(group).lower() for hit in pattern.finditer(text))
35
- return keys, "\n".join(corpus), scanned
38
+ return keys, "\n".join(corpus), scanned, key_type_warning(accessor_texts, types, mapping_hint=True)
36
39
 
37
40
 
38
41
  def filled(value):
@@ -54,12 +57,26 @@ def main():
54
57
  parser.add_argument("--screen-path", required=True, help="folder or single file of the built screen")
55
58
  parser.add_argument("--resources-root", help="resources checkout or snapshot; turns on the Suggested-file check")
56
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
+ )
57
65
  args = parser.parse_args()
58
66
 
59
67
  with open(args.mapping, encoding="utf-8") as stream:
60
68
  mapping = json.load(stream)
61
69
  rows = mapping.get("rows") or []
62
- 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)
63
80
 
64
81
  verified = 0
65
82
  exempt = 0
@@ -113,6 +130,8 @@ def main():
113
130
  "valueGaps": gaps,
114
131
  "untranslated": untranslated,
115
132
  }
133
+ if type_warning:
134
+ report["keyTypeWarning"] = type_warning
116
135
  text = json.dumps(report, indent=2, ensure_ascii=False) + "\n"
117
136
  if args.out:
118
137
  with open(args.out, "w", encoding="utf-8") as sink:
@@ -128,6 +147,8 @@ def main():
128
147
  f"value gaps {len(gaps)} ({len(no_legacy)} hard), untranslated {len(untranslated)}",
129
148
  file=sys.stderr,
130
149
  )
150
+ if type_warning:
151
+ print(f"# {type_warning}", file=sys.stderr)
131
152
  for item in unverified[:12]:
132
153
  print(f"# UNVERIFIED {item['key']} ({item['element']})", file=sys.stderr)
133
154
  for key in not_in_map[:12]:
@@ -135,7 +156,7 @@ def main():
135
156
  if hard:
136
157
  print(f"# FAIL: {hard} hard issue(s); fix them or record a conscious waiver before publishing", file=sys.stderr)
137
158
  sys.exit(2)
138
- print("# PASS", file=sys.stderr)
159
+ print("# PASS, but see the key type WARNING above" if type_warning else "# PASS", file=sys.stderr)
139
160
 
140
161
 
141
162
  if __name__ == "__main__":
@@ -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` and an optional `metadata` block with `version` (semver of the skill text) and `source` (`multi-agent-pipeline` for a first-party skill, otherwise the upstream it derives from, which a `NOTICE-*.md` beside it must list). Drop the block rather than leave a placeholder in it.
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
 
@@ -44,7 +44,9 @@ optional.
44
44
 
45
45
  Most requests are value types. Requests that must remember earlier frames are
46
46
  `final class` types conforming to `StatefulRequest`: `TrackObjectRequest`,
47
- `TrackRectangleRequest`, `TrackOpticalFlowRequest`, `DetectTrajectoriesRequest`,
47
+ `TrackRectangleRequest`, `TrackOpticalFlowRequest`,
48
+ `TrackTranslationalImageRegistrationRequest`,
49
+ `TrackHomographicImageRegistrationRequest`, `DetectTrajectoriesRequest`,
48
50
  `DetectHumanBodyPose3DRequest` and `GeneratePersonSegmentationRequest`. Keep one
49
51
  instance alive for the whole sequence.
50
52