@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,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
@@ -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
 
@@ -12,30 +12,63 @@
12
12
  * node uninstall.mjs # interactive, removes from all installed targets
13
13
  * node uninstall.mjs --yes # skip prompts, remove from all
14
14
  * node uninstall.mjs --claude # only Claude Code
15
+ * node uninstall.mjs --all-data # ALSO remove pipeline settings + logs/state/metrics
15
16
  * node uninstall.mjs --cursor --target=/path/to/repo # legacy adapter-file cleanup
16
17
  * node uninstall.mjs --dry-run # report what would be removed, change nothing
17
18
  *
18
19
  * Targeted by:
19
- * - `/multi-agent:delete` slash command (Claude Code)
20
- * - `multi-agent-delete` skill (Copilot CLI)
20
+ * - `/multi-agent:uninstall` slash command (Claude Code)
21
+ * - `multi-agent-uninstall` skill (Copilot CLI)
21
22
  * - `npx @mmerterden/multi-agent-pipeline uninstall` (direct CLI)
22
23
  *
23
- * Always preserved:
24
- * - $HOME/.claude/CLAUDE.md (user customizations)
25
- * - $HOME/.claude/multi-agent-preferences.json
24
+ * Always preserved (both modes):
26
25
  * - Personal access tokens in the OS credential store
26
+ * - $HOME/.claude/CLAUDE.md (user customizations)
27
+ * - $HOME/.claude/rules/ (user-owned; installed write-if-missing)
27
28
  * - User content outside the `<!-- multi-agent-pipeline:begin/end -->` markers
28
29
  * in shared rules files (copilot-instructions.md)
29
30
  *
31
+ * Preserved by default, removed with --all-data:
32
+ * - $HOME/.claude/multi-agent-preferences.json (pipeline settings)
33
+ * - $HOME/.claude/logs/multi-agent/ (task logs, state, metrics, audit trail)
34
+ *
30
35
  * @module pipeline/scripts/runtime/uninstall
31
36
  */
32
37
 
33
38
  import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
34
39
  import { join } from "path";
40
+ import { pathToFileURL } from "url";
35
41
  import { createInterface } from "readline";
36
42
 
37
43
  const HOME = process.env.HOME || process.env.USERPROFILE;
38
44
 
45
+ /**
46
+ * Agent definition files the pipeline ships (pipeline/agents/). This runs
47
+ * standalone from ~/.claude/scripts, so the list is static here; a unit test
48
+ * (test/uninstall.test.mjs) asserts it matches the source tree so it cannot
49
+ * go stale. Only these file names are removed from the installed agents
50
+ * dirs - user-authored agent files are never touched.
51
+ */
52
+ export const PIPELINE_AGENT_FILES = [
53
+ "android-architect.md",
54
+ "backend-architect.md",
55
+ "code-reviewer.md",
56
+ "dev-critic.md",
57
+ "explorer.md",
58
+ "ios-architect.md",
59
+ "security-auditor.md",
60
+ "task-clarifier.md",
61
+ ];
62
+
63
+ /**
64
+ * Non-multi-agent core skills the installer ships from
65
+ * pipeline/skills/shared/core/ (the multi-agent* dirs are matched by prefix).
66
+ */
67
+ export const PIPELINE_CORE_SKILL_DIRS = [
68
+ "apple-archive-compliance",
69
+ "google-play-compliance",
70
+ ];
71
+
39
72
  const flags = process.argv.slice(2).filter((a) => a !== "uninstall");
40
73
 
