@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
@@ -85,6 +85,13 @@ process.stdin.on("end", () => {
85
85
  });
86
86
  ' 2>/dev/null || true)"
87
87
  fi
88
+ # A command that never spells git, even with its quotes and backslashes
89
+ # removed (g'i't, "git", \git), cannot run it through any path the helper
90
+ # below would recognise, so it returns before paying for the parse.
91
+ if [ -n "$HOOK_COMMAND" ] \
92
+ && ! printf '%s' "$HOOK_COMMAND" | tr -d "\"'\\\\" | grep -qi 'git'; then
93
+ exit 0
94
+ fi
88
95
  if [ -n "$HOOK_COMMAND" ]; then
89
96
  # Commit detection is a tokenizer, not a regex: agent-guard.py shares its
90
97
  # shell parser through `--git-subcommand`, so an absolute git path, a
@@ -156,7 +163,7 @@ is_binary() {
156
163
  # a diff, or the whole file for something not yet tracked at all - every line
157
164
  # of a brand-new file is an addition).
158
165
  scan_file() {
159
- local file="$1" content="$2"
166
+ local file="$1" content="$2" folded="${3:-}"
160
167
 
161
168
  # .env files
162
169
  if echo "$file" | grep -qE '\.env($|\.)'; then
@@ -174,17 +181,11 @@ scan_file() {
174
181
 
175
182
  [ -z "$content" ] && return 0
176
183
 
177
- # A token written with an invisible character or a look-alike letter: scan a
178
- # folded copy too. Only non-ASCII content pays for the node call.
179
- if printf '%s' "$content" | LC_ALL=C grep -q '[^ -~[:space:]]' 2>/dev/null \
180
- && command -v node >/dev/null 2>&1 \
181
- && [ -f "$PRE_COMMIT_DIR/../lib/normalize-text.mjs" ]; then
182
- local folded
183
- folded=$(printf '%s' "$content" | node "$PRE_COMMIT_DIR/../lib/normalize-text.mjs" 2>/dev/null) || folded=""
184
- if [ -n "$folded" ] && [ "$folded" != "$content" ]; then
185
- content="$content
184
+ # A token written with an invisible character or a look-alike letter: the
185
+ # folded copy (see fold_and_scan) is scanned together with the raw text.
186
+ if [ -n "$folded" ] && [ "$folded" != "$content" ]; then
187
+ content="$content
186
188
  $folded"
187
- fi
188
189
  fi
189
190
 
190
191
  # API keys / tokens (key = "long_string" pattern)
@@ -281,15 +282,50 @@ $folded"
281
282
  if [ -n "$candidates" ]; then
282
283
  entropy_hit=$(printf '%s\n' "$candidates" | awk '
283
284
  # Word-shaped identifier: letters and _ or - only, split into segments at
284
- # separators and case boundaries (UIApplication -> UI, Application), where
285
- # the segments read like words: average length 4+, none longer than 24,
286
- # and at least 70 percent carrying a vowel. Random letters-only tokens
287
- # flip case every one or two characters, so their segments average about
288
- # two characters with few vowels; a long lowercase run is one oversized
289
- # segment. Neither shape passes.
290
- function wordlike(t, n, i, c, p, nx, seg, nseg, total, maxlen, voweled, split_here) {
285
+ # separators and case boundaries (UIApplication -> UI, Application),
286
+ # where the segments average 4+ letters, none is longer than 24, and the
287
+ # letter pairs inside them read like English. PAIR scores every pair as
288
+ # log2 of how often it occurs across a 235k-word English word list
289
+ # relative to a uniform draw, rounded and clipped to -4..4 (stored +4,
290
+ # one row per first letter). Identifiers made of words average above
291
+ # +0.5 per pair even with an abbreviation or two (Qr, Ffp, Otp, Kg);
292
+ # uniformly random letters average below +0.1, so the mean must reach
293
+ # +0.25. Clipping keeps one unusual pair from outweighing the rest. An
294
+ # all-caps segment of up to five letters inside a token that also has
295
+ # lowercase letters is an acronym (URL, HTTP, JSON) and is not scored.
296
+ BEGIN {
297
+ rows = "06655352403757150767433232 52015000500600400421400010 " \
298
+ "60306006504400600515500040 50026021600313500420301030 " \
299
+ "63564442401657452876433420 40004300400400400302400020 " \
300
+ "50005033500423400510400030 60006000600322600403301050 " \
301
+ "64764551103657651577350105 20002000000000200000200000 " \
302
+ "30005001400201200010100010 71237220701622630024520060 " \
303
+ "64006000600032650010400040 62667462613324631357422031 " \
304
+ "44553352403667560766644321 60006006500501630644400030 " \
305
+ "00000000000000000000400000 73447233703354740445432050 " \
306
+ "51506105602453552067502040 61307106700421600624502050 " \
307
+ "44443230401657240665000000 40006000400000300000100000 " \
308
+ "40004002400102400100000000 20102000300000220002000020 " \
309
+ "31332020200433340343000000 30004000200000300000000011";
310
+ split(rows, row, " ");
311
+ alpha = "abcdefghijklmnopqrstuvwxyz";
312
+ for (r = 1; r <= 26; r++)
313
+ for (k = 1; k <= 26; k++)
314
+ PAIR[substr(alpha, r, 1) substr(alpha, k, 1)] = substr(row[r], k, 1) - 4;
315
+ }
316
+ function score_segment(seg, has_lower, l, j) {
317
+ if (has_lower && seg ~ /^[A-Z]+$/ && length(seg) <= 5) return;
318
+ l = tolower(seg);
319
+ for (j = 2; j <= length(l); j++) {
320
+ pairs++;
321
+ pair_score += PAIR[substr(l, j - 1, 2)];
322
+ }
323
+ }
324
+ function wordlike(t, n, i, c, p, nx, seg, nseg, total, maxlen, split_here, has_lower) {
291
325
  if (t !~ /^[A-Za-z_-]+$/) return 0;
292
- n = length(t); seg = ""; nseg = 0; total = 0; maxlen = 0; voweled = 0;
326
+ has_lower = (t ~ /[a-z]/);
327
+ pairs = 0; pair_score = 0;
328
+ n = length(t); seg = ""; nseg = 0; total = 0; maxlen = 0;
293
329
  for (i = 1; i <= n + 1; i++) {
294
330
  c = (i <= n) ? substr(t, i, 1) : "_";
295
331
  split_here = 0;
@@ -302,13 +338,13 @@ $folded"
302
338
  if (split_here && seg != "") {
303
339
  nseg++; total += length(seg);
304
340
  if (length(seg) > maxlen) maxlen = length(seg);
305
- if (tolower(seg) ~ /[aeiouy]/) voweled++;
341
+ score_segment(seg, has_lower);
306
342
  seg = "";
307
343
  }
308
344
  if (c != "_" && c != "-") seg = seg c;
309
345
  }
310
- if (nseg == 0) return 0;
311
- return (total / nseg >= 4 && maxlen <= 24 && voweled * 10 >= nseg * 7);
346
+ if (nseg == 0 || pairs == 0) return 0;
347
+ return (total / nseg >= 4 && maxlen <= 24 && pair_score * 4 >= pairs);
312
348
  }
313
349
  {
314
350
  s=$0;
@@ -357,9 +393,63 @@ $folded"
357
393
  fi
358
394
  }
359
395
 
396
+ # Files are queued and scanned together so that the look-alike fold runs in
397
+ # one node process for every non-ASCII file of the run rather than one per
398
+ # file. ASCII content folds to itself and is not sent.
399
+ Q_FILES=()
400
+ Q_CONTENTS=()
401
+ queue_file() {
402
+ Q_FILES+=("$1")
403
+ Q_CONTENTS+=("$2")
404
+ }
405
+
406
+ FOLD_WARNED=0
407
+ fold_warn() {
408
+ [ "$FOLD_WARNED" = "1" ] && return 0
409
+ FOLD_WARNED=1
410
+ echo "pre-commit-check: $1 - non-ASCII content scanned without look-alike folding" >&2
411
+ }
412
+
413
+ fold_and_scan() {
414
+ local i n=${#Q_FILES[@]} lib="$PRE_COMMIT_DIR/../lib/normalize-text.mjs" out rec
415
+ local -a idx=() folded=()
416
+ for ((i = 0; i < n; i++)); do
417
+ folded[i]=""
418
+ if printf '%s' "${Q_CONTENTS[i]}" | LC_ALL=C grep -q '[^ -~[:space:]]' 2>/dev/null; then
419
+ idx+=("$i")
420
+ fi
421
+ done
422
+ if [ "${#idx[@]}" -gt 0 ]; then
423
+ if ! command -v node >/dev/null 2>&1 || [ ! -f "$lib" ]; then
424
+ fold_warn "node or lib/normalize-text.mjs unavailable"
425
+ else
426
+ out=$(mktemp "${TMPDIR:-/tmp}/pre-commit-fold.XXXXXX") || out=""
427
+ if [ -n "$out" ] && for i in "${idx[@]}"; do printf '%s\0' "${Q_CONTENTS[i]}"; done \
428
+ | node "$lib" --nul >"$out" 2>/dev/null; then
429
+ local -a recs=()
430
+ while IFS= read -r -d '' rec; do recs+=("$rec"); done <"$out"
431
+ if [ "${#recs[@]}" -eq "${#idx[@]}" ]; then
432
+ for ((i = 0; i < ${#idx[@]}; i++)); do folded[idx[i]]="${recs[i]}"; done
433
+ else
434
+ fold_warn "the look-alike fold returned ${#recs[@]} of ${#idx[@]} records"
435
+ fi
436
+ else
437
+ fold_warn "the look-alike fold (node lib/normalize-text.mjs) failed"
438
+ fi
439
+ [ -n "$out" ] && rm -f "$out"
440
+ fi
441
+ fi
442
+ for ((i = 0; i < n; i++)); do
443
+ scan_file "${Q_FILES[i]}" "${Q_CONTENTS[i]}" "${folded[i]}"
444
+ done
445
+ Q_FILES=()
446
+ Q_CONTENTS=()
447
+ }
448
+
360
449
  if [ -n "$SCAN_FILE_TARGET" ]; then
361
450
  is_binary "$SCAN_FILE_TARGET" && exit 0
362
- scan_file "$SCAN_FILE_TARGET" "$(cat "$SCAN_FILE_TARGET" 2>/dev/null)"
451
+ queue_file "$SCAN_FILE_TARGET" "$(cat "$SCAN_FILE_TARGET" 2>/dev/null)"
452
+ fold_and_scan
363
453
  if [ "$FOUND" -eq 1 ]; then
364
454
  echo "" >&2
365
455
  echo "Secret check failed on $SCAN_FILE_TARGET." >&2
@@ -402,10 +492,11 @@ if [ -n "$SCAN_RANGE" ]; then
402
492
  # blob at this commit so a NUL-laden file cannot hide a key.
403
493
  content=$(gitscan show --text --no-textconv "${commit}:${file}" 2>/dev/null || true)
404
494
  fi
405
- scan_file "$file" "$content"
495
+ queue_file "$file" "$content"
406
496
  done < <(gitscan diff-tree --no-commit-id -r --root -m --first-parent \
407
497
  --name-only -z --diff-filter=d "$commit" 2>/dev/null)
408
498
  done
499
+ fold_and_scan
409
500
  if [ "$FOUND" -eq 1 ]; then
410
501
  echo "" >&2
411
502
  echo "Secret check failed on $SCAN_RANGE." >&2
@@ -424,7 +515,7 @@ while IFS= read -r -d '' file; do
424
515
  [ ! -f "$file" ] && continue
425
516
  is_binary "$file" && continue
426
517
  content=$(git diff --cached -- "$file" 2>/dev/null | grep "^+" | grep -v "^+++" || true)
427
- scan_file "$file" "$content"
518
+ queue_file "$file" "$content"
428
519
  done < <(git diff --cached --name-only -z 2>/dev/null)
429
520
 
430
521
  if [ "$SCAN_UNSTAGED" = "1" ]; then
@@ -435,7 +526,7 @@ if [ "$SCAN_UNSTAGED" = "1" ]; then
435
526
  [ ! -f "$file" ] && continue
436
527
  is_binary "$file" && continue
437
528
  content=$(git diff HEAD -- "$file" 2>/dev/null | grep "^+" | grep -v "^+++" || true)
438
- scan_file "$file" "$content"
529
+ queue_file "$file" "$content"
439
530
  done < <(git diff HEAD --name-only -z 2>/dev/null)
440
531
 
441
532
  # Untracked (brand-new) files - there is no diff to take yet, so every line
@@ -445,9 +536,10 @@ if [ "$SCAN_UNSTAGED" = "1" ]; then
445
536
  [ ! -f "$file" ] && continue
446
537
  is_binary "$file" && continue
447
538
  content=$(sed 's/^/+/' "$file" 2>/dev/null || true)
448
- scan_file "$file" "$content"
539
+ queue_file "$file" "$content"
449
540
  done < <(git ls-files --others --exclude-standard -z 2>/dev/null)
450
541
  fi
542
+ fold_and_scan
451
543
 
452
544
  if [ $FOUND -eq 1 ]; then
453
545
  echo "" >&2
@@ -123,16 +123,16 @@ for _event, entries in (t.get("hooks") or {}).items():
123
123
  for e in entries:
124
124
  for h in e.get("hooks", []):
125
125
  c = h.get("command", "")
126
- # Allow only `bash $HOME/.claude/scripts/<name>` plus plain long
127
- # flags. Flags had to be admitted because a hook that cannot take
128
- # one cannot be correct: SessionEnd runs capture-flush with
129
- # --if-stale, and without it every session end would re-flush a run
130
- # that already finished. What stays banned is everything that could
131
- # turn a flag into a second command - no spaces inside a token, no
132
- # quotes, no metacharacters, and the eval/substitution check below
133
- # still runs on the whole string.
126
+ # Allow only `bash "$HOME/.claude/scripts/<name>"` (the path quoted
127
+ # so a HOME with a space still runs it, or unquoted) plus plain long
128
+ # flags. SessionEnd needs one: capture-flush takes --if-stale so a
129
+ # session end does not re-flush a run that already finished. What
130
+ # stays banned is everything that could turn a flag into a second
131
+ # command - no spaces inside a token, no quotes beyond the pair
132
+ # around the path, no metacharacters, and the eval/substitution
133
+ # check below still runs on the whole string.
134
134
  if not re.fullmatch(
135
- r'bash \$HOME/\.claude/scripts/[A-Za-z0-9._-]+(?: --[a-z0-9][a-z0-9-]*)*',
135
+ r'bash (?:"\$HOME/\.claude/scripts/[A-Za-z0-9._-]+"|\$HOME/\.claude/scripts/[A-Za-z0-9._-]+)(?: --[a-z0-9][a-z0-9-]*)*',
136
136
  c.strip()):
137
137
  bad.append(c)
138
138
  if re.search(r'\beval\b|\$\(|`|[;&|><]', c):
@@ -451,7 +451,14 @@ def structural_refusal(cmd):
451
451
  return None
452
452
 
453
453
 
454
+ # shlex is quadratic in a token's length; a longer segment is split on
455
+ # whitespace with quotes and backslashes removed (agent-guard.py coarse_tokens).
456
+ SHLEX_MAX_CHARS = 16384
457
+
458
+
454
459
  def tokenize(text):
460
+ if len(text) > SHLEX_MAX_CHARS:
461
+ return [re.sub(r"[\"'\\]", "", t) for t in text.split()]
455
462
  try:
456
463
  return shlex.split(text)
457
464
  except ValueError:
@@ -2603,9 +2610,11 @@ TOOLKIT_PREFIX = "mcp__multi-agent-toolkit__"
2603
2610
  # Keys a toolkit tool uses for a filesystem destination it writes. Checked with
2604
2611
  # check_path so a toolkit call cannot write a screenshot, recording, build
2605
2612
  # output or export over a protected path. MCP tools run outside the OS sandbox,
2606
- # so this is their only path boundary.
2607
- TOOLKIT_PATH_KEYS = ("path", "filename", "out_dir", "output_path", "output_dir",
2608
- "export_path", "derived_data_path", "result_bundle_path",
2613
+ # so this is their only path boundary. The toolkit's write parameters are
2614
+ # listed in test/fixtures/toolkit-path-params.json, generated from its tool
2615
+ # schemas, and the unattended-guard test checks each one against this list.
2616
+ TOOLKIT_PATH_KEYS = ("path", "filename", "out_dir", "output", "output_path", "output_dir",
2617
+ "output_graph", "export_path", "derived_data_path", "result_bundle_path",
2609
2618
  "figma_path", "screenshot_path", "video_path", "file_path")
2610
2619
 
2611
2620
 
@@ -49,6 +49,7 @@ import {
49
49
  writeFileSync,
50
50
  } from "fs";
51
51
  import { execFileSync } from "child_process";
52
+ import { createHash } from "crypto";
52
53
  import { join, dirname } from "path";
53
54
  import { homedir } from "os";
54
55
  import { pathToFileURL } from "url";
@@ -82,6 +83,22 @@ export const PIPELINE_AGENT_FILES = [
82
83
  */
83
84
  export const PIPELINE_CORE_SKILL_DIRS = ["apple-archive-compliance", "google-play-compliance"];
84
85
 
86
+ /**
87
+ * Files the installer ships into ~/.claude/templates (install/templates/).
88
+ * Used only for an install that predates the install manifest; a unit test
89
+ * asserts it matches the source tree.
90
+ */
91
+ export const PIPELINE_TEMPLATE_FILES = [
92
+ "claude-hooks.json",
93
+ "codex-instructions.md",
94
+ "copilot-instructions.md",
95
+ "multi-agent-autopilot-awake.plist.template",
96
+ "multi-agent-autopilot.plist.template",
97
+ ];
98
+
99
+ /** Written by install/_common.mjs writeInstallManifest at the host dir root. */
100
+ export const INSTALL_MANIFEST = ".pipeline-manifest.json";
101
+
85
102
  const flags = process.argv.slice(2).filter((a) => a !== "uninstall");
86
103
 
87
104
  const TOOL_FLAGS = [
@@ -238,6 +255,65 @@ function preservePrefsCredentialMapping(prefsPath) {
238
255
  );
239
256
  }
240
257
 
258
+ function sha256Of(path) {
259
+ try {
260
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ function rmdirIfEmpty(dir) {
267
+ try {
268
+ if (readdirSync(dir).length > 0) return;
269
+ } catch {
270
+ return;
271
+ }
272
+ rmIfExists(dir);
273
+ }
274
+
275
+ /**
276
+ * Remove the co-owned files the install manifest records (top-level commands,
277
+ * lib/, templates/), each only while its hash is still the one installed. An
278
+ * edited file is the user's now and stays. Returns false when there is no
279
+ * manifest, so the caller falls back to the pre-manifest cleanup.
280
+ *
281
+ * @param {string} hostDir e.g. ~/.claude
282
+ * @param {string[]} keepInLib lib/ files kept on purpose
283
+ * @returns {boolean}
284
+ */
285
+ function rmManifestFiles(hostDir, keepInLib) {
286
+ const manifestPath = join(hostDir, INSTALL_MANIFEST);
287
+ let files;
288
+ try {
289
+ files = JSON.parse(readFileSync(manifestPath, "utf-8"))?.files;
290
+ } catch {
291
+ return false;
292
+ }
293
+ if (!files || typeof files !== "object") return false;
294
+ const kept = [];
295
+ const dirs = new Set();
296
+ for (const [rel, hash] of Object.entries(files)) {
297
+ if (typeof rel !== "string" || rel.split("/").some((part) => part === ".." || part === "")) {
298
+ continue;
299
+ }
300
+ if (rel.startsWith("lib/") && keepInLib.includes(rel.slice(4))) continue;
301
+ const path = join(hostDir, rel);
302
+ if (!existsSync(path)) continue;
303
+ dirs.add(dirname(path));
304
+ if (sha256Of(path) === hash) rmIfExists(path);
305
+ else kept.push(rel);
306
+ }
307
+ for (const dir of [...dirs].sort((a, b) => b.length - a.length)) {
308
+ if (dir !== join(hostDir, "commands")) rmdirIfEmpty(dir);
309
+ }
310
+ if (kept.length > 0) {
311
+ console.log(` kept ${kept.length} file(s) you changed since install: ${kept.join(", ")}`);
312
+ }
313
+ rmIfExists(manifestPath);
314
+ return true;
315
+ }
316
+
241
317
  /**
242
318
  * Conditional rm - respects --dry-run.
243
319
  * @param {string} path
@@ -766,8 +842,19 @@ export async function main() {
766
842
  rmIfExists(join(CLAUDE, "scripts"));
767
843
  // Pipeline-managed trees the installer lays down alongside scripts/.
768
844
  rmIfExists(join(CLAUDE, "multi-agent-refs"));
845
+ rmIfExists(join(CLAUDE, ".pipeline-swap"));
769
846
  rmIfExists(join(CLAUDE, "schemas"));
770
- rmDirExcept(join(CLAUDE, "lib"), KEEP_IN_LIB);
847
+ if (!rmManifestFiles(CLAUDE, KEEP_IN_LIB)) {
848
+ // An install that predates the manifest wiped lib/ and templates/ on
849
+ // every run, so their shipped names are the pipeline's. The top-level
850
+ // commands cannot be told from the user's own and stay.
851
+ rmDirExcept(join(CLAUDE, "lib"), KEEP_IN_LIB);
852
+ rmMatchingFiles(join(CLAUDE, "templates"), (name) => PIPELINE_TEMPLATE_FILES.includes(name));
853
+ rmdirIfEmpty(join(CLAUDE, "templates"));
854
+ console.log(
855
+ ` note: no ${INSTALL_MANIFEST}; top-level files in ${join(CLAUDE, "commands")} were left in place`,
856
+ );
857
+ }
771
858
  rmIfExists(join(CLAUDE, ".pipeline-version"));
772
859
  // NOTE: ~/.claude/rules/ is USER-OWNED. install lays baseline rules down
773
860
  // write-if-missing and NEVER overwrites them (install/claude.mjs
@@ -200,7 +200,7 @@ function persist(prefsPath, patch, env) {
200
200
  j.global.usageLog = { ...(j.global.usageLog || {}), ...patch };
201
201
  return j;
202
202
  },
203
- { mode: 0o600, timeoutMs: lockTimeoutFrom(env) },
203
+ { mode: 0o600, followSymlink: true, timeoutMs: lockTimeoutFrom(env) },
204
204
  );
205
205
  return written !== undefined;
206
206
  } catch {
@@ -230,7 +230,7 @@ export function persistPrefs(
230
230
  if (registeredAs) j.global.usageLog.registeredAs = registeredAs;
231
231
  return j;
232
232
  },
233
- { mode: 0o600, timeoutMs: lockTimeoutFrom(process.env) },
233
+ { mode: 0o600, followSymlink: true, timeoutMs: lockTimeoutFrom(process.env) },
234
234
  );
235
235
  return written !== undefined;
236
236
  } catch {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-26T19:14:09Z",
3
+ "generatedAt": "2026-09-27T07:55:06Z",
4
4
  "skillCount": 218,
5
5
  "entries": [
6
6
  {
@@ -269,7 +269,7 @@
269
269
  },
270
270
  {
271
271
  "path": "shared/external/alarmkit/SKILL.md",
272
- "sha256": "dad252df33be787eb9dc9cf0a1d4657b19f69b44c6b4d14010b0cf50117cf4b5"
272
+ "sha256": "ce285a6e95d03e3bc2f5c3150822e6ee2d07a4b629b81e8c0df81e6840161cb9"
273
273
  },
274
274
  {
275
275
  "path": "shared/external/android-architecture/SKILL.md",
@@ -325,15 +325,15 @@
325
325
  },
326
326
  {
327
327
  "path": "shared/external/app-store-optimization/SKILL.md",
328
- "sha256": "3a933c14158a77a8124f74b7f4c145ec202d7ccb74c9b65af436dc236159648d"
328
+ "sha256": "6015dd7b4c8661e23b677c53c391a3e39116fee2ad6b2b031b3d310eef4a3e2b"
329
329
  },
330
330
  {
331
331
  "path": "shared/external/app-store-review/SKILL.md",
332
- "sha256": "a2cd777753347aa66ea7d3094df4209a4a377a1214387283db7ba7247007e4a1"
332
+ "sha256": "0b77f648fd2d54e991f1c25540629f0a7d4fd2a1b30de7f8aaf07331b93963ea"
333
333
  },
334
334
  {
335
335
  "path": "shared/external/apple-on-device-ai/SKILL.md",
336
- "sha256": "7c89227595db96798f9a4225d75ab314c4eab3115ab54a4648501c820e0f2f87"
336
+ "sha256": "a86a4032c158cdf5523009e332618c0e460a70fe695be99fed8c8767a69afff8"
337
337
  },
338
338
  {
339
339
  "path": "shared/external/architecture/SKILL.md",
@@ -341,7 +341,7 @@
341
341
  },
342
342
  {
343
343
  "path": "shared/external/authentication/SKILL.md",
344
- "sha256": "5b8fe68bd207b8ac88874ea514dce7683f7e91de60748b4444df643540bf45c1"
344
+ "sha256": "4d3da49257432d30196b9970721f801524a7cdce4e6ddcaefc4fe31832a49157"
345
345
  },
346
346
  {
347
347
  "path": "shared/external/avkit/SKILL.md",
@@ -349,7 +349,7 @@
349
349
  },
350
350
  {
351
351
  "path": "shared/external/background-processing/SKILL.md",
352
- "sha256": "a356a35289da428d977c85f6984fe204b50298955489752891f98332b891f971"
352
+ "sha256": "876516478a6019297b886f754e2ec59e96bc73419cfff753dc7bdc7a4336b6e9"
353
353
  },
354
354
  {
355
355
  "path": "shared/external/backlog/SKILL.md",
@@ -357,7 +357,7 @@
357
357
  },
358
358
  {
359
359
  "path": "shared/external/callkit-voip/SKILL.md",
360
- "sha256": "0a7102e006a54e38011e24c274c50b0988f210cdd34fb6405ba5db922f23d3e7"
360
+ "sha256": "44646c2f192170a5468c18a0c78fb77ffcf135bb8d8cf2a7581ac2d1c0909eeb"
361
361
  },
362
362
  {
363
363
  "path": "shared/external/ci-cd-pipelines/SKILL.md",
@@ -369,7 +369,7 @@
369
369
  },
370
370
  {
371
371
  "path": "shared/external/cloudkit-sync/SKILL.md",
372
- "sha256": "842889bc86a8be353871c6f2104a8f5f8e506cd81203e135ee25911cac050526"
372
+ "sha256": "9464e93c0435628f3628a6f1775b7478dea75514b0fb3aa3cc09c7cec83be48d"
373
373
  },
374
374
  {
375
375
  "path": "shared/external/compose-components/SKILL.md",
@@ -393,11 +393,11 @@
393
393
  },
394
394
  {
395
395
  "path": "shared/external/core-bluetooth/SKILL.md",
396
- "sha256": "4911322b33fc791564b3ee6498a6df66626f1749e479f3a869ff7528a2597208"
396
+ "sha256": "3b7b41c7287cb86ee3c5bc477952c659fddbf050ceefbad95ab8a95ff15750be"
397
397
  },
398
398
  {
399
399
  "path": "shared/external/core-data/SKILL.md",
400
- "sha256": "c0204397e9dabadad75d4ccbb0c06b4f55c47c97f61c65072a33e4480832ecf0"
400
+ "sha256": "c232488bc3de3542b25a8b40ba7da648a1c16b8fd467ade21dbde28620ab2c26"
401
401
  },
402
402
  {
403
403
  "path": "shared/external/core-motion/SKILL.md",
@@ -405,11 +405,11 @@
405
405
  },
406
406
  {
407
407
  "path": "shared/external/core-nfc/SKILL.md",
408
- "sha256": "0f58b94d639b887588a472385791762181286ab30f015b423d868a1cef8fa5d7"
408
+ "sha256": "c838b7e38bb0c65f409674220ef9f6b668839ce2c0546609b75c69c1d7c18567"
409
409
  },
410
410
  {
411
411
  "path": "shared/external/coreml/SKILL.md",
412
- "sha256": "b16e3b2f288a9f98dabce1d70e759ca2af9e9773830719a64bee57c8b18f5eec"
412
+ "sha256": "b5f3fdf161a2bd2ab839a52093f0cb99c2c3026211e48a7d13dbab712e11b255"
413
413
  },
414
414
  {
415
415
  "path": "shared/external/council/SKILL.md",
@@ -417,7 +417,7 @@
417
417
  },
418
418
  {
419
419
  "path": "shared/external/cryptokit/SKILL.md",
420
- "sha256": "6c1999c0199c7937c063997383f64b353ac692715f579f585adc8d2075f414b2"
420
+ "sha256": "419f46a3058145f48e1f38e56741dc8074441132a1834812a6d9ad020e1c2c7e"
421
421
  },
422
422
  {
423
423
  "path": "shared/external/css-modern/SKILL.md",
@@ -437,7 +437,7 @@
437
437
  },
438
438
  {
439
439
  "path": "shared/external/device-integrity/SKILL.md",
440
- "sha256": "9ac0956d2afb1afa48cc26514aa3ade9aa6ec57d5eb765d42ada61147360dfe2"
440
+ "sha256": "9d8c39c65f7b5918c25c7ef3dd438fe62901e61ecc6fa2d7ecbd7c9a0fcfa4c0"
441
441
  },
442
442
  {
443
443
  "path": "shared/external/docker-expert/SKILL.md",
@@ -569,7 +569,7 @@
569
569
  },
570
570
  {
571
571
  "path": "shared/external/localization-reuse-map/SKILL.md",
572
- "sha256": "565fa80ffa729f49cb4a54fb8f1da98397de5aaf7fb308753d261b5dc9f51f46"
572
+ "sha256": "0a7429a2bbc2454b12ae4fb9af1eb09cc5a5d1ab96c05c693fc4827747bd8d15"
573
573
  },
574
574
  {
575
575
  "path": "shared/external/macos-menubar-tuist-app/SKILL.md",
@@ -581,7 +581,7 @@
581
581
  },
582
582
  {
583
583
  "path": "shared/external/mapkit-location/SKILL.md",
584
- "sha256": "fa78d74db0a13cdcf215a5f62c6b0ed33c5e1a6757b795a9968d131f0a9030ee"
584
+ "sha256": "d6be293b79f2e0e3312db441c7c315779b7f345cea3c264508bfbf76f49e79ef"
585
585
  },
586
586
  {
587
587
  "path": "shared/external/metrickit-diagnostics/SKILL.md",
@@ -613,7 +613,7 @@
613
613
  },
614
614
  {
615
615
  "path": "shared/external/passkit-wallet/SKILL.md",
616
- "sha256": "5780862b6f98b9a2b65975a79c690d2b200126f1004fac5055907b89f4bd8a0f"
616
+ "sha256": "972a26763ef557d6534e970c6a444526c0fc3eb0ac005f01ac1fee103e052e58"
617
617
  },
618
618
  {
619
619
  "path": "shared/external/pdfkit/SKILL.md",
@@ -621,11 +621,11 @@
621
621
  },
622
622
  {
623
623
  "path": "shared/external/pencilkit-drawing/SKILL.md",
624
- "sha256": "080a07ed7fb2f2065030a9d0c543023035b11a180cdc069026be195464afb12e"
624
+ "sha256": "f18ccc4d5d985492758003aba555ccbfbede2e9c18c70932dc7c1198c4c2f624"
625
625
  },
626
626
  {
627
627
  "path": "shared/external/permissionkit/SKILL.md",
628
- "sha256": "92167db142929bcfcb9458163d540bc4badc141dd5de5f7f98e7a5dc79b5b6db"
628
+ "sha256": "ea30c7a3bb9128a5f43064d5d2e46360d1fe08c732eb3ecedd63490d7d4420a5"
629
629
  },
630
630
  {
631
631
  "path": "shared/external/photos-camera-media/SKILL.md",
@@ -637,7 +637,7 @@
637
637
  },
638
638
  {
639
639
  "path": "shared/external/push-notifications/SKILL.md",
640
- "sha256": "f962752d6e540cdd8e98e96f9b0790eccede1d0a88bf5bd22928891d26d172f3"
640
+ "sha256": "9f73964265e99c21aa0bab61b153a77ad3ac953f45766c852b1c8cfbef8498c3"
641
641
  },
642
642
  {
643
643
  "path": "shared/external/python-patterns/SKILL.md",
@@ -649,7 +649,7 @@
649
649
  },
650
650
  {
651
651
  "path": "shared/external/realitykit-ar/SKILL.md",
652
- "sha256": "9c576081a515662c03d462fa0a15828f6e8ab02c8e14e1d4911fc0cfceef47fb"
652
+ "sha256": "66726df1164bb6ca96878cb941b4395ff3cff10ebc6743f91aa22b8ec795c246"
653
653
  },
654
654
  {
655
655
  "path": "shared/external/rest-api-design/SKILL.md",
@@ -721,7 +721,7 @@
721
721
  },
722
722
  {
723
723
  "path": "shared/external/swift-concurrency/SKILL.md",
724
- "sha256": "b231d006488648ec679cac3fb9f6676c32594db3425e32b1e217a7caff73aec0"
724
+ "sha256": "428a7e31167e98ef300d0c000afda438bfdd7853793571ab4d1d05623e6d4383"
725
725
  },
726
726
  {
727
727
  "path": "shared/external/swift-formatstyle/SKILL.md",
@@ -733,7 +733,7 @@
733
733
  },
734
734
  {
735
735
  "path": "shared/external/swift-security/SKILL.md",
736
- "sha256": "2dbc80cc138e70c0477f5340d372102e588a85970710add24d06dae19b5e58ff"
736
+ "sha256": "0358e6b23ec268c51bf34d9341a80150746d2cbe7f9e4159de5133442a1b0d2e"
737
737
  },
738
738
  {
739
739
  "path": "shared/external/swift-testing-pro/SKILL.md",
@@ -741,7 +741,7 @@
741
741
  },
742
742
  {
743
743
  "path": "shared/external/swift-testing/SKILL.md",
744
- "sha256": "71bcab71b1d26f04220561c347ded36920784cdaec85fb3e303405359c99e2de"
744
+ "sha256": "bed2911b41450d88b77cc913648ea421d99d7c167fb49f4c8bcf54b580952c29"
745
745
  },
746
746
  {
747
747
  "path": "shared/external/swiftdata-pro/SKILL.md",
@@ -749,7 +749,7 @@
749
749
  },
750
750
  {
751
751
  "path": "shared/external/swiftdata/SKILL.md",
752
- "sha256": "65518d8167b0ec9ed61608731aa9e7877124e21433cb64f230b9a8da792ceff8"
752
+ "sha256": "5fa27f7d9c0997acc5959ad811aa4b49ccbd34d62b5237556e1ffaa650447163"
753
753
  },
754
754
  {
755
755
  "path": "shared/external/swiftlint/SKILL.md",
@@ -777,7 +777,7 @@
777
777
  },
778
778
  {
779
779
  "path": "shared/external/swiftui-navigation/SKILL.md",
780
- "sha256": "a40a53957bd01be79ff2f692f639fbf618c29da9b98d5f1150618bafd458825d"
780
+ "sha256": "422a6c0b680d422af731474a9b03d141ef04e1f16359ce25a3ba22780f2bf1c2"
781
781
  },
782
782
  {
783
783
  "path": "shared/external/swiftui-patterns/SKILL.md",
@@ -829,7 +829,7 @@
829
829
  },
830
830
  {
831
831
  "path": "shared/external/vision-framework/SKILL.md",
832
- "sha256": "68ef66fc020e65885f47e26e665c88365bbbf52cb1fb8fb8de5c4cab7237c9e8"
832
+ "sha256": "370fdf6eaf71833dff0e8f2d3989a849b5c42ff3198605aa818c2fd0c12c7ef7"
833
833
  },
834
834
  {
835
835
  "path": "shared/external/vue-composition/SKILL.md",
@@ -837,7 +837,7 @@
837
837
  },
838
838
  {
839
839
  "path": "shared/external/weatherkit/SKILL.md",
840
- "sha256": "cafc8e7c7ec3ee6107be992086d5ba3280ef524b5177e5069b8525914b272811"
840
+ "sha256": "db8a11fc9c572c6d9754c031ad97a868c5d0e6748855d90b72a2aeb44e2d4255"
841
841
  },
842
842
  {
843
843
  "path": "shared/external/web-accessibility/SKILL.md",
@@ -853,7 +853,7 @@
853
853
  },
854
854
  {
855
855
  "path": "shared/external/widgetkit/SKILL.md",
856
- "sha256": "e00e47ad977c7775e6c83bcaee68546325cb1b7eb38d208d6fe01ca88090eb77"
856
+ "sha256": "d4bce1c362e57616b252304414cfd01354586c7ed0be6c22f61efcc0980e26b0"
857
857
  },
858
858
  {
859
859
  "path": "shared/external/xcode-build-benchmark/SKILL.md",