@mmerterden/multi-agent-pipeline 11.3.2 → 11.5.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 (73) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/README.md +1 -0
  3. package/index.js +9 -2
  4. package/install/_common.mjs +107 -5
  5. package/install/_telemetry.mjs +5 -23
  6. package/install/claude.mjs +113 -11
  7. package/install/copilot.mjs +75 -6
  8. package/package.json +4 -10
  9. package/pipeline/commands/multi-agent/SKILL.md +5 -1
  10. package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
  11. package/pipeline/commands/multi-agent/help/SKILL.md +6 -2
  12. package/pipeline/commands/multi-agent/review/SKILL.md +36 -4
  13. package/pipeline/commands/multi-agent/review-issue/SKILL.md +22 -0
  14. package/pipeline/commands/multi-agent/review-jira/SKILL.md +22 -0
  15. package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
  16. package/pipeline/lib/account-resolver.sh +61 -23
  17. package/pipeline/lib/channels-multi-repo.sh +7 -2
  18. package/pipeline/lib/count-lib.sh +27 -0
  19. package/pipeline/lib/credential-store.sh +23 -6
  20. package/pipeline/lib/extract-conventions.sh +27 -21
  21. package/pipeline/lib/fetch-confluence.sh +25 -13
  22. package/pipeline/lib/fetch-crashlytics.sh +30 -8
  23. package/pipeline/lib/fetch-fortify.sh +11 -2
  24. package/pipeline/lib/fetch-swagger.sh +18 -7
  25. package/pipeline/lib/figma-mcp-refresh.sh +26 -11
  26. package/pipeline/lib/figma-screenshot.sh +11 -2
  27. package/pipeline/lib/issue-fetcher.sh +31 -6
  28. package/pipeline/lib/multi-repo-pipeline.sh +84 -20
  29. package/pipeline/lib/plan-todos.sh +12 -3
  30. package/pipeline/lib/post-pr-review.sh +5 -3
  31. package/pipeline/lib/repo-cache.sh +53 -9
  32. package/pipeline/lib/review-watch.sh +26 -6
  33. package/pipeline/lib/shadow-git.sh +45 -4
  34. package/pipeline/lib/vercel-deploy.sh +10 -1
  35. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
  36. package/pipeline/multi-agent-refs/readiness-review.md +65 -0
  37. package/pipeline/scripts/audit-log-rotate.sh +38 -10
  38. package/pipeline/scripts/check-md-links.mjs +34 -9
  39. package/pipeline/scripts/diff-risk-score.mjs +4 -1
  40. package/pipeline/scripts/evidence-gate.mjs +10 -1
  41. package/pipeline/scripts/fixtures/install-layout.tsv +7 -7
  42. package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
  43. package/pipeline/scripts/keychain-save.sh +13 -9
  44. package/pipeline/scripts/lint-skills.mjs +40 -2
  45. package/pipeline/scripts/migrate-prefs.mjs +26 -7
  46. package/pipeline/scripts/phase-tracker.sh +71 -0
  47. package/pipeline/scripts/pre-commit-check.sh +39 -0
  48. package/pipeline/scripts/scan-skills.sh +217 -127
  49. package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
  50. package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
  51. package/pipeline/scripts/smoke-generate-issue.sh +3 -3
  52. package/pipeline/scripts/smoke-install-layout.sh +11 -3
  53. package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
  54. package/pipeline/scripts/smoke-pack-contents.sh +140 -0
  55. package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
  56. package/pipeline/scripts/smoke-review-readiness.sh +91 -0
  57. package/pipeline/scripts/smoke-shadow-git.sh +48 -1
  58. package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
  59. package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
  60. package/pipeline/scripts/smoke-wrapper-preservation.sh +68 -0
  61. package/pipeline/scripts/test-gap-scan.mjs +12 -1
  62. package/pipeline/scripts/triage-memory.mjs +10 -1
  63. package/pipeline/scripts/uninstall.mjs +105 -36
  64. package/pipeline/scripts/update-issue-progress.sh +60 -41
  65. package/pipeline/scripts/write-state.mjs +14 -4
  66. package/pipeline/skills/.skill-manifest.json +13 -5
  67. package/pipeline/skills/.skills-index.json +20 -2
  68. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +6 -2
  69. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -10
  70. package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +25 -0
  71. package/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md +25 -0
  72. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  73. package/pipeline/skills/skills-index.md +4 -2
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # smoke-plugin-validate.sh - run `claude plugin validate --strict` against
4
+ # every plugin / marketplace surface committed to this repo.
5
+ #
6
+ # Discovery: any directory containing `.claude-plugin/plugin.json` or
7
+ # `.claude-plugin/marketplace.json` (node_modules excluded). The `claude`
8
+ # CLI validates the containing directory; --strict promotes warnings
9
+ # (unrecognized fields, missing metadata) to failures so CI catches drift
10
+ # the runtime would silently tolerate.
11
+ #
12
+ # Skip semantics (exit 0 with a SKIP line):
13
+ # - `claude` binary not on PATH (optional tooling; billing-friendly CI
14
+ # runners don't install it)
15
+ # - no validatable surface in the repo (the marketplace plugins live in
16
+ # the separate multi-agent-plugins repo; this repo may carry none)
17
+ #
18
+ # Exit codes: 0=pass or skip, 1=validation failure
19
+
20
+ set -uo pipefail
21
+
22
+ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
23
+
24
+ if ! command -v claude >/dev/null 2>&1; then
25
+ echo "SKIP smoke-plugin-validate: claude binary not on PATH"
26
+ exit 0
27
+ fi
28
+
29
+ # Each hit is a manifest; the validatable surface is its .claude-plugin parent's parent.
30
+ MANIFESTS=$(find "$REPO_ROOT" \
31
+ -path "$REPO_ROOT/node_modules" -prune -o \
32
+ -path "$REPO_ROOT/coverage" -prune -o \
33
+ -type f -path '*/.claude-plugin/*' \( -name plugin.json -o -name marketplace.json \) -print \
34
+ 2>/dev/null | sort)
35
+
36
+ if [ -z "$MANIFESTS" ]; then
37
+ echo "SKIP smoke-plugin-validate: no .claude-plugin plugin/marketplace manifest in this repo"
38
+ exit 0
39
+ fi
40
+
41
+ PASS=0
42
+ FAIL=0
43
+ pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
44
+ fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
45
+
46
+ # Validate each surface directory once, even if it carries both manifests.
47
+ SURFACES=$(echo "$MANIFESTS" | sed 's|/\.claude-plugin/[^/]*$||' | sort -u)
48
+
49
+ for surface in $SURFACES; do
50
+ rel="${surface#"$REPO_ROOT"/}"
51
+ [ "$surface" = "$REPO_ROOT" ] && rel="."
52
+ echo ""
53
+ echo "→ claude plugin validate --strict $rel"
54
+ if OUT=$(claude plugin validate --strict "$surface" 2>&1); then
55
+ pass "$rel validates clean (strict)"
56
+ else
57
+ fail "$rel failed strict validation"
58
+ echo "$OUT" | sed 's/^/ /'
59
+ fi
60
+ done
61
+
62
+ echo ""
63
+ echo "══ smoke-plugin-validate: $PASS passed, $FAIL failed ══"
64
+ [ "$FAIL" -gt 0 ] && exit 1 || exit 0
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-review-readiness.sh - contract for /multi-agent:review-jira + /multi-agent:review-issue.
3
+ #
4
+ # Verifies:
5
+ # 1. Files exist: 2 commands, 2 Copilot skills, 1 shared ref.
6
+ # 2. Frontmatter keys on both surfaces.
7
+ # 3. Both commands delegate to refs/readiness-review.md and reuse issue-fetcher.
8
+ # 4. The shared ref names the maturity engine + both comment channels + the confirm gate.
9
+ # 5. Registration: dispatcher rows, routing rows, help EN+TR, contract inventory + count (38).
10
+ # 6. Read-only guarantee (no worktree/commit/create) stated.
11
+ # 7. Forbidden strings absent (em-dash, section sign, the M-word).
12
+
13
+ set -euo pipefail
14
+ HERE="$(cd "$(dirname "$0")" && pwd)"
15
+ ROOT="$(cd "$HERE/../.." && pwd)"
16
+
17
+ REF="$ROOT/pipeline/multi-agent-refs/readiness-review.md"
18
+ CMD_J="$ROOT/pipeline/commands/multi-agent/review-jira/SKILL.md"
19
+ CMD_I="$ROOT/pipeline/commands/multi-agent/review-issue/SKILL.md"
20
+ SK_J="$ROOT/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md"
21
+ SK_I="$ROOT/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md"
22
+ DISPATCHER="$ROOT/pipeline/commands/multi-agent/SKILL.md"
23
+ CONTRACT="$ROOT/pipeline/multi-agent-refs/cross-cli-contract.md"
24
+ HELP="$ROOT/pipeline/commands/multi-agent/help/SKILL.md"
25
+
26
+ PASS=0; FAIL=0
27
+ pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
28
+ fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
29
+
30
+ echo "→ 1. files exist"
31
+ for f in "$REF" "$CMD_J" "$CMD_I" "$SK_J" "$SK_I"; do
32
+ if [ -f "$f" ]; then pass "exists: ${f#$ROOT/}"; else fail "missing: ${f#$ROOT/}"; fi
33
+ done
34
+ [ "$FAIL" -eq 0 ] || { echo "══ review-readiness smoke: $PASS passed, $FAIL failed ══"; exit 1; }
35
+
36
+ echo ""
37
+ echo "→ 2. frontmatter keys"
38
+ for f in "$CMD_J" "$CMD_I"; do
39
+ head -4 "$f" | grep -q '^description:' && pass "description: ${f#$ROOT/}" || fail "description missing: ${f#$ROOT/}"
40
+ head -4 "$f" | grep -q '^argument-hint:' && pass "argument-hint: ${f#$ROOT/}" || fail "argument-hint missing: ${f#$ROOT/}"
41
+ done
42
+ head -8 "$SK_J" | grep -q '^name: multi-agent-review-jira$' && pass "name: multi-agent-review-jira" || fail "name wrong: $SK_J"
43
+ head -8 "$SK_I" | grep -q '^name: multi-agent-review-issue$' && pass "name: multi-agent-review-issue" || fail "name wrong: $SK_I"
44
+ for f in "$SK_J" "$SK_I"; do
45
+ head -8 "$f" | grep -q '^user-invocable: true' && pass "user-invocable: ${f#$ROOT/}" || fail "user-invocable missing: ${f#$ROOT/}"
46
+ done
47
+
48
+ echo ""
49
+ echo "→ 3. delegate to shared ref + reuse issue-fetcher"
50
+ for f in "$CMD_J" "$CMD_I" "$SK_J" "$SK_I"; do
51
+ grep -Fq "readiness-review.md" "$f" && pass "ref link: ${f#$ROOT/}" || fail "ref link missing: ${f#$ROOT/}"
52
+ grep -Fq "issue-fetcher.sh" "$f" && pass "issue-fetcher reuse: ${f#$ROOT/}" || fail "issue-fetcher missing: ${f#$ROOT/}"
53
+ done
54
+
55
+ echo ""
56
+ echo "→ 4. shared ref names maturity + channels + confirm gate"
57
+ grep -Fqi "maturity" "$REF" && pass "ref uses maturity engine" || fail "ref missing maturity"
58
+ grep -Fq "channels/jira.md" "$REF" && pass "ref -> jira comment channel" || fail "ref missing jira channel"
59
+ grep -Fq "channels/issue-comment.md" "$REF" && pass "ref -> github comment channel" || fail "ref missing issue channel"
60
+ grep -Fqi "confirm" "$REF" && pass "ref has confirm-before-post gate" || fail "ref missing confirm gate"
61
+ grep -Fqi "rubric" "$REF" && pass "ref has readiness rubric" || fail "ref missing rubric"
62
+
63
+ echo ""
64
+ echo "→ 5. registration + count (38)"
65
+ grep -Fq '| `review-jira ' "$DISPATCHER" && pass "dispatcher desc: review-jira" || fail "dispatcher desc missing review-jira"
66
+ grep -Fq '| `review-issue ' "$DISPATCHER" && pass "dispatcher desc: review-issue" || fail "dispatcher desc missing review-issue"
67
+ grep -Fq "review-jira/SKILL.md" "$DISPATCHER" && pass "dispatcher routing: review-jira" || fail "dispatcher routing missing review-jira"
68
+ grep -Fq "review-issue/SKILL.md" "$DISPATCHER" && pass "dispatcher routing: review-issue" || fail "dispatcher routing missing review-issue"
69
+ grep -Fq "## 1. Command Inventory (38 commands)" "$CONTRACT" && pass "contract count = 38" || fail "contract count wrong"
70
+ grep -Eq '(^|[ ,])review-jira([ ,]|$)' "$CONTRACT" && pass "contract inventory has review-jira" || fail "contract missing review-jira"
71
+ grep -Eq '(^|[ ,])review-issue([ ,]|$)' "$CONTRACT" && pass "contract inventory has review-issue" || fail "contract missing review-issue"
72
+ grep -Fq "multi-agent:review-jira" "$HELP" && pass "help lists review-jira" || fail "help missing review-jira"
73
+ grep -Fq "multi-agent:review-issue" "$HELP" && pass "help lists review-issue" || fail "help missing review-issue"
74
+
75
+ echo ""
76
+ echo "→ 6. read-only guarantee stated"
77
+ for f in "$CMD_J" "$CMD_I"; do
78
+ grep -Fqi "no worktree" "$f" && pass "read-only stated: ${f#$ROOT/}" || fail "read-only note missing: ${f#$ROOT/}"
79
+ done
80
+
81
+ echo ""
82
+ echo "→ 7. forbidden strings absent"
83
+ MWORD="MAN""DATORY"
84
+ for f in "$REF" "$CMD_J" "$CMD_I" "$SK_J" "$SK_I"; do
85
+ if grep -q $'—\|§\|…' "$f"; then fail "forbidden unicode punctuation in ${f##*/}"; else pass "punctuation clean: ${f##*/}"; fi
86
+ if grep -q "$MWORD" "$f"; then fail "forbidden word in ${f##*/}"; else pass "vocabulary clean: ${f##*/}"; fi
87
+ done
88
+
89
+ echo ""
90
+ echo "══ review-readiness smoke: $PASS passed, $FAIL failed ══"
91
+ [ "$FAIL" -eq 0 ]
@@ -98,8 +98,22 @@ else
98
98
  record_fail "node_modules leaked into shadow tracking"
