@mmerterden/multi-agent-pipeline 11.4.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +1 -0
  3. package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
  4. package/index.js +9 -2
  5. package/install/_common.mjs +107 -5
  6. package/install/_telemetry.mjs +5 -23
  7. package/install/claude.mjs +95 -11
  8. package/install/copilot.mjs +75 -6
  9. package/install/index.mjs +34 -1
  10. package/package.json +4 -10
  11. package/pipeline/commands/multi-agent/SKILL.md +1 -0
  12. package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -0
  13. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -0
  14. package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -0
  15. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -0
  16. package/pipeline/commands/multi-agent/channels/SKILL.md +1 -0
  17. package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -0
  18. package/pipeline/commands/multi-agent/dev/SKILL.md +1 -0
  19. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -0
  20. package/pipeline/commands/multi-agent/dev-local/SKILL.md +2 -1
  21. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -0
  22. package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -0
  23. package/pipeline/commands/multi-agent/finish/SKILL.md +1 -0
  24. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +29 -3
  25. package/pipeline/commands/multi-agent/help/SKILL.md +3 -2
  26. package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
  27. package/pipeline/commands/multi-agent/jira/SKILL.md +1 -0
  28. package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
  29. package/pipeline/commands/multi-agent/language/SKILL.md +20 -1
  30. package/pipeline/commands/multi-agent/local/SKILL.md +1 -0
  31. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -0
  32. package/pipeline/commands/multi-agent/log/SKILL.md +1 -0
  33. package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -0
  34. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
  35. package/pipeline/commands/multi-agent/purge/SKILL.md +27 -24
  36. package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -0
  37. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
  38. package/pipeline/commands/multi-agent/review/SKILL.md +1 -0
  39. package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
  40. package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -0
  41. package/pipeline/commands/multi-agent/scan/SKILL.md +1 -0
  42. package/pipeline/commands/multi-agent/search/SKILL.md +1 -0
  43. package/pipeline/commands/multi-agent/setup/SKILL.md +1 -0
  44. package/pipeline/commands/multi-agent/stack/SKILL.md +1 -0
  45. package/pipeline/commands/multi-agent/status/SKILL.md +1 -0
  46. package/pipeline/commands/multi-agent/sync/SKILL.md +20 -2
  47. package/pipeline/commands/multi-agent/test/SKILL.md +1 -0
  48. package/pipeline/commands/multi-agent/uninstall/SKILL.md +87 -0
  49. package/pipeline/commands/multi-agent/update/SKILL.md +1 -0
  50. package/pipeline/lib/account-resolver.sh +61 -23
  51. package/pipeline/lib/channels-multi-repo.sh +7 -2
  52. package/pipeline/lib/count-lib.sh +27 -0
  53. package/pipeline/lib/credential-store.sh +23 -6
  54. package/pipeline/lib/extract-conventions.sh +27 -21
  55. package/pipeline/lib/fetch-confluence.sh +25 -13
  56. package/pipeline/lib/fetch-crashlytics.sh +30 -8
  57. package/pipeline/lib/fetch-fortify.sh +11 -2
  58. package/pipeline/lib/fetch-swagger.sh +18 -7
  59. package/pipeline/lib/figma-mcp-refresh.sh +26 -11
  60. package/pipeline/lib/figma-screenshot.sh +11 -2
  61. package/pipeline/lib/issue-fetcher.sh +31 -6
  62. package/pipeline/lib/multi-repo-pipeline.sh +84 -20
  63. package/pipeline/lib/plan-todos.sh +12 -3
  64. package/pipeline/lib/post-pr-review.sh +5 -3
  65. package/pipeline/lib/repo-cache.sh +53 -9
  66. package/pipeline/lib/review-watch.sh +26 -6
  67. package/pipeline/lib/shadow-git.sh +45 -4
  68. package/pipeline/lib/vercel-deploy.sh +10 -1
  69. package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -5
  70. package/pipeline/multi-agent-refs/phases/phase-0-init.md +12 -1
  71. package/pipeline/scripts/audit-log-rotate.sh +38 -10
  72. package/pipeline/scripts/check-md-links.mjs +34 -9
  73. package/pipeline/scripts/diff-risk-score.mjs +4 -1
  74. package/pipeline/scripts/evidence-gate.mjs +10 -1
  75. package/pipeline/scripts/fixtures/install-layout.tsv +6 -4
  76. package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
  77. package/pipeline/scripts/gc-tmp.sh +67 -8
  78. package/pipeline/scripts/gc-worktrees.sh +140 -0
  79. package/pipeline/scripts/keychain-save.sh +13 -9
  80. package/pipeline/scripts/lint-skills.mjs +40 -2
  81. package/pipeline/scripts/localize-commands.mjs +130 -0
  82. package/pipeline/scripts/migrate-prefs.mjs +26 -7
  83. package/pipeline/scripts/phase-tracker.sh +71 -0
  84. package/pipeline/scripts/pre-commit-check.sh +39 -0
  85. package/pipeline/scripts/prune-logs.sh +100 -15
  86. package/pipeline/scripts/purge.sh +235 -0
  87. package/pipeline/scripts/scan-skills.sh +217 -127
  88. package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
  89. package/pipeline/scripts/smoke-description-tr.sh +82 -0
  90. package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
  91. package/pipeline/scripts/smoke-gc-tmp.sh +50 -4
  92. package/pipeline/scripts/smoke-gc-worktrees.sh +125 -0
  93. package/pipeline/scripts/smoke-install-layout.sh +11 -3
  94. package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
  95. package/pipeline/scripts/smoke-pack-contents.sh +140 -0
  96. package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
  97. package/pipeline/scripts/smoke-prune-logs.sh +57 -7
  98. package/pipeline/scripts/smoke-purge.sh +138 -0
  99. package/pipeline/scripts/smoke-shadow-git.sh +48 -1
  100. package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
  101. package/pipeline/scripts/smoke-update-check.sh +13 -0
  102. package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
  103. package/pipeline/scripts/test-gap-scan.mjs +12 -1
  104. package/pipeline/scripts/triage-memory.mjs +10 -1
  105. package/pipeline/scripts/uninstall.mjs +146 -42
  106. package/pipeline/scripts/update-check.sh +9 -0
  107. package/pipeline/scripts/update-issue-progress.sh +60 -41
  108. package/pipeline/scripts/write-state.mjs +14 -4
  109. package/pipeline/skills/.skill-manifest.json +7 -7
  110. package/pipeline/skills/.skills-index.json +9 -9
  111. package/pipeline/skills/shared/README.md +19 -10
  112. package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -0
  113. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
  114. package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +74 -0
  115. package/pipeline/skills/skills-index.md +1 -1
  116. package/pipeline/commands/multi-agent/delete/SKILL.md +0 -67
  117. package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +0 -66
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # smoke-fetchers-offline.sh - offline invocation tests for the pipeline/lib
4
+ # fetchers. No network, no real tokens.
5
+ #
6
+ # Asserts, per fetcher:
7
+ # 1. usage / arg-validation paths exit with the documented code
8
+ # 2. URL/host parsing succeeds for a representative URL. This is the
9
+ # permanent regression guard for the env-prefix bug where
10
+ # `VAR=x PARSED=$(python3 ...)` never exported VAR to the child, so
11
+ # fetch-confluence always failed with "could not determine Confluence
12
+ # host" before reaching the credential stage.
13
+ # 3. with stub credentials (fake credential-store in a temp HOME, fake curl
14
+ # on PATH) the failure moves past parsing to the documented
15
+ # credential/network exit code
16
+ # 4. static source checks: no bash-4-only constructs (macOS /bin/bash is
17
+ # 3.2) and no secret-bearing argv patterns (curl -H auth headers,
18
+ # --data-urlencode secrets, security add-generic-password -w on argv)
19
+ #
20
+ # Fetchers run under /bin/bash when available so bash-4-isms fail loudly on
21
+ # macOS instead of passing under a newer Homebrew bash.
22
+ #
23
+ # Exit codes: 0=pass, 1=failure
24
+
25
+ set -euo pipefail
26
+
27
+ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
28
+ LIB="$REPO_ROOT/pipeline/lib"
29
+ SCRIPTS="$REPO_ROOT/pipeline/scripts"
30
+
31
+ # Prefer the system bash (3.2 on macOS) so bash-4-isms regress loudly here.
32
+ SH="/bin/bash"
33
+ [ -x "$SH" ] || SH="$(command -v bash)"
34
+
35
+ PASS=0
36
+ FAIL=0
37
+ note() { echo " -> $*"; }
38
+ pass() { echo " ok $*"; PASS=$((PASS + 1)); }
39
+ fail() { echo " FAIL $*"; FAIL=$((FAIL + 1)); }
40
+
41
+ WORK=$(mktemp -d "${TMPDIR:-/tmp}/smoke-fetchers.XXXXXX")
42
+ trap 'rm -rf "$WORK"' EXIT
43
+
44
+ OUT="$WORK/out.log"
45
+ ERR="$WORK/err.log"
46
+
47
+ # expect_rc <expected-rc> <label> <cmd...>
48
+ # Runs cmd, captures stdout/stderr to $OUT/$ERR, asserts the exit code.
49
+ expect_rc() {
50
+ local expected="$1" label="$2"
51
+ shift 2
52
+ local rc=0
53
+ "$@" >"$OUT" 2>"$ERR" || rc=$?
54
+ if [ "$rc" -eq "$expected" ]; then
55
+ pass "$label (rc=$rc)"
56
+ else
57
+ fail "$label (rc=$rc, expected $expected)"
58
+ tail -5 "$ERR" | sed 's/^/ /'
59
+ fi
60
+ }
61
+
62
+ assert_in_file() {
63
+ local needle="$1" file="$2" label="$3"
64
+ if grep -q -e "$needle" "$file" 2>/dev/null; then
65
+ pass "$label"
66
+ else
67
+ fail "$label (pattern '$needle' not found)"
68
+ fi
69
+ }
70
+
71
+ assert_not_in_file() {
72
+ local needle="$1" file="$2" label="$3"
73
+ if grep -q -e "$needle" "$file" 2>/dev/null; then
74
+ fail "$label (pattern '$needle' present)"
75
+ else
76
+ pass "$label"
77
+ fi
78
+ }
79
+
80
+ # -----------------------------------------------------------------------------
81
+ note "0. fixtures: temp HOME with resolver + fake credential store, fake curls"
82
+ # -----------------------------------------------------------------------------
83
+ FAKE_HOME="$WORK/home"
84
+ mkdir -p "$FAKE_HOME/.claude/lib"
85
+ cp "$LIB/credential-store-resolver.sh" "$FAKE_HOME/.claude/lib/credential-store-resolver.sh"
86
+
87
+ # Fake credential store: empty by default, returns FAKE_CRED_VALUE when set.
88
+ cat > "$FAKE_HOME/.claude/lib/credential-store.sh" <<'FAKE'
89
+ #!/bin/bash
90
+ case "${1:-}" in
91
+ get)
92
+ if [ -n "${FAKE_CRED_VALUE:-}" ]; then printf '%s' "$FAKE_CRED_VALUE"; exit 0; fi
93
+ exit 1 ;;
94
+ platform) echo "fake" ;;
95
+ *) exit 3 ;;
96
+ esac
97
+ FAKE
98
+ chmod +x "$FAKE_HOME/.claude/lib/credential-store.sh"
99
+
100
+ # Fake curl variants, each in its own PATH-prepend dir.
101
+ BIN_CURL_FAIL="$WORK/bin-curl-fail"
102
+ mkdir -p "$BIN_CURL_FAIL"
103
+ cat > "$BIN_CURL_FAIL/curl" <<'FAKE'
104
+ #!/bin/bash
105
+ echo "curl: (7) offline stub - connection refused" >&2
106
+ exit 7
107
+ FAKE
108
+ chmod +x "$BIN_CURL_FAIL/curl"
109
+
110
+ # Swagger meta stub: honours --output, emits the --write-out triple with a
111
+ # content type that matches neither json nor yaml, forcing the URL-suffix
112
+ # lowercase path (the former bash-4 ${URL,,} site).
113
+ BIN_CURL_META="$WORK/bin-curl-meta"
114
+ mkdir -p "$BIN_CURL_META"
115
+ cat > "$BIN_CURL_META/curl" <<'FAKE'
116
+ #!/bin/bash
117
+ out=""
118
+ prev=""
119
+ for a in "$@"; do
120
+ if [ "$prev" = "--output" ]; then out="$a"; fi
121
+ prev="$a"
122
+ done
123
+ if [ -n "$out" ]; then printf '{}' > "$out"; fi
124
+ printf '200\napplication/octet-stream\n2\n'
125
+ exit 0
126
+ FAKE
127
+ chmod +x "$BIN_CURL_META/curl"
128
+
129
+ # 401 stub: honours -o, prints the HTTP code the way curl -w '%{http_code}' does.
130
+ BIN_CURL_401="$WORK/bin-curl-401"
131
+ mkdir -p "$BIN_CURL_401"
132
+ cat > "$BIN_CURL_401/curl" <<'FAKE'
133
+ #!/bin/bash
134
+ out=""
135
+ prev=""
136
+ for a in "$@"; do
137
+ if [ "$prev" = "-o" ] || [ "$prev" = "--output" ]; then out="$a"; fi
138
+ prev="$a"
139
+ done
140
+ if [ -n "$out" ]; then printf '{}' > "$out"; fi
141
+ printf '401'
142
+ exit 0
143
+ FAKE
144
+ chmod +x "$BIN_CURL_401/curl"
145
+
146
+ # Fake security + uname (forces the macOS branch of credential-store.sh) so
147
+ # the no-secret-on-argv property of `security -i` writes can be asserted on
148
+ # any platform without touching a real keychain.
149
+ BIN_DARWIN="$WORK/bin-darwin"
150
+ mkdir -p "$BIN_DARWIN"
151
+ cat > "$BIN_DARWIN/uname" <<'FAKE'
152
+ #!/bin/bash
153
+ echo "Darwin"
154
+ FAKE
155
+ cat > "$BIN_DARWIN/security" <<'FAKE'
156
+ #!/bin/bash
157
+ printf '%s\n' "$*" >> "$SMOKE_CAPTURE_DIR/security-argv.log"
158
+ if [ "${1:-}" = "-i" ]; then
159
+ cat >> "$SMOKE_CAPTURE_DIR/security-stdin.log"
160
+ fi
161
+ exit 0
162
+ FAKE
163
+ chmod +x "$BIN_DARWIN/uname" "$BIN_DARWIN/security"
164
+
165
+ # -----------------------------------------------------------------------------
166
+ note "1. fetch-confluence.sh"
167
+ # -----------------------------------------------------------------------------
168
+ expect_rc 4 "no args exits 4 (usage)" \
169
+ "$SH" "$LIB/fetch-confluence.sh"
170
+ expect_rc 4 "--help exits 4 (usage)" \
171
+ "$SH" "$LIB/fetch-confluence.sh" --help
172
+
173
+ # Regression guard for the env-prefix parse bug: a plain page URL must get
174
+ # past host/pageId resolution and fail at the credential stage (exit 2),
175
+ # never with the parse error.
176
+ expect_rc 2 "URL parse reaches credential stage (viewpage.action?pageId)" \
177
+ env HOME="$FAKE_HOME" USER=smoke \
178
+ "$SH" "$LIB/fetch-confluence.sh" \
179
+ "https://confluence.example.com/pages/viewpage.action?pageId=12345"
180
+ assert_in_file "missing-token" "$ERR" "blocked JSON names missing-token"
181
+ assert_not_in_file "could not determine Confluence host" "$ERR" \
182
+ "no host-parse error (env-prefix regression)"
183
+
184
+ expect_rc 2 "URL parse reaches credential stage (cloud /wiki/spaces form)" \
185
+ env HOME="$FAKE_HOME" USER=smoke \
186
+ "$SH" "$LIB/fetch-confluence.sh" \
187
+ "https://tenant.atlassian.net/wiki/spaces/ENG/pages/99887/Some+Page"
188
+ assert_in_file "missing-token" "$ERR" "cloud form also blocked on token only"
189
+
190
+ # With a stub token and a failing curl, the failure must be the documented
191
+ # network error (3), proving parsing and auth wiring are past.
192
+ expect_rc 3 "stub token + offline curl exits 3 (network stage)" \
193
+ env HOME="$FAKE_HOME" USER=smoke FAKE_CRED_VALUE=fake-token \
194
+ PATH="$BIN_CURL_FAIL:$PATH" \
195
+ "$SH" "$LIB/fetch-confluence.sh" \
196
+ "https://confluence.example.com/pages/viewpage.action?pageId=12345"
197
+ assert_in_file "Confluence GET failed" "$ERR" "network-stage error message"
198
+
199
+ # -----------------------------------------------------------------------------
200
+ note "2. fetch-crashlytics.sh"
201
+ # -----------------------------------------------------------------------------
202
+ expect_rc 4 "no args exits 4 (usage)" \
203
+ "$SH" "$LIB/fetch-crashlytics.sh"
204
+ expect_rc 4 "--help exits 4 (usage)" \
205
+ "$SH" "$LIB/fetch-crashlytics.sh" --help
206
+
207
+ CRASH_URL="https://console.firebase.google.com/project/my-proj/crashlytics/app/ios:com.example.app/issues/abc123"
208
+ expect_rc 2 "URL parse reaches credential stage" \
209
+ env HOME="$FAKE_HOME" USER=smoke \
210
+ "$SH" "$LIB/fetch-crashlytics.sh" "$CRASH_URL"
211
+ assert_in_file "missing-token" "$ERR" "blocked JSON names missing-token"
212
+
213
+ # Stub SA JSON whose project_id mismatches the URL: proves parse + credential
214
+ # read + base64 decode + JSON validation all pass, fully offline (exit 5).
215
+ MISMATCH_SA_B64=$(printf '{"project_id":"other-proj"}' | base64 | tr -d '\n')
216
+ expect_rc 5 "stub SA JSON reaches project-mismatch check (exit 5)" \
217
+ env HOME="$FAKE_HOME" USER=smoke FAKE_CRED_VALUE="$MISMATCH_SA_B64" \
218
+ "$SH" "$LIB/fetch-crashlytics.sh" "$CRASH_URL"
219
+ assert_in_file "project_id" "$ERR" "mismatch error mentions project_id"
220
+
221
+ # The decoded SA JSON must never touch a predictable /tmp path.
222
+ assert_not_in_file "/tmp/.fc-sa" "$LIB/fetch-crashlytics.sh" \
223
+ "predictable /tmp SA path removed from source"
224
+
225
+ # -----------------------------------------------------------------------------
226
+ note "3. fetch-swagger.sh"
227
+ # -----------------------------------------------------------------------------
228
+ expect_rc 4 "no args exits 4 (usage)" \
229
+ "$SH" "$LIB/fetch-swagger.sh"
230
+
231
+ expect_rc 2 "offline curl exits 2 (network stage)" \
232
+ env PATH="$BIN_CURL_FAIL:$PATH" \
233
+ "$SH" "$LIB/fetch-swagger.sh" "https://spec.example.com/openapi.json"
234
+
235
+ # Uppercase .YAML suffix with a non-matching content type exercises the
236
+ # lowercase-URL branch (formerly bash-4 ${URL,,}, dies on /bin/bash 3.2).
237
+ expect_rc 0 "uppercase .YAML suffix detected via lowercase branch" \
238
+ env PATH="$BIN_CURL_META:$PATH" \
239
+ "$SH" "$LIB/fetch-swagger.sh" "https://spec.example.com/openapi.YAML"
240
+ assert_in_file '"format": "yaml"' "$OUT" "output reports yaml format"
241
+
242
+ # -----------------------------------------------------------------------------
243
+ note "4. fetch-fortify.sh"
244
+ # -----------------------------------------------------------------------------
245
+ expect_rc 6 "no host configured exits 6" \
246
+ env HOME="$FAKE_HOME" USER=smoke \
247
+ "$SH" "$LIB/fetch-fortify.sh" --version-id 123
248
+ expect_rc 4 "host override + no version id exits 4 (usage)" \
249
+ env HOME="$FAKE_HOME" USER=smoke FORTIFY_HOST_OVERRIDE=ssc.example.com \
250
+ "$SH" "$LIB/fetch-fortify.sh"
251
+
252
+ FORTIFY_URL="https://ssc.example.com/ssc/html/ssc/version/123/fix/ABCD-1"
253
+ expect_rc 2 "URL parse reaches credential stage" \
254
+ env HOME="$FAKE_HOME" USER=smoke FORTIFY_HOST_OVERRIDE=ssc.example.com \
255
+ "$SH" "$LIB/fetch-fortify.sh" "$FORTIFY_URL"
256
+ assert_in_file "missing-token" "$ERR" "blocked JSON names missing-token"
257
+
258
+ expect_rc 0 "stub token + offline curl degrades to empty result (exit 0)" \
259
+ env HOME="$FAKE_HOME" USER=smoke FORTIFY_HOST_OVERRIDE=ssc.example.com \
260
+ FAKE_CRED_VALUE=fake-token PATH="$BIN_CURL_FAIL:$PATH" \
261
+ "$SH" "$LIB/fetch-fortify.sh" "$FORTIFY_URL"
262
+ assert_in_file '"gateOutcome"' "$OUT" "normalized JSON still emitted"
263
+
264
+ # -----------------------------------------------------------------------------
265
+ note "5. figma-screenshot.sh"
266
+ # -----------------------------------------------------------------------------
267
+ expect_rc 1 "no args exits 1 (usage)" \
268
+ "$SH" "$LIB/figma-screenshot.sh"
269
+ expect_rc 0 "--help exits 0" \
270
+ "$SH" "$LIB/figma-screenshot.sh" --help
271
+
272
+ expect_rc 3 "no PAT resolvable exits 3 (Tier 3 required)" \
273
+ env -u FIGMA_PAT HOME="$FAKE_HOME" USER=smoke \
274
+ "$SH" "$LIB/figma-screenshot.sh" --file-key abc123 --node-id 1-2 \
275
+ --output-dir "$WORK/shots-no-pat"
276
+
277
+ expect_rc 2 "stub PAT + 401 curl exits 2 (auth stage, past parsing)" \
278
+ env HOME="$FAKE_HOME" USER=smoke FIGMA_PAT=fake-pat \
279
+ PATH="$BIN_CURL_401:$PATH" \
280
+ "$SH" "$LIB/figma-screenshot.sh" --file-key abc123 --node-id 1-2 \
281
+ --output-dir "$WORK/shots-401"
282
+
283
+ # -----------------------------------------------------------------------------
284
+ note "6. figma-mcp-refresh.sh"
285
+ # -----------------------------------------------------------------------------
286
+ expect_rc 2 "missing prefs exits 2" \
287
+ env HOME="$FAKE_HOME" USER=smoke \
288
+ "$SH" "$LIB/figma-mcp-refresh.sh"
289
+
290
+ if command -v jq >/dev/null 2>&1; then
291
+ PREFS_HOME="$WORK/home-prefs"
292
+ mkdir -p "$PREFS_HOME/.claude"
293
+ printf '{"global":{"keychainMapping":{"figma_mcp":"Smoke_Figma_MCP"}}}\n' \
294
+ > "$PREFS_HOME/.claude/multi-agent-preferences.json"
295
+ export SMOKE_CAPTURE_DIR="$WORK"
296
+ expect_rc 2 "mapped key + empty keychain exits 2 (no refresh token)" \
297
+ env HOME="$PREFS_HOME" USER=smoke SMOKE_CAPTURE_DIR="$WORK" \
298
+ PATH="$BIN_DARWIN:$PATH" \
299
+ "$SH" "$LIB/figma-mcp-refresh.sh"
300
+ else
301
+ note " (jq not installed - skipping mapped-key case)"
302
+ fi
303
+
304
+ # -----------------------------------------------------------------------------
305
+ note "7. credential-store.sh + keychain-save.sh"
306
+ # -----------------------------------------------------------------------------
307
+ expect_rc 0 "platform subcommand works" \
308
+ "$SH" "$LIB/credential-store.sh" platform
309
+ expect_rc 3 "get without key exits 3 (usage)" \
310
+ "$SH" "$LIB/credential-store.sh" get
311
+ expect_rc 1 "keychain-save without service name exits 1 (usage)" \
312
+ "$SH" "$SCRIPTS/keychain-save.sh"
313
+
314
+ # set <key> - : the secret must travel via stdin to `security -i`, never on
315
+ # the security argv. Fake uname forces the macOS branch; fake security logs
316
+ # argv and stdin separately.
317
+ rm -f "$WORK/security-argv.log" "$WORK/security-stdin.log"
318
+ SECRET_VALUE="topsecret-smoke-value-123"
319
+ rc=0
320
+ printf '%s' "$SECRET_VALUE" \
321
+ | env PATH="$BIN_DARWIN:$PATH" KEYCHAIN_DELEGATE=0 SMOKE_CAPTURE_DIR="$WORK" \
322
+ "$SH" "$LIB/credential-store.sh" set smoke-test-key - >"$OUT" 2>"$ERR" || rc=$?
323
+ if [ "$rc" -eq 0 ]; then
324
+ pass "set <key> - via stdin succeeds (rc=0)"
325
+ else
326
+ fail "set <key> - via stdin failed (rc=$rc)"
327
+ tail -5 "$ERR" | sed 's/^/ /'
328
+ fi
329
+ assert_in_file "^-i" "$WORK/security-argv.log" "security invoked in -i stdin mode"
330
+ assert_not_in_file "$SECRET_VALUE" "$WORK/security-argv.log" "secret absent from security argv"
331
+ assert_in_file "$SECRET_VALUE" "$WORK/security-stdin.log" "secret delivered via stdin"
332
+
333
+ # -----------------------------------------------------------------------------
334
+ note "8. static source checks (bash-3.2 compat + no secrets on argv)"
335
+ # -----------------------------------------------------------------------------
336
+ OWNED_FILES=(
337
+ "$LIB/fetch-confluence.sh"
338
+ "$LIB/fetch-crashlytics.sh"
339
+ "$LIB/fetch-swagger.sh"
340
+ "$LIB/fetch-fortify.sh"
341
+ "$LIB/figma-mcp-refresh.sh"
342
+ "$LIB/figma-screenshot.sh"
343
+ "$LIB/credential-store.sh"
344
+ "$SCRIPTS/keychain-save.sh"
345
+ )
346
+
347
+ if grep -nE '\$\{[A-Za-z_0-9]+(,,|\^\^)|mapfile |readarray |declare -A' "${OWNED_FILES[@]}"; then
348
+ fail "bash-4-only construct found (must run on /bin/bash 3.2)"
349
+ else
350
+ pass "no bash-4-only constructs"
351
+ fi
352
+
353
+ if grep -nE -- '-H "(Authorization|X-Figma-Token)' "${OWNED_FILES[@]}"; then
354
+ fail "auth header passed to curl via argv (-H)"
355
+ else
356
+ pass "no auth headers on curl argv"
357
+ fi
358
+
359
+ if grep -nE -- '--data-urlencode "(refresh_token|client_secret)' "${OWNED_FILES[@]}"; then
360
+ fail "OAuth secret passed to curl via argv (--data-urlencode)"
361
+ else
362
+ pass "no OAuth secrets on curl argv"
363
+ fi
364
+
365
+ if grep -nE 'add-generic-password[^|]*-w "\$' "${OWNED_FILES[@]}"; then
366
+ fail "keychain secret passed to security via argv (-w)"
367
+ else
368
+ pass "no keychain secrets on security argv"
369
+ fi
370
+
371
+ # shellcheck disable=SC2016 # literal $HOME is the pattern being hunted
372
+ if grep -nE '\$HOME/\.claude/lib/credential-store\.sh|~/\.claude/lib/credential-store\.sh' "${OWNED_FILES[@]}"; then
373
+ fail "hardcoded credential-store path (must go through the resolver)"
374
+ else
375
+ pass "credential-store access routed through the resolver"
376
+ fi
377
+
378
+ # -----------------------------------------------------------------------------
379
+ echo ""
380
+ echo "smoke-fetchers-offline: $PASS passed, $FAIL failed"
381
+ [ "$FAIL" -eq 0 ] || exit 1
382
+ exit 0
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # smoke-gc-tmp.sh - gc-tmp.sh sweeps only known scratch prefixes, is dry-run
3
- # by default, and never touches unrelated files.
3
+ # by default, never touches unrelated files, refuses unsafe scratch roots,
4
+ # never deletes the root itself, and spares fresh scratch by default.
4
5
  #