41
74
  const TOOL_FLAGS = [
@@ -58,6 +91,9 @@ const forCodex = flags.includes("--codex") || flags.includes("--all") || !explic
58
91
 
59
92
  const dryRun = flags.includes("--dry-run");
60
93
  const skipPrompt = flags.includes("--yes") || flags.includes("-y");
94
+ // Full cleanup: also remove pipeline settings + the multi-agent log root
95
+ // (task logs, state, metrics, audit). Tokens, CLAUDE.md and rules/ stay.
96
+ const allData = flags.includes("--all-data");
61
97
 
62
98
  const adapterTarget = (() => {
63
99
  const raw = flags.find((f) => f.startsWith("--target="));
@@ -122,6 +158,33 @@ function rmMatchingFiles(parent, predicate) {
122
158
  return count;
123
159
  }
124
160
 
161
+ // Explicit terminator install/copilot.mjs writes after the pipeline section
162
+ // (v11.4.1+). Must stay in sync with INSTRUCTIONS_END_MARKER there.
163
+ const COPILOT_END_MARKER = "<!-- multi-agent-pipeline:copilot-instructions:end -->";
164
+
165
+ /**
166
+ * Legacy copilot-instructions files (written before the end marker existed)
167
+ * have no explicit terminator. Bound the pipeline span at the next top-level
168
+ * "# " heading after the start marker when one exists OUTSIDE fenced code
169
+ * blocks (the pipeline body carries bash comments like "# Bootstrap once ..."
170
+ * inside fences); otherwise the span runs to EOF (pre-marker behavior).
171
+ * Mirrors install/copilot.mjs legacyTrailingContent.
172
+ * @param {string} section - content from the start marker onward
173
+ * @returns {string} user content trailing the pipeline span ("" if none)
174
+ */
175
+ function legacyTrailingContent(section) {
176
+ const lines = section.split("\n");
177
+ let inFence = false;
178
+ for (let i = 1; i < lines.length; i++) {
179
+ if (/^\s*(```|~~~)/.test(lines[i])) {
180
+ inFence = !inFence;
181
+ continue;
182
+ }
183
+ if (!inFence && /^# /.test(lines[i])) return lines.slice(i).join("\n");
184
+ }
185
+ return "";
186
+ }
187
+
125
188
  /**
126
189
  * Strip a marker-wrapped `<!-- multi-agent-pipeline:begin/end -->` block from
127
190
  * a user-owned file. Preserves everything outside the markers. Deletes the
@@ -135,25 +198,33 @@ function stripManagedBlock(filePath) {
135
198
  /\s*<!-- multi-agent-pipeline:begin -->[\s\S]*?<!-- multi-agent-pipeline:end -->\s*/m;
136
199
  if (!re.test(content)) {
137
200
  // 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).
201
+ // begin/end markers. v11.4.1+ writes an explicit end marker after the
202
+ // pipeline section; strip only the start..end span so user content BELOW
203
+ // the section survives. Legacy files without the end marker are bounded
204
+ // at the next top-level heading outside code fences, else EOF.
143
205
  if (content.includes("# Multi-Agent Development Pipeline")) {
144
- // Heuristic: Copilot's generateCopilotInstructions output. Only strip the
145
- // pipeline section, not the whole file.
146
206
  const marker = "# Multi-Agent Development Pipeline";
147
207
  const idx = content.indexOf(marker);
148
208
  if (idx >= 0) {
149
209
  const before = content.slice(0, idx).trimEnd();
210
+ const fromStart = content.slice(idx);
211
+ const endIdx = fromStart.indexOf(COPILOT_END_MARKER);
212
+ const rawTrailing =
213
+ endIdx >= 0
214
+ ? fromStart.slice(endIdx + COPILOT_END_MARKER.length)
215
+ : legacyTrailingContent(fromStart);
216
+ const trailing = rawTrailing.replace(/^[\r\n]+/, "").trimEnd();
217
+ let remaining = before;
218
+ if (trailing.length > 0) {
219
+ remaining = before.length > 0 ? before + "\n\n" + trailing : trailing;
220
+ }
150
221
  if (dryRun) {
151
222
  report("would update", filePath);
152
- } else if (before.length === 0) {
223
+ } else if (remaining.length === 0) {
153
224
  rmSync(filePath, { force: true });
154
225
  console.log(` removed (was pipeline-only): ${filePath}`);
155
226
  } else {
156
- writeFileSync(filePath, before + "\n");
227
+ writeFileSync(filePath, remaining + "\n");
157
228
  console.log(` cleaned pipeline section: ${filePath}`);
158
229
  }
159
230
  return true;
@@ -200,9 +271,12 @@ function cleanClaudeSettings(settingsPath) {
200
271
 
201
272
  if (settings.hooks?.PreToolUse?.length) {
202
273
  const before = settings.hooks.PreToolUse.length;
274
+ // Match both the current matcher ("Bash") and the dead pre-v11.4.1 one
275
+ // ("Bash(git commit:*)"); the pre-commit-check.sh command identifies the
276
+ // entry as ours either way.
203
277
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
204
278
  (h) =>
205
- !(h.matcher === "Bash(git commit:*)" &&
279
+ !((h.matcher === "Bash" || h.matcher === "Bash(git commit:*)") &&
206
280
  h.hooks?.some((sub) => sub.command?.includes("pre-commit-check.sh"))),
207
281
  );
208
282
  if (settings.hooks.PreToolUse.length < before) touched = true;
@@ -239,18 +313,24 @@ async function main() {
239
313
  if (dryRun) console.log(" [DRY-RUN] Nothing will be modified.");
240
314
  console.log("");
241
315
  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)");
316
+ if (forClaude) console.log(" - Claude Code (~/.claude/{commands/multi-agent, scripts, multi-agent-refs, schemas, lib} + pipeline skills + pipeline agent files)");
317
+ if (forCopilot) console.log(" - Copilot CLI (~/.copilot/{scripts, schemas, lib} + pipeline skills + pipeline agent files + copilot-instructions.md section)");
244
318
  if (forCursor) console.log(` - Cursor (${adapterTarget}/.cursor/rules/multi-agent-* + .cursorrules)`);
245
319
  if (forCopilotChat) console.log(` - GitHub Copilot Chat (${adapterTarget}/.github/copilot-instructions.md block + .github/instructions/multi-agent-*)`);
246
320
  if (forAntigravity) console.log(` - Antigravity (${adapterTarget}/.agent/rules + .agent/workflows + AGENTS.md block + .agent/mcp_config.json)`);
247
321
  if (forCodex) console.log(" - OpenAI Codex CLI (~/.codex/prompts/multi-agent.md + AGENTS.md block + config.toml mcp block)");
248
322
  console.log("");
323
+ if (allData) {
324
+ console.log(" FULL CLEANUP (--all-data) also removes:");
325
+ console.log(" - ~/.claude/multi-agent-preferences.json (pipeline settings)");
326
+ console.log(" - ~/.claude/logs/multi-agent/ (task logs, state, metrics, audit trail)");
327
+ console.log("");
328
+ }
249
329
  console.log(" PRESERVED (never touched):");
250
330
  console.log(" - Personal access tokens in OS credential store (Keychain / Credential Manager / libsecret)");
251
331
  console.log(" - ~/.claude/CLAUDE.md (your customizations)");
252
332
  console.log(" - ~/.claude/rules/ (user-owned; installed write-if-missing, never overwritten)");
253
- console.log(" - ~/.claude/multi-agent-preferences.json (your settings)");
333
+ if (!allData) console.log(" - ~/.claude/multi-agent-preferences.json (your settings)");
254
334
  console.log(" - Your own content in copilot-instructions.md above the pipeline section");
255
335
  console.log("");
256
336
 
@@ -265,6 +345,11 @@ async function main() {
265
345
  const CLAUDE = join(HOME, ".claude");
266
346
  rmIfExists(join(CLAUDE, "commands", "multi-agent"));
267
347
  rmIfExists(join(CLAUDE, "scripts"));
348
+ // Pipeline-managed trees the installer lays down alongside scripts/.
349
+ rmIfExists(join(CLAUDE, "multi-agent-refs"));
350
+ rmIfExists(join(CLAUDE, "schemas"));
351
+ rmIfExists(join(CLAUDE, "lib"));
352
+ rmIfExists(join(CLAUDE, ".pipeline-version"));
268
353
  // NOTE: ~/.claude/rules/ is USER-OWNED. install lays baseline rules down
269
354
  // write-if-missing and NEVER overwrites them (install/claude.mjs
270
355
  // installRules, skipExisting) so users can evolve their global rules
@@ -274,20 +359,14 @@ async function main() {
274
359
  const skills = join(CLAUDE, "skills");
275
360
  let n = rmMatchingDirs(skills, (name) => name === "multi-agent" || name.startsWith("multi-agent-"));
276
361
  n += rmMatchingDirs(skills, (name) => name === "figma-ios" || name === "figma-android" || name === "figma-common" || name === "figma-to-component");
362
+ n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
277
363
  if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
278
364
  rmIfExists(join(skills, ".skills-index.json"));
279
365
  rmIfExists(join(skills, "skills-index.md"));
280
366
  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
- );
367
+ // Agent definitions are FILES; remove only the pipeline-shipped names so
368
+ // user-authored agents survive.
369
+ rmMatchingFiles(join(CLAUDE, "agents"), (name) => PIPELINE_AGENT_FILES.includes(name));
291
370
  cleanClaudeSettings(join(CLAUDE, "settings.json"));
292
371
  }
293
372
 
@@ -296,24 +375,34 @@ async function main() {
296
375
  console.log(" [Copilot CLI] Removing...");
297
376
  const COP = join(HOME, ".copilot");
298
377
  rmIfExists(join(COP, "scripts"));
378
+ // Pipeline-managed trees the installer lays down alongside scripts/.
379
+ rmIfExists(join(COP, "schemas"));
380
+ rmIfExists(join(COP, "lib"));
381
+ rmIfExists(join(COP, ".pipeline-version"));
299
382
  const skills = join(COP, "skills");
300
383
  let n = rmMatchingDirs(skills, (name) => name === "multi-agent" || name.startsWith("multi-agent-"));
301
384
  n += rmMatchingDirs(skills, (name) => name === "figma-ios" || name === "figma-android" || name === "figma-common" || name === "figma-to-component");
385
+ n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
302
386
  if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
387
+ rmIfExists(join(skills, ".skills-index.json"));
388
+ rmIfExists(join(skills, "skills-index.md"));
303
389
  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
- );
390
+ // Agent definitions are FILES; remove only the pipeline-shipped names so
391
+ // user-authored agents survive.
392
+ rmMatchingFiles(join(COP, "agents"), (name) => PIPELINE_AGENT_FILES.includes(name));
314
393
  stripManagedBlock(join(COP, "copilot-instructions.md"));
315
394
  }
316
395
 
396
+ // Full cleanup: pipeline settings + the multi-agent log root. Both are
397
+ // pipeline-generated data (not user-authored like CLAUDE.md / rules), so
398
+ // --all-data removes them; the default run leaves them in place.
399
+ if (allData && HOME) {
400
+ console.log("");
401
+ console.log(" [Full cleanup] Removing pipeline settings + logs...");
402
+ rmIfExists(join(HOME, ".claude", "multi-agent-preferences.json"));
403
+ rmIfExists(join(HOME, ".claude", "logs", "multi-agent"));
404
+ }
405
+
317
406
  // Legacy adapter-file cleanup (adapters removed in v10.7.0). These blocks
318
407
  // delete files a pre-v10.7.0 install generated; they never touch user files
319
408
  // outside the multi-agent-* namespace / managed markers.
@@ -360,6 +449,15 @@ async function main() {
360
449
 
361
450
  console.log("");
362
451
  console.log(" ✓ Pipeline removed.");
452
+ if (!allData) {
453
+ console.log("");
454
+ console.log(" Kept your pipeline settings + logs. To remove those too, re-run with --all-data.");
455
+ } else {
456
+ console.log("");
457
+ console.log(" Full cleanup done. Still on disk (user-owned, remove by hand if you want a bare system):");
458
+ console.log(" - ~/.claude/CLAUDE.md");
459
+ console.log(" - ~/.claude/rules/");
460
+ }
363
461
  console.log("");
364
462
  console.log(" Personal access tokens were NOT touched. Inspect with:");
365
463
  console.log(" macOS: security find-generic-password -s \"<service>\" -w");
@@ -370,7 +468,13 @@ async function main() {
370
468
  console.log("");
371
469
  }
372
470
 
373
- main().catch((e) => {
374
- console.error("uninstall failed:", e.message);
375
- process.exit(1);
376
- });
471
+ // Run only when executed directly (node uninstall.mjs). Guard lets tests
472
+ // import the exported constants without triggering the uninstall.
473
+ const isMainModule =
474
+ process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
475
+ if (isMainModule) {
476
+ main().catch((e) => {
477
+ console.error("uninstall failed:", e.message);
478
+ process.exit(1);
479
+ });
480
+ }
@@ -46,6 +46,15 @@ if [ -z "$LOCAL_VERSION" ]; then
46
46
  fi
47
47
  done
48
48
  fi
49
+ # npx-only installs have no repo clone; the installer stamps the version here.
50
+ if [ -z "$LOCAL_VERSION" ]; then
51
+ for marker in "$HOME/.claude/.pipeline-version" "$HOME/.copilot/.pipeline-version"; do
52
+ if [ -f "$marker" ]; then
53
+ LOCAL_VERSION=$(head -1 "$marker" 2>/dev/null | tr -d '[:space:]')
54
+ [ -n "$LOCAL_VERSION" ] && break
55
+ fi
56
+ done
57
+ fi
49
58
  [ -z "$LOCAL_VERSION" ] && exit 0 # cannot determine local version -> silent no-op
50
59
 
51
60
  now=$(date +%s)
@@ -29,12 +29,63 @@
29
29
 
30
30
  set -euo pipefail
31
31
 
32
+ # Rewrite the `### Progress` block of the body file ($2) with $1 and print the
33
+ # result to stdout. The replaced region is bounded at the FIRST of:
34
+ # - a line starting with `<!--` (the legend comment, printed and kept), or
35
+ # - the next heading line (^#, printed and kept), or
36
+ # - EOF.
37
+ # Bounding at the next heading matters: bodies without the legend comment used
38
+ # to have everything from the table to EOF silently dropped and persisted.
39
+ rewrite_progress_body() {
40
+ # Block passed via environment: BSD awk rejects literal newlines in -v
41
+ # assignments ("newline in string"), so -v cannot carry the table.
42
+ NEW_BLOCK_ENV="$1" awk '
43
+ BEGIN { new_block = ENVIRON["NEW_BLOCK_ENV"] }
44
+ BEGIN { in_progress = 0 }
45
+ /^### Progress/ {
46
+ print
47
+ print ""
48
+ print new_block
49
+ print ""
50
+ in_progress = 1
51
+ next
52
+ }
53
+ in_progress && /^<!--/ { in_progress = 0; print; next }
54
+ in_progress && /^#/ { in_progress = 0; print; next }
55
+ in_progress { next }
56
+ { print }
57
+ ' "$2"
58
+ }
59
+
60
+ build_progress_block() {
61
+ cat <<EOF
62
+ | Flag | State |
63
+ |---|---|
64
+ | Implementation | ${1} |
65
+ | Testing | ${2} |
66
+ | Code Connect | ${3} |
67
+ | Wiki | ${4} |
68
+ EOF
69
+ }
70
+
32
71
  TASK_ID="${1:-}"
33
72
  if [ -z "$TASK_ID" ]; then
34
73
  echo "usage: update-issue-progress.sh <task-id>" >&2
35
74
  exit 64
36
75
  fi
37
76
 
77
+ # Internal test seam: `--rewrite <body-file>` runs only the Progress-block
78
+ # rewrite (placeholder glyphs) and prints the result. No state, no gh.
79
+ if [ "$TASK_ID" = "--rewrite" ]; then
80
+ BODY_IN="${2:-}"
81
+ if [ -z "$BODY_IN" ] || [ ! -f "$BODY_IN" ]; then
82
+ echo "usage: update-issue-progress.sh --rewrite <body-file>" >&2
83
+ exit 64
84
+ fi
85
+ rewrite_progress_body "$(build_progress_block "⚪" "⚪" "⚪" "⚪")" "$BODY_IN"
86
+ exit 0
87
+ fi
88
+
38
89
  # Resolve the project state directory (matches phase-tracker convention).
39
90
  STATE_GLOB="$HOME/.claude/projects/*"
40
91
  AGENT_STATE=""
@@ -75,9 +126,9 @@ F_WIKI=$(jq -r '.flags.wiki // false' "$AGENT_STATE")
75
126
  # Each flag may be: true | false | "yellow" (validated against older impl).
76
127
  glyph_for() {
77
128
  case "$1" in
78
- true|"true") echo "🟢" ;;
79
- yellow|"yellow") echo "🟡" ;;
80
- *) echo "⚪" ;;
129
+ true) echo "🟢" ;;
130
+ yellow) echo "🟡" ;;
131
+ *) echo "⚪" ;;
81
132
  esac
82
133
  }