99
99
  fi
100
100
 
101
- # Make a real edit + snapshot
101
+ # 7b. Forced excludes apply even when NOT in the project .gitignore
102
+ # (DerivedData is not in this fixture's .gitignore; it must be excluded via
103
+ # the shadow repo's .git/info/exclude).
104
+ mkdir -p DerivedData && echo "buildjunk" > DerivedData/junk.txt
105
+ bash "$SG" snapshot "$TASK_ID" "$TMP" "derived-data-probe" >/dev/null 2>&1
106
+ if ! git --git-dir="$HOME/.claude/state/shadow-git/$TASK_ID/.git" --work-tree="$TMP" \
107
+ ls-files 2>/dev/null | grep -q "^DerivedData/"; then
108
+ record_pass "DerivedData NOT tracked (forced exclude in .git/info/exclude)"
109
+ else
110
+ record_fail "DerivedData leaked into shadow tracking (forced excludes dead)"
111
+ fi
112
+ rm -rf DerivedData
113
+
114
+ # Make a real edit + a brand-new file + snapshot
102
115
  echo "hello-changed" > a.txt
116
+ echo "post-baseline" > b.txt
103
117
  sha_edit=$(bash "$SG" snapshot "$TASK_ID" "$TMP" "edited a.txt" 2>/dev/null | head -1)
