@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.
- package/CHANGELOG.md +36 -0
- package/docs/facts.json +3 -3
- package/index.js +1 -0
- package/install/_codex-agents.mjs +1 -1
- package/install/_common.mjs +486 -53
- package/install/_mcp-register.mjs +173 -117
- package/install/claude.mjs +281 -220
- package/install/codex.mjs +7 -7
- package/install/copilot.mjs +13 -11
- package/install/index.mjs +92 -27
- package/install/templates/claude-hooks.json +9 -9
- package/manifest.json +75 -78
- package/package.json +4 -1
- package/pipeline/commands/multi-agent/update/SKILL.md +28 -17
- package/pipeline/lib/confusables.json +79 -33
- package/pipeline/lib/extract-conventions.sh +3 -3
- package/pipeline/lib/json-file-lock.mjs +27 -7
- package/pipeline/lib/normalize-text.mjs +86 -17
- package/pipeline/lib/outbound-gate.mjs +13 -4
- package/pipeline/lib/redact.mjs +87 -14
- package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
- package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
- package/pipeline/multi-agent-refs/conventions-defaults.md +1 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
- package/pipeline/scripts/agent-guard.py +150 -25
- package/pipeline/scripts/audit-log.sh +3 -4
- package/pipeline/scripts/autopilot-runner.mjs +14 -5
- package/pipeline/scripts/doctor.mjs +8 -2
- package/pipeline/scripts/log-metric.sh +9 -3
- package/pipeline/scripts/migrate-prefs.mjs +18 -4
- package/pipeline/scripts/pre-commit-check.sh +119 -31
- package/pipeline/scripts/scan-agent-config.sh +9 -9
- package/pipeline/scripts/unattended_policy.py +12 -3
- package/pipeline/scripts/uninstall.mjs +88 -1
- package/pipeline/scripts/usage-identity.mjs +1 -1
- package/pipeline/scripts/usage-register.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +11 -11
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +14 -13
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +7 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +105 -4
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +20 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -8
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +20 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +15 -8
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +17 -9
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -17
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +28 -7
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/skill-creator/template.md +1 -1
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
- package/pipeline/scripts/gen-ref-toc.mjs +0 -279
- package/pipeline/scripts/make-manifest.mjs +0 -199
- 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
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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` |
|
|
137
|
-
| `.version4` |
|
|
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
|
|
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
|
|
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
|
|
219
|
-
|
|
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.
|
|
36
|
-
a
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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.
|
|
429
|
-
and `summaryArgumentCount` on
|
|
430
|
-
|
|
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
|
|
442
|
+
categorySummaryFormat: "%u new messages",
|
|
439
443
|
options: []
|
|
440
444
|
)
|
|
441
445
|
```
|
package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md
CHANGED
|
@@ -385,7 +385,7 @@ enum NotificationCategories {
|
|
|
385
385
|
actions: [reply, markRead],
|
|
386
386
|
intentIdentifiers: [],
|
|
387
387
|
hiddenPreviewsBodyPlaceholder: nil,
|
|
388
|
-
categorySummaryFormat: "%u more
|
|
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
|
-
|
|
151
|
-
|
|
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,
|
|
156
|
-
guard let hit =
|
|
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
|
-
|
|
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 `
|
|
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
|
-
"
|
|
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
|
|
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`,
|
|
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
|
|