83
134
 
@@ -87,49 +138,17 @@ G_CC=$(glyph_for "$F_CC")
87
138
  G_WIKI=$(glyph_for "$F_WIKI")
88
139
 
89
140
  # Fetch current body, replace the Progress table, write back.
90
- TMP_BODY=$(mktemp /tmp/issue-progress-${TASK_ID}-XXXXXX.md)
141
+ TMP_BODY=$(mktemp /tmp/issue-progress-"${TASK_ID}"-XXXXXX.md)
91
142
  trap 'rm -f "$TMP_BODY"' EXIT
92
143
 
93
144
  gh issue view "$ISSUE_NUM" --repo "$ORG_REPO" --json body --jq .body > "$TMP_BODY"
94
145
 
95
146
  # Build the new Progress block.
96
- NEW_BLOCK=$(cat <<EOF
97
- | Flag | State |
98
- |---|---|
99
- | Implementation | ${G_IMPL} |
100
- | Testing | ${G_TEST} |
101
- | Code Connect | ${G_CC} |
102
- | Wiki | ${G_WIKI} |
103
- EOF
104
- )
105
-
106
- # Replace the table between `### Progress` and the next `<!--` HTML comment.
107
- # Use awk to keep the legend comment in place.
108
- awk -v new_block="$NEW_BLOCK" '
109
- BEGIN { in_progress = 0; replaced = 0 }
110
- /^### Progress/ {
111
- print
112
- print ""
113
- print new_block
114
- print ""
115
- in_progress = 1
116
- replaced = 1
117
- next
118
- }
119
- in_progress && /^<!--/ {
120
- in_progress = 0
121
- print
122
- next
123
- }
124
- in_progress { next }
125
- { print }
126
- END {
127
- if (replaced == 0) {
128
- # Body had no Progress section; nothing to replace.
129
- exit 0
130
- }
131
- }
132
- ' "$TMP_BODY" > "${TMP_BODY}.new"
147
+ NEW_BLOCK=$(build_progress_block "$G_IMPL" "$G_TEST" "$G_CC" "$G_WIKI")
148
+
149
+ # Replace the table between `### Progress` and the first terminator (legend
150
+ # comment, next heading, or EOF) - see rewrite_progress_body above.
151
+ rewrite_progress_body "$NEW_BLOCK" "$TMP_BODY" > "${TMP_BODY}.new"
133
152
 