104
118
  if [ -n "$sha_edit" ] && [ "${#sha_edit}" -ge 7 ]; then
105
119
  record_pass "snapshot after edit produced a sha: $sha_edit"
@@ -115,6 +129,7 @@ else
115
129
  fi
116
130
 
117
131
  # 8. Restore the baseline
132
+ echo "never-snapshotted" > c.txt # created after the last snapshot
118
133
  baseline_sha=$(bash "$SG" list "$TASK_ID" 2>/dev/null | tail -1 | awk '{print $1}')
119
134
  bash "$SG" restore "$TASK_ID" "$TMP" "$baseline_sha" --files >/dev/null 2>&1
120
135
  if [ "$(cat "$TMP/a.txt")" = "hello" ]; then
@@ -123,6 +138,23 @@ else
123
138
  record_fail "restore did not revert a.txt (got '$(cat "$TMP/a.txt")')"
124
139
  fi
125
140
 
141
+ # 8b. Restore removes files that did not exist in the snapshot
142
+ if [ ! -f "$TMP/b.txt" ]; then
143
+ record_pass "restore removed b.txt (committed after baseline)"
144
+ else
145
+ record_fail "restore left b.txt in place (created after snapshot must be removed)"
146
+ fi
147
+ if [ ! -f "$TMP/c.txt" ]; then
148
+ record_pass "restore removed c.txt (never snapshotted)"
149
+ else
150
+ record_fail "restore left c.txt in place (untracked new file must be removed)"
151
+ fi
152
+ if [ -f "$TMP/node_modules/x.txt" ]; then
153
+ record_pass "restore left ignored node_modules untouched"
154
+ else
155
+ record_fail "restore deleted ignored files (node_modules must survive clean)"
156
+ fi
157
+
126
158
  # 9. Prune