5
6
  # Exit 0 = all pass, 1 = any failure.
6
7
 
@@ -40,7 +41,7 @@ seed() {
40
41
  echo "→ 1. dry-run lists matches and deletes nothing"
41
42
  seed
42
43
  before=$(find "$SB" -mindepth 1 | wc -l | tr -d ' ')
43
- out=$(GC_TMP_ROOT="$SB" bash "$GC" 2>&1)
44
+ out=$(GC_TMP_ROOT="$SB" GC_TMP_GRACE_MIN=0 bash "$GC" 2>&1)
44
45
  after=$(find "$SB" -mindepth 1 | wc -l | tr -d ' ')
45
46
  echo "$out" | grep -q "DRY-RUN" && pass "dry-run banner shown" || fail "dry-run banner missing"
46
47
  [ "$before" = "$after" ] && pass "dry-run deleted nothing ($after entries intact)" || fail "dry-run changed the tree ($before -> $after)"
@@ -49,7 +50,7 @@ echo "$out" | grep -q "unrelated-file.txt" && fail "dry-run matched an unrelated
49
50
  echo ""
50
51
  echo "→ 2. --yes removes only matched scratch"
51
52
  seed
52
- GC_TMP_ROOT="$SB" bash "$GC" --yes >/dev/null 2>&1
53
+ GC_TMP_ROOT="$SB" GC_TMP_GRACE_MIN=0 bash "$GC" --yes >/dev/null 2>&1
53
54
  [ -f "$SB/unrelated-file.txt" ] && pass "unrelated file preserved" || fail "unrelated file was deleted"
54
55
  [ ! -e "$SB/multi-agent-picker-PROJ-1.json" ] && pass "multi-agent-* file removed" || fail "multi-agent-* file survived"
55
56
  [ ! -e "$SB/multi-agent-review-PROJ-1-x" ] && pass "scratch dir removed" || fail "scratch dir survived"
@@ -61,7 +62,7 @@ remaining=$(find "$SB" -mindepth 1 | wc -l | tr -d ' ')
61
62
  echo ""
62
63
  echo "→ 3. --older-than excludes fresh scratch"
63
64
  seed
64
- out=$(GC_TMP_ROOT="$SB" bash "$GC" --older-than=1 2>&1)
65
+ out=$(GC_TMP_ROOT="$SB" GC_TMP_GRACE_MIN=0 bash "$GC" --older-than=1 2>&1)
65
66
  echo "$out" | grep -q "nothing to do" && pass "freshly-created scratch excluded by --older-than=1" || fail "--older-than did not exclude fresh files"
66
67
 
67
68
  echo ""
@@ -79,6 +80,51 @@ rc=$?
79
80
  rm -rf "$EMPTY"
80
81
  { [ "$rc" = "0" ] && echo "$out" | grep -q "nothing to do"; } && pass "empty root exits 0 with no-op message" || fail "empty root not a clean no-op"
81
82
 
83
+ echo ""
84
+ echo "→ 6. unsafe scratch roots are refused (exit 2)"
85
+ GC_TMP_ROOT="$HOME" bash "$GC" >/dev/null 2>&1
86
+ [ "$?" = "2" ] && pass "\$HOME as scratch root refused" || fail "\$HOME as scratch root not refused"
87
+ GC_TMP_ROOT="/" bash "$GC" >/dev/null 2>&1
88
+ [ "$?" = "2" ] && pass "/ as scratch root refused" || fail "/ as scratch root not refused"
89
+ GITROOT="$(mktemp -d)"
90
+ mkdir -p "$GITROOT/.git"
91
+ touch "$GITROOT/multi-agent-bait.json"
92
+ GC_TMP_ROOT="$GITROOT" bash "$GC" --yes >/dev/null 2>&1
93
+ rc=$?
94
+ [ "$rc" = "2" ] && pass "git work tree as scratch root refused" || fail "git work tree as scratch root not refused"
95
+ [ -f "$GITROOT/multi-agent-bait.json" ] && pass "bait file inside refused root untouched" || fail "refused root was still swept"
96
+ rm -rf "$GITROOT"
97
+
98
+ echo ""
99
+ echo "→ 7. a root itself named like scratch is never deleted"
100
+ SR="$(mktemp -d)"
101
+ mkdir -p "$SR/multi-agent-root"
102
+ touch "$SR/multi-agent-root/multi-agent-inner.json"
103
+ GC_TMP_ROOT="$SR/multi-agent-root" GC_TMP_GRACE_MIN=0 bash "$GC" --yes >/dev/null 2>&1
104
+ [ -d "$SR/multi-agent-root" ] && pass "matching-named root survives --yes" || fail "root dir itself was deleted"
105
+ [ ! -e "$SR/multi-agent-root/multi-agent-inner.json" ] && pass "inner scratch still removed" || fail "inner scratch survived"
106
+ rm -rf "$SR"
107
+
108
+ echo ""
109
+ echo "→ 8. symlinked scratch is unlinked without following it"
110
+ seed
111
+ VICTIM="$(mktemp -d)"
112
+ echo keep >"$VICTIM/keep.txt"
113
+ ln -s "$VICTIM" "$SB/multi-agent-linked"
114
+ GC_TMP_ROOT="$SB" GC_TMP_GRACE_MIN=0 bash "$GC" --yes >/dev/null 2>&1
115
+ [ ! -e "$SB/multi-agent-linked" ] && pass "symlink itself removed" || fail "symlink survived"
116
+ [ -f "$VICTIM/keep.txt" ] && pass "symlink target untouched" || fail "symlink target was deleted"
117
+ rm -rf "$VICTIM"
118
+
119
+ echo ""
120
+ echo "→ 9. default grace spares fresh scratch"
121
+ seed
122
+ out=$(GC_TMP_ROOT="$SB" bash "$GC" --yes 2>&1)
123
+ rc=$?
124
+ [ "$rc" = "0" ] && pass "grace run exits 0" || fail "grace run exited $rc"
125
+ [ -f "$SB/multi-agent-picker-PROJ-1.json" ] && pass "fresh scratch spared by default grace" || fail "default grace deleted fresh scratch"
126
+ echo "$out" | grep -qi "spared" && pass "output mentions spared fresh items" || fail "no spared note in output"
127
+
82
128
  echo ""
83
129
  echo "══ gc-tmp smoke: $PASS passed, $FAIL failed ══"
84
130
  [ "$FAIL" -eq 0 ]
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-gc-worktrees.sh - contract of the worktree residue sweeper.
3
+ #
4
+ # Verifies gc-worktrees.sh: dry-run-by-default, orphan-dir removal only with
5
+ # --yes, registered worktrees untouched, gitlink residue unstaged, exclude
6
+ # guard added idempotently, --older-than sparing, non-repo no-op, and flag
7
+ # validation.
8
+
9
+ set -uo pipefail
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ SCRIPT="$SCRIPT_DIR/gc-worktrees.sh"
13
+
14
+ PASS=0
15
+ FAIL=0
16
+ record_pass() { PASS=$((PASS + 1)); printf ' \342\234\223 %s\n' "$1"; }
17
+ record_fail() { FAIL=$((FAIL + 1)); printf ' \342\234\227 %s\n' "$1"; }
18
+
19
+ printf '\342\206\222 smoke-gc-worktrees: worktree residue sweep contract\n'
20
+
21
+ tmpdir=$(mktemp -d)
22
+ trap 'rm -rf "$tmpdir"' EXIT
23
+
24
+ # Fixture repo: one registered worktree, one orphan dir, one gitlink residue.
25
+ REPO="$tmpdir/repo"
26
+ mkdir -p "$REPO"
27
+ git -C "$REPO" init -q -b main
28
+ git -C "$REPO" config user.email smoke@test
29
+ git -C "$REPO" config user.name smoke
30
+ echo hi > "$REPO/f.txt"
31
+ git -C "$REPO" add f.txt
32
+ git -C "$REPO" commit -qm init
33
+ git -C "$REPO" worktree add -q "$REPO/.worktrees/REG" -b reg-branch
34
+ mkdir -p "$REPO/.worktrees/ORPHAN"
35
+ echo x > "$REPO/.worktrees/ORPHAN/file"
36
+ git -C "$REPO" update-index --add --cacheinfo "160000,$(git -C "$REPO" rev-parse HEAD),.worktrees/GONE"
37
+
38
+ # 1. Script parses
39
+ if bash -n "$SCRIPT" 2>/dev/null; then
40
+ record_pass "gc-worktrees.sh parses (bash -n)"
41
+ else
42
+ record_fail "gc-worktrees.sh has syntax errors"
43
+ fi
44
+
45
+ # 2. Dry-run lists all three findings and deletes nothing
46
+ out=$(bash "$SCRIPT" --repo "$REPO" 2>&1); rc=$?
47
+ if [ "$rc" -eq 0 ] \
48
+ && printf '%s' "$out" | grep -q "would remove orphan worktree dir: .worktrees/ORPHAN" \
49
+ && printf '%s' "$out" | grep -q "would unstage gitlink residue from the index: .worktrees/GONE" \
50
+ && printf '%s' "$out" | grep -q "would add .worktrees/ to .git/info/exclude"; then
51
+ record_pass "dry-run lists orphan dir + gitlink + missing guard"
52
+ else
53
+ record_fail "dry-run should list all three findings (rc=$rc): $out"
54
+ fi
55
+ if [ -d "$REPO/.worktrees/ORPHAN" ] && git -C "$REPO" ls-files -s -- .worktrees | grep -q 160000; then
56
+ record_pass "dry-run changed nothing"
57
+ else
58
+ record_fail "dry-run must not delete or unstage anything"
59
+ fi
60
+
61
+ # 3. --older-than spares the freshly-touched orphan
62
+ out=$(bash "$SCRIPT" --repo "$REPO" --older-than=60 2>&1)
63
+ if printf '%s' "$out" | grep -q "would remove orphan"; then
64
+ record_fail "--older-than=60 should spare the fresh orphan dir"
65
+ else
66
+ record_pass "--older-than spares recently-touched orphan dirs"
67
+ fi
68
+
69
+ # 4. --yes applies all three
70
+ out=$(bash "$SCRIPT" --repo "$REPO" --yes 2>&1); rc=$?
71
+ if [ "$rc" -eq 0 ] && [ ! -d "$REPO/.worktrees/ORPHAN" ]; then
72
+ record_pass "--yes removes the orphan dir"
73
+ else
74
+ record_fail "--yes should remove the orphan dir (rc=$rc): $out"
75
+ fi
76
+ if git -C "$REPO" ls-files -s -- .worktrees | grep -q 160000; then
77
+ record_fail "--yes should unstage the gitlink residue"
78
+ else
79
+ record_pass "--yes unstages the gitlink residue"
80
+ fi
81
+ if [ "$(grep -cxF '.worktrees/' "$REPO/.git/info/exclude" 2>/dev/null)" = "1" ]; then
82
+ record_pass "--yes adds the exclude guard exactly once"
83
+ else
84
+ record_fail "exclude guard missing or duplicated"
85
+ fi
86
+
87
+ # 5. Registered worktree untouched
88
+ if [ -d "$REPO/.worktrees/REG" ] && git -C "$REPO" worktree list | grep -q "REG"; then
89
+ record_pass "registered worktree left untouched"
90
+ else
91
+ record_fail "registered worktree must never be touched"
92
+ fi
93
+
94
+ # 6. Re-run -> nothing to do, guard not duplicated
95
+ out=$(bash "$SCRIPT" --repo "$REPO" 2>&1); rc=$?
96
+ if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -q "nothing to do"; then
97
+ record_pass "clean repo -> nothing to do (exit 0)"
98
+ else
99
+ record_fail "clean repo should be a no-op (rc=$rc): $out"
100
+ fi
101
+ bash "$SCRIPT" --repo "$REPO" --yes >/dev/null 2>&1
102
+ if [ "$(grep -cxF '.worktrees/' "$REPO/.git/info/exclude")" = "1" ]; then
103
+ record_pass "exclude guard is idempotent across --yes reruns"
104
+ else
105
+ record_fail "exclude guard duplicated on rerun"
106
+ fi
107
+
108
+ # 7. Non-repo -> silent no-op exit 0
109
+ out=$(bash "$SCRIPT" --repo "$tmpdir" 2>&1); rc=$?
110
+ if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -q "not a git work tree"; then
111
+ record_pass "non-repo -> exit 0 no-op"
112
+ else
113
+ record_fail "non-repo should exit 0 with a no-op message (rc=$rc)"
114
+ fi
115
+
116
+ # 8. Unknown flag -> usage error exit 2
117
+ bash "$SCRIPT" --definitely-not-a-flag >/dev/null 2>&1; rc=$?
118
+ if [ "$rc" -eq 2 ]; then
119
+ record_pass "unknown flag -> exit 2"
120
+ else
121
+ record_fail "unknown flag should exit 2 (got $rc)"
122
+ fi
123
+
124
+ printf '\n\342\225\220\342\225\220 smoke-gc-worktrees: %d passed, %d failed \342\225\220\342\225\220\n' "$PASS" "$FAIL"
125
+ [ "$FAIL" -eq 0 ]
@@ -110,11 +110,19 @@ assert_file "$TMP_CLAUDE/.claude/CLAUDE.md" "~/.claude/CLAUDE.md"
110
110
  assert_file "$TMP_CLAUDE/.claude/multi-agent-preferences.json" "~/.claude/multi-agent-preferences.json"
111
111
  assert_file "$TMP_CLAUDE/.claude/settings.json" "~/.claude/settings.json"
112
112
 
113
- # settings.json must have the secret-detection hook
113
+ # settings.json must have the secret-detection hook. v11.4.1 changed the
114
+ # matcher from the dead "Bash(git commit:*)" (tool-name matchers never matched
115
+ # it) to "Bash"; command-level filtering happens inside pre-commit-check.sh.
116
+ if grep -q '"matcher": "Bash"' "$TMP_CLAUDE/.claude/settings.json" \
117
+ && grep -q "pre-commit-check.sh" "$TMP_CLAUDE/.claude/settings.json"; then
118
+ pass "settings.json registers PreToolUse secret-scan hook (matcher Bash)"
119
+ else
120
+ fail "settings.json missing PreToolUse secret-scan hook with matcher Bash"
121
+ fi
114
122
  if grep -q "Bash(git commit:\*)" "$TMP_CLAUDE/.claude/settings.json"; then
115
- pass "settings.json registers PreToolUse git-commit hook"
123
+ fail "settings.json still contains the dead Bash(git commit:*) matcher"
116
124
  else
117
- fail "settings.json missing PreToolUse git-commit hook"
125
+ pass "settings.json has no dead Bash(git commit:*) matcher"
118
126
  fi
119
127
  if grep -q "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE" "$TMP_CLAUDE/.claude/settings.json"; then
120
128
  pass "settings.json sets CLAUDE_AUTOCOMPACT_PCT_OVERRIDE"