134
153
  # Check if body actually changed (idempotent no-op).
135
154
  if cmp -s "$TMP_BODY" "${TMP_BODY}.new"; then
@@ -130,6 +130,10 @@ function isLockStale(lockPath, staleMs) {
130
130
  }
131
131
  }
132
132
 
133
+ // Path whose lock this process currently holds (null when none). Lets the
134
+ // top-level error handler release our lock on unexpected failures.
135
+ let heldLockFor = null;
136
+
133
137
  /** @param {string} path */
134
138
  function releaseLock(path) {
135
139
  const lockPath = `${path}.lock`;
@@ -138,6 +142,7 @@ function releaseLock(path) {
138
142
  } catch {
139
143
  /* already gone - fine */
140
144
  }
145
+ if (heldLockFor === path) heldLockFor = null;
141
146
  }
142
147
 
143
148
  /**
@@ -178,6 +183,7 @@ async function main() {
178
183
 
179
184
  try {
180
185
  await acquireLock(path);
186
+ heldLockFor = path;
181
187
  } catch (err) {
182
188
  if (err.code === "LOCK_TIMEOUT") {
183
189
  console.error(err.message);
@@ -187,6 +193,10 @@ async function main() {
187
193
  process.exit(3);
188
194
  }
189
195
 
196
+ // process.exit() inside a try block skips `finally`, so every exit path in
197
+ // here must release OUR lock explicitly before exiting - otherwise a corrupt
198
+ // state file (or any I/O error) leaks agent-state.json.lock and blocks the
199
+ // next writer until the stale-lock window elapses.
190
200
  try {
191
201
  let next;
192
202
  if (replace) {
@@ -198,8 +208,7 @@ async function main() {
198
208
  try {
199
209
  current = JSON.parse(readFileSync(path, "utf-8"));
200
210
  } catch (err) {
201
- console.error(`write-state: existing state unreadable - ${err.message}`);
202
- process.exit(3);
211
+ throw new Error(`existing state unreadable - ${err.message}`, { cause: err });
203
212
  }
204
213
  }
205
214
  next = deepMerge(current, payload);
@@ -210,15 +219,16 @@ async function main() {
210
219
  renameSync(tmp, path); // atomic on POSIX
211
220
  } catch (err) {
212
221
  console.error(`write-state: ${err.message}`);
213
- process.exit(3);
214
- } finally {
215
222
  releaseLock(path);
223
+ process.exit(3);
216
224
  }
217
225
 
226
+ releaseLock(path);
218
227
  process.exit(0);
219
228
  }
220
229
 
221
230
  main().catch((err) => {
222
231
  console.error(`write-state: unhandled - ${err.message}`);
232
+ if (heldLockFor) releaseLock(heldLockFor);
223
233
  process.exit(3);
224
234
  });