127
159
  bash "$SG" prune "$TASK_ID" >/dev/null 2>&1
128
160
  if [ ! -d "$HOME/.claude/state/shadow-git/$TASK_ID" ]; then
@@ -131,6 +163,21 @@ else
131
163
  record_fail "prune did not remove the shadow dir"
132
164
  fi
133
165
 
166
+ # 9b. Task-id validation: traversal and separator ids are rejected before any
167
+ # rm -rf (a "prune .." must never delete ~/.claude/state).
168
+ for bad_id in ".." "." "a/b" "-x!"; do
169
+ if bash "$SG" prune "$bad_id" >/dev/null 2>&1; then
170
+ record_fail "prune accepted unsafe task id '$bad_id'"
171
+ else
172
+ record_pass "prune rejects unsafe task id '$bad_id'"
173
+ fi
174
+ done
175
+ if [ -d "$HOME/.claude/state" ]; then
176
+ record_pass "~/.claude/state survived unsafe prune attempts"
177
+ else
178
+ record_fail "~/.claude/state is GONE after unsafe prune attempts"
179
+ fi
180
+
134
181
  cd - >/dev/null
135
182
  rm -rf "$TMP"
136
183
 
@@ -51,7 +51,7 @@ TARGETS=(
51
51
 
52
52
  # Collect the list once.
53
53
  SKILLS=$(find "${TARGETS[@]}" -name 'SKILL.md' -type f 2>/dev/null | sort)
54
- SKILL_COUNT=$(echo "$SKILLS" | grep -c . || echo 0)
54
+ SKILL_COUNT=$(echo "$SKILLS" | grep -c . || true)
55
55
 
56
56
  echo "→ Scope: $SKILL_COUNT authored SKILL.md files under shared/core/"
57
57
  echo " (external/ intentionally excluded - upstream conventions)"
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # smoke-workflow-audit.sh - security + correctness audit of
4
+ # `.github/workflows/` with the two dedicated external linters:
5
+ #
6
+ # - zizmor GitHub Actions security auditor (template injection,
7
+ # credential persistence, unpinned actions, ...). Runs in
8
+ # offline mode so the smoke never touches the network.
9
+ # - actionlint workflow correctness linter (expression syntax, matrix
10
+ # typos, shellcheck of run: blocks, ...).
11
+ #
12
+ # Both tools are optional: each is skipped with a SKIP line when its binary
13
+ # is not on PATH (billing-friendly CI runners install neither; run locally
14
+ # via `brew install zizmor actionlint` for full coverage). Structural
15
+ # baseline checks that need no external binary live in
16
+ # smoke-ci-workflows.sh - this smoke is the deeper, tool-backed layer.
17
+ #
18
+ # Any finding from either tool fails the smoke.
19
+ #
20
+ # Exit codes: 0=pass or all tools skipped, 1=findings, 2=workflows missing
21
+
22
+ set -uo pipefail
23
+
24
+ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
25
+ WF_DIR="$REPO_ROOT/.github/workflows"
26
+
27
+ [ -d "$WF_DIR" ] || { echo "FAIL: .github/workflows/ missing"; exit 2; }
28
+
29
+ PASS=0
30
+ FAIL=0
31
+ SKIPPED=0
32
+ pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
33
+ fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
34
+ skip() { SKIPPED=$((SKIPPED + 1)); echo " ↷ SKIP: $1"; }
35
+
36
+ # ──────────────────────────────────────────────────────────────────────────
37
+ echo "→ 1. zizmor (security audit, offline)"
38
+ if command -v zizmor >/dev/null 2>&1; then
39
+ # Flag names shifted across zizmor releases (--offline vs --no-online-audits);
40
+ # probe --help so the smoke works with whichever is installed.
41
+ HELP=$(zizmor --help 2>&1 || true)
42
+ OFFLINE_FLAG="--offline"
43
+ echo "$HELP" | grep -q -- '--offline' || OFFLINE_FLAG="--no-online-audits"
44
+ if OUT=$(zizmor "$WF_DIR" --min-severity medium "$OFFLINE_FLAG" 2>&1); then
45
+ pass "zizmor: no findings at medium+ severity"
46
+ else
47
+ fail "zizmor reported findings (medium+ severity)"
48
+ echo "$OUT" | sed 's/^/ /'
49
+ fi
50
+ else
51
+ skip "zizmor not on PATH (brew install zizmor)"
52
+ fi
53
+
54
+ # ──────────────────────────────────────────────────────────────────────────
55
+ echo "→ 2. actionlint (workflow correctness)"
56
+ if command -v actionlint >/dev/null 2>&1; then
57
+ if OUT=$(cd "$REPO_ROOT" && actionlint 2>&1); then
58
+ pass "actionlint: no findings"
59
+ else
60
+ fail "actionlint reported findings"
61
+ echo "$OUT" | sed 's/^/ /'
62
+ fi
63
+ else
64
+ skip "actionlint not on PATH (brew install actionlint)"
65
+ fi
66
+
67
+ echo ""
68
+ echo "══ smoke-workflow-audit: $PASS passed, $FAIL failed, $SKIPPED skipped ══"
69
+ [ "$FAIL" -gt 0 ] && exit 1 || exit 0
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-wrapper-preservation.sh - local-only alias wrappers must survive install/update.
3
+ #
4
+ # Local-only wrappers (frontmatter `local-only: true`) live ONLY under
5
+ # ~/.claude/commands/multi-agent/ (never in pipeline/, never synced). install
6
+ # wipes that namespace before copying from pipeline; without the snapshot+restore
7
+ # carve-out in install/claude.mjs installCommands, wrappers are destroyed on every
8
+ # run. This asserts they survive a second install.
9
+
10
+ set -uo pipefail
11
+ HERE="$(cd "$(dirname "$0")" && pwd)"
12
+ ROOT="$(cd "$HERE/../.." && pwd)"
13
+ INSTALL_JS="$ROOT/install.js"
14
+
15
+ PASS=0; FAIL=0
16
+ pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
17
+ fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
18
+
19
+ TMP="$(mktemp -d)"
20
+ trap 'rm -rf "$TMP"' EXIT
21
+
22
+ echo "→ 1. first install lays down the namespace"
23
+ if HOME="$TMP" node "$INSTALL_JS" --claude >/dev/null 2>&1; then
24
+ pass "install --claude (1st) exited 0"
25
+ else
26
+ fail "install --claude (1st) failed"
27
+ echo "══ wrapper-preservation smoke: $PASS passed, $FAIL failed ══"; exit 1
28
+ fi
29
+
30
+ WRAP_DIR="$TMP/.claude/commands/multi-agent/zz-wrapper-probe"
31
+ mkdir -p "$WRAP_DIR"
32
+ cat > "$WRAP_DIR/SKILL.md" <<'EOF'
33
+ ---
34
+ description: "probe local-only wrapper"
35
+ allowed-tools: Skill
36
+ local-only: true
37
+ ---
38
+ Use the Skill tool to invoke `some-plugin:some-skill`.
39
+ EOF
40
+ [ -f "$WRAP_DIR/SKILL.md" ] && pass "seeded a local-only wrapper" || fail "could not seed wrapper"
41
+
42
+ echo ""
43
+ echo "→ 2. second install must PRESERVE the wrapper"
44
+ if HOME="$TMP" node "$INSTALL_JS" --claude >/dev/null 2>&1; then
45
+ pass "install --claude (2nd) exited 0"
46
+ else
47
+ fail "install --claude (2nd) failed"
48
+ fi
49
+ if [ -f "$WRAP_DIR/SKILL.md" ] && grep -q '^local-only: true' "$WRAP_DIR/SKILL.md"; then
50
+ pass "local-only wrapper survived re-install"
51
+ else
52
+ fail "local-only wrapper was DESTROYED by re-install (carve-out regressed)"
53
+ fi
54
+
55
+ echo ""
56
+ echo "→ 3. a normal (non-local-only) stray dir is still wiped"
57
+ STRAY="$TMP/.claude/commands/multi-agent/zz-stray-probe"
58
+ mkdir -p "$STRAY"; printf -- '---\ndescription: "stray"\n---\n' > "$STRAY/SKILL.md"
59
+ HOME="$TMP" node "$INSTALL_JS" --claude >/dev/null 2>&1
60
+ if [ -f "$STRAY/SKILL.md" ]; then
61
+ fail "non-local-only stray survived (wipe too weak)"
62
+ else
63
+ pass "non-local-only stray correctly wiped"
64
+ fi
65
+
66
+ echo ""
67
+ echo "══ wrapper-preservation smoke: $PASS passed, $FAIL failed ══"
68
+ [ "$FAIL" -eq 0 ]
@@ -114,7 +114,18 @@ function isExcluded(path) {
114
114
  if (pat.endsWith("/")) {
115
115
  if (path.includes(pat) || path.startsWith(pat)) return true;
116
116
  } else if (pat.includes("**")) {
117
- const re = new RegExp("^" + pat.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
117
+ // Tokenize ** before the single-* pass so the * rewrite cannot mangle
118
+ // the already-translated .* (which broke cross-directory matches).
119
+ const DOUBLE_STAR = "\u0000";
120
+ const re = new RegExp(
121
+ "^" +
122
+ pat
123
+ .replace(/\./g, "\\.")
124
+ .replace(/\*\*/g, DOUBLE_STAR)
125
+ .replace(/\*/g, "[^/]*")
126
+ .replace(new RegExp(DOUBLE_STAR, "g"), ".*") +
127
+ "$",
128
+ );
118
129
  if (re.test(path)) return true;
119
130
  } else if (path.endsWith(pat)) return true;
120
131
  }
@@ -110,7 +110,16 @@ function score(queryTokens, row) {
110
110
  }
111
111
 
112
112
  function globToRegExp(glob) {
113
- const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".");
113
+ // Tokenize ** before the single-* pass: a sequential replace would turn
114
+ // ** into .* and then the global * pass would rewrite it to .[^/]*,
115
+ // breaking cross-directory matches (src/**/x vs src/a/b/x).
116
+ const DOUBLE_STAR = "\u0000";
117
+ const escaped = glob
118
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
119
+ .replace(/\*\*/g, DOUBLE_STAR)
120
+ .replace(/\*/g, "[^/]*")
121
+ .replace(/\?/g, ".")
122
+ .replace(new RegExp(DOUBLE_STAR, "g"), ".*");
114
123
  return new RegExp("^" + escaped + "$", "i");
115
124
  }
116
125
 
@@ -32,10 +32,38 @@
32
32
 
33
33
  import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
34
34
  import { join } from "path";
35
+ import { pathToFileURL } from "url";
35
36
  import { createInterface } from "readline";
36
37
 
37
38
  const HOME = process.env.HOME || process.env.USERPROFILE;
38
39
 
40
+ /**
41
+ * Agent definition files the pipeline ships (pipeline/agents/). This runs
42
+ * standalone from ~/.claude/scripts, so the list is static here; a unit test
43
+ * (test/uninstall.test.mjs) asserts it matches the source tree so it cannot
44
+ * go stale. Only these file names are removed from the installed agents
45
+ * dirs - user-authored agent files are never touched.
46
+ */
47
+ export const PIPELINE_AGENT_FILES = [
48
+ "android-architect.md",
49
+ "backend-architect.md",
50
+ "code-reviewer.md",
51
+ "dev-critic.md",
52
+ "explorer.md",
53
+ "ios-architect.md",
54
+ "security-auditor.md",
55
+ "task-clarifier.md",
56
+ ];
57
+
58
+ /**
59
+ * Non-multi-agent core skills the installer ships from
60
+ * pipeline/skills/shared/core/ (the multi-agent* dirs are matched by prefix).
61
+ */
62
+ export const PIPELINE_CORE_SKILL_DIRS = [
63
+ "apple-archive-compliance",
64
+ "google-play-compliance",
65
+ ];
66
+
39
67
  const flags = process.argv.slice(2).filter((a) => a !== "uninstall");
40
68
 
41
69
  const TOOL_FLAGS = [
@@ -122,6 +150,33 @@ function rmMatchingFiles(parent, predicate) {
122
150
  return count;
123
151
  }
124
152
 
153
+ // Explicit terminator install/copilot.mjs writes after the pipeline section
154
+ // (v11.4.1+). Must stay in sync with INSTRUCTIONS_END_MARKER there.
155
+ const COPILOT_END_MARKER = "<!-- multi-agent-pipeline:copilot-instructions:end -->";
156
+
157
+ /**
158
+ * Legacy copilot-instructions files (written before the end marker existed)
159
+ * have no explicit terminator. Bound the pipeline span at the next top-level
160
+ * "# " heading after the start marker when one exists OUTSIDE fenced code
161
+ * blocks (the pipeline body carries bash comments like "# Bootstrap once ..."
162
+ * inside fences); otherwise the span runs to EOF (pre-marker behavior).
163
+ * Mirrors install/copilot.mjs legacyTrailingContent.
164
+ * @param {string} section - content from the start marker onward
165
+ * @returns {string} user content trailing the pipeline span ("" if none)
166
+ */
167
+ function legacyTrailingContent(section) {
168
+ const lines = section.split("\n");
169
+ let inFence = false;
170
+ for (let i = 1; i < lines.length; i++) {
171
+ if (/^\s*(```|~~~)/.test(lines[i])) {
172
+ inFence = !inFence;
173
+ continue;
174
+ }
175
+ if (!inFence && /^# /.test(lines[i])) return lines.slice(i).join("\n");
176
+ }
177
+ return "";
178
+ }
179
+
125
180
  /**
126
181
  * Strip a marker-wrapped `<!-- multi-agent-pipeline:begin/end -->` block from
127
182
  * a user-owned file. Preserves everything outside the markers. Deletes the
@@ -135,25 +190,33 @@ function stripManagedBlock(filePath) {
135
190
  /\s*<!-- multi-agent-pipeline:begin -->[\s\S]*?<!-- multi-agent-pipeline:end -->\s*/m;
136
191
  if (!re.test(content)) {
137
192
  // Copilot's install (install/copilot.mjs) does not wrap the block in
138
- // begin/end markers; it always writes the pipeline section LAST, from the
139
- // `# Multi-Agent Development Pipeline` heading to EOF, keeping any user
140
- // content ABOVE it. So slicing from that heading to EOF reliably removes
141
- // only our section regardless of file size (no arbitrary size cap - a cap
142
- // used to orphan the block in files >50 KB).
193
+ // begin/end markers. v11.4.1+ writes an explicit end marker after the
194
+ // pipeline section; strip only the start..end span so user content BELOW
195
+ // the section survives. Legacy files without the end marker are bounded
196
+ // at the next top-level heading outside code fences, else EOF.
143
197
  if (content.includes("# Multi-Agent Development Pipeline")) {
144
- // Heuristic: Copilot's generateCopilotInstructions output. Only strip the
145
- // pipeline section, not the whole file.
146
198
  const marker = "# Multi-Agent Development Pipeline";
147
199
  const idx = content.indexOf(marker);
148
200
  if (idx >= 0) {
149
201
  const before = content.slice(0, idx).trimEnd();
202
+ const fromStart = content.slice(idx);
203
+ const endIdx = fromStart.indexOf(COPILOT_END_MARKER);
204
+ const rawTrailing =
205
+ endIdx >= 0
206
+ ? fromStart.slice(endIdx + COPILOT_END_MARKER.length)
207
+ : legacyTrailingContent(fromStart);
208
+ const trailing = rawTrailing.replace(/^[\r\n]+/, "").trimEnd();
209
+ let remaining = before;
210
+ if (trailing.length > 0) {
211
+ remaining = before.length > 0 ? before + "\n\n" + trailing : trailing;
212
+ }
150
213
  if (dryRun) {
151
214
  report("would update", filePath);
152
- } else if (before.length === 0) {
215
+ } else if (remaining.length === 0) {
153
216
  rmSync(filePath, { force: true });
154
217
  console.log(` removed (was pipeline-only): ${filePath}`);
155
218
  } else {
156
- writeFileSync(filePath, before + "\n");
219
+ writeFileSync(filePath, remaining + "\n");
157
220
  console.log(` cleaned pipeline section: ${filePath}`);
158
221
  }
159
222
  return true;
@@ -200,9 +263,12 @@ function cleanClaudeSettings(settingsPath) {
200
263
 
201
264
  if (settings.hooks?.PreToolUse?.length) {
202
265
  const before = settings.hooks.PreToolUse.length;
266
+ // Match both the current matcher ("Bash") and the dead pre-v11.4.1 one
267
+ // ("Bash(git commit:*)"); the pre-commit-check.sh command identifies the
268
+ // entry as ours either way.
203
269
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
204
270
  (h) =>
205
- !(h.matcher === "Bash(git commit:*)" &&
271
+ !((h.matcher === "Bash" || h.matcher === "Bash(git commit:*)") &&
206
272
  h.hooks?.some((sub) => sub.command?.includes("pre-commit-check.sh"))),
207
273
  );
208
274
  if (settings.hooks.PreToolUse.length < before) touched = true;
@@ -239,8 +305,8 @@ async function main() {
239
305
  if (dryRun) console.log(" [DRY-RUN] Nothing will be modified.");
240
306
  console.log("");
241
307
  console.log(" Targets:");
242
- if (forClaude) console.log(" - Claude Code (~/.claude/{commands,skills,agents,rules,scripts}/multi-agent*)");
243
- if (forCopilot) console.log(" - Copilot CLI (~/.copilot/{skills,agents,scripts}/multi-agent* + copilot-instructions.md section)");
308
+ if (forClaude) console.log(" - Claude Code (~/.claude/{commands/multi-agent, scripts, multi-agent-refs, schemas, lib} + pipeline skills + pipeline agent files)");
309
+ if (forCopilot) console.log(" - Copilot CLI (~/.copilot/{scripts, schemas, lib} + pipeline skills + pipeline agent files + copilot-instructions.md section)");
244
310
  if (forCursor) console.log(` - Cursor (${adapterTarget}/.cursor/rules/multi-agent-* + .cursorrules)`);
245
311
  if (forCopilotChat) console.log(` - GitHub Copilot Chat (${adapterTarget}/.github/copilot-instructions.md block + .github/instructions/multi-agent-*)`);
246
312
  if (forAntigravity) console.log(` - Antigravity (${adapterTarget}/.agent/rules + .agent/workflows + AGENTS.md block + .agent/mcp_config.json)`);
@@ -265,6 +331,10 @@ async function main() {
265
331
  const CLAUDE = join(HOME, ".claude");
266
332
  rmIfExists(join(CLAUDE, "commands", "multi-agent"));
267
333
  rmIfExists(join(CLAUDE, "scripts"));
334
+ // Pipeline-managed trees the installer lays down alongside scripts/.
335
+ rmIfExists(join(CLAUDE, "multi-agent-refs"));
336
+ rmIfExists(join(CLAUDE, "schemas"));
337
+ rmIfExists(join(CLAUDE, "lib"));
268
338
  // NOTE: ~/.claude/rules/ is USER-OWNED. install lays baseline rules down
269
339
  // write-if-missing and NEVER overwrites them (install/claude.mjs
270
340
  // installRules, skipExisting) so users can evolve their global rules
@@ -274,20 +344,14 @@ async function main() {
274
344
  const skills = join(CLAUDE, "skills");
275
345
  let n = rmMatchingDirs(skills, (name) => name === "multi-agent" || name.startsWith("multi-agent-"));
276
346
  n += rmMatchingDirs(skills, (name) => name === "figma-ios" || name === "figma-android" || name === "figma-common" || name === "figma-to-component");
347
+ n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
277
348
  if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
278
349
  rmIfExists(join(skills, ".skills-index.json"));
279
350
  rmIfExists(join(skills, "skills-index.md"));
280
351
  rmIfExists(join(skills, "README.md"));
281
- rmMatchingDirs(join(CLAUDE, "agents"), (name) =>
282
- [
283
- "code-reviewer.md",
284
- "explorer.md",
285
- "ios-architect.md",
286
- "android-architect.md",
287
- "backend-architect.md",
288
- "security-auditor.md",
289
- ].includes(name),
290
- );
352
+ // Agent definitions are FILES; remove only the pipeline-shipped names so
353
+ // user-authored agents survive.
354
+ rmMatchingFiles(join(CLAUDE, "agents"), (name) => PIPELINE_AGENT_FILES.includes(name));
291
355
  cleanClaudeSettings(join(CLAUDE, "settings.json"));
292
356
  }
293
357
 
@@ -296,21 +360,20 @@ async function main() {
296
360
  console.log(" [Copilot CLI] Removing...");
297
361
  const COP = join(HOME, ".copilot");
298
362
  rmIfExists(join(COP, "scripts"));
363
+ // Pipeline-managed trees the installer lays down alongside scripts/.
364
+ rmIfExists(join(COP, "schemas"));
365
+ rmIfExists(join(COP, "lib"));
299
366
  const skills = join(COP, "skills");
300
367
  let n = rmMatchingDirs(skills, (name) => name === "multi-agent" || name.startsWith("multi-agent-"));
301
368
  n += rmMatchingDirs(skills, (name) => name === "figma-ios" || name === "figma-android" || name === "figma-common" || name === "figma-to-component");
369
+ n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
302
370
  if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
371
+ rmIfExists(join(skills, ".skills-index.json"));
372
+ rmIfExists(join(skills, "skills-index.md"));
303
373
  rmIfExists(join(skills, "README.md"));
304
- rmMatchingDirs(join(COP, "agents"), (name) =>
305
- [
306
- "code-reviewer.md",
307
- "explorer.md",
308
- "ios-architect.md",
309
- "android-architect.md",
310
- "backend-architect.md",
311
- "security-auditor.md",
312
- ].includes(name),
313
- );
374
+ // Agent definitions are FILES; remove only the pipeline-shipped names so
375
+ // user-authored agents survive.
376
+ rmMatchingFiles(join(COP, "agents"), (name) => PIPELINE_AGENT_FILES.includes(name));
314
377
  stripManagedBlock(join(COP, "copilot-instructions.md"));
315
378
  }
316
379
 
@@ -370,7 +433,13 @@ async function main() {
370
433
  console.log("");
371
434
  }
372
435
 
373
- main().catch((e) => {
374
- console.error("uninstall failed:", e.message);
375
- process.exit(1);
376
- });
436
+ // Run only when executed directly (node uninstall.mjs). Guard lets tests
437
+ // import the exported constants without triggering the uninstall.
438
+ const isMainModule =
439
+ process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
440
+ if (isMainModule) {
441
+ main().catch((e) => {
442
+ console.error("uninstall failed:", e.message);
443
+ process.exit(1);
444
+ });
445
+ }