@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
package/CHANGELOG.md CHANGED
@@ -14,6 +14,178 @@ Internal file-layout changes that don't affect the slash-command surface are sti
14
14
 
15
15
  ---
16
16
 
17
+ ## [12.0.0] - 2026-07-20
18
+
19
+ Command rename (breaking), a full-cleanup uninstall mode, outputLanguage-aware
20
+ picker descriptions, worktree residue cleanup, and cleanup-command hardening.
21
+
22
+ ### Breaking
23
+
24
+ - **`/multi-agent:delete` renamed to `/multi-agent:uninstall`.** The old name is
25
+ gone (no alias). The Copilot twin is now `multi-agent-uninstall`. Command
26
+ count stays 38; the canonical inventory, sync list, help, and cross-cli
27
+ contract were updated. Per the versioning policy a renamed command forces a
28
+ major bump.
29
+
30
+ ### Added
31
+
32
+ - **`uninstall --all-data` full-cleanup mode.** Standard uninstall still keeps
33
+ settings + logs; `--all-data` additionally removes
34
+ `multi-agent-preferences.json` and the entire `~/.claude/logs/multi-agent/`
35
+ tree (task logs, state, metrics, audit). Tokens, `CLAUDE.md`, and the
36
+ user-owned `rules/` are preserved in both modes. The slash command asks a
37
+ scope picker (Standard vs Full cleanup) before the double confirmation.
38
+ - **outputLanguage-aware slash-command descriptions.** Every command ships an
39
+ English `description` plus a `description-tr`; when `outputLanguage=tr`, the
40
+ installer and the `/multi-agent:language` toggle swap the installed
41
+ `~/.claude` descriptions to Turkish (reversible via a `description-en`
42
+ sidecar). Repo and npm files stay English; `/multi-agent:sync` normalizes and
43
+ gates the round trip so localized text can never leak back into the repo.
44
+ New `localize-commands.mjs` + `smoke-description-tr.sh`.
45
+ - **`/multi-agent:garbage-collect` worktree phase.** New `gc-worktrees.sh`
46
+ (dry-run by default) prunes stale worktree admin entries, removes orphan
47
+ `.worktrees/*` dirs no longer registered as worktrees, unstages gitlink
48
+ ("Subproject commit") residue from a pre-guard `git add -A`, and ensures the
49
+ `.worktrees/` `info/exclude` guard. Registered worktrees are never touched.
50
+ - **Worktree residue guard on creation.** Phase 0 (single- and multi-repo) and
51
+ the Copilot core skill now append `.worktrees/` to the clone-local
52
+ `.git/info/exclude` before every `worktree add`, so a blanket `git add -A`
53
+ can no longer record `.worktrees/{id}` gitlinks into shared branches.
54
+ - **update-check reaches npx-only installs.** The installer stamps
55
+ `~/.claude/.pipeline-version` (and the Copilot equivalent); `update-check.sh`
56
+ falls back to it when no repo clone is present, so users who never cloned the
57
+ repo still see the "update available" prompt. Uninstall removes the marker.
58
+
59
+ ### Changed / hardened
60
+
61
+ - **`/multi-agent:purge` now runs a real backend** (`purge.sh`): dry-run by
62
+ default, every `rm` target resolved strictly inside `<repo>/.worktrees/`,
63
+ refuses `/`, `$HOME`, and the project root; main worktree and
64
+ main/master/develop always preserved. `--delete-remote` gates remote-branch
65
+ deletion behind `--yes`.
66
+ - **`prune-logs.sh` / `gc-tmp.sh` hardened**: safe id/path validation, the
67
+ audit trail + metrics corpus never removed, containment guards on deletion
68
+ roots. New/expanded smokes: `smoke-purge.sh`, plus prune-logs and gc-tmp
69
+ coverage.
70
+
71
+ ## [11.5.0] - 2026-07-16
72
+
73
+ Full-project refactor round: 20 verified defect fixes (install lifecycle, shell
74
+ libs, fetchers, gate scripts), an install-time performance overhaul, and new
75
+ supply-chain / validation gates. Driven by a 4-band analysis (best-practices
76
+ research, bug hunt, category scoring, upstream-drift check).
77
+
78
+ ### Fixed - data loss and destructive paths
79
+
80
+ - **Install no longer wipes user agent files.** `installAgents` wiped the whole
81
+ `~/.claude/agents` / `~/.copilot/agents` dir on every install/update, deleting
82
+ user-authored subagents. It now removes only pipeline-owned files (list derived
83
+ from `pipeline/agents/` so it cannot go stale).
84
+ - **Plain install after `--link` no longer destroys the dev checkout.** Every
85
+ wipe/copy target now goes through a symlink guard (`ensureRealDir`); a `--link`
86
+ re-install just re-links instead of wiping through the symlink into the repo.
87
+ - **Unguarded `rm -rf` paths locked down.** `shadow-git.sh` validates task ids
88
+ (`prune ..` refused); `multi-repo-pipeline.sh teardown` refuses `/`, `$HOME`,
89
+ the project root, and their ancestors after path resolution.
90
+ - **`update-issue-progress.sh` no longer eats issue bodies.** The Progress-table
91
+ rewrite is now bounded at the legend comment, the next heading, or EOF; bodies
92
+ without the legend comment keep all trailing sections.
93
+ - **Copilot instructions merge is now marker-bounded.** An explicit end marker
94
+ preserves user content appended after the pipeline section across install,
95
+ update, and uninstall (previously sliced to EOF).
96
+ - **Uninstall actually removes what install creates.** Agent files (dir-vs-file
97
+ bug meant none were ever removed), `multi-agent-refs/`, `schemas/`, `lib/`,
98
+ and the compliance core skills are now covered on both targets; token and
99
+ preferences guarantees unchanged.
100
+
101
+ ### Fixed - security
102
+
103
+ - **Pre-commit secret-scan hook fired never.** The PreToolUse matcher used
104
+ permission-rule syntax that can never match the tool name; matcher is now
105
+ `Bash` with a fast stdin filter in `pre-commit-check.sh`, and existing installs
106
+ migrate the dead entry in place.
107
+ - **Secrets off argv.** OAuth refresh grants, bearer/PAT headers, Basic auth,
108
+ and keychain writes across nine call sites (fetchers, repo-cache, issue-fetcher,
109
+ figma-mcp-refresh, credential-store, keychain-save) moved to `curl -K <(...)`
110
+ configs and stdin so tokens never appear in `ps`.
111
+ - **Crashlytics service-account key hygiene.** The decoded Firebase SA JSON now
112
+ lives in a `mktemp` file created under `umask 077` with trap cleanup instead of
113
+ a predictable world-readable `/tmp` name.
114
+ - **Telemetry honesty.** The "anonymous" install ping no longer sends
115
+ `githubUser`, `gitEmail`, or `hostname`.
116
+
117
+ ### Fixed - correctness
118
+
119
+ - **`fetch-confluence.sh` worked never.** An env-prefix-on-assignment bug meant
120
+ the URL parser always saw empty input; every invocation failed. Parsing now
121
+ goes through argv and the fetcher honors its documented exit codes.
122
+ - **`diff-risk-score.mjs` default invocation** resolved the diff base after
123
+ using it (`null...HEAD` fatal, gate fail-open); base now resolves first.
124
+ - **`evidence-gate.mjs`** no longer flags passing logs containing "0 tests
125
+ failed" as failures.
126
+ - **`grep -c || echo 0` double-output idiom** fixed at ~20 sites via a shared
127
+ `count-lib.sh` helper (bogus `* PR:` bullets in Jira/Confluence bodies, silently
128
+ zeroed convention counters in `extract-conventions.sh`).
129
+ - **False-success family.** `multi-repo-pipeline.sh` no longer prints
130
+ "committed/pushed" on failure; `repo-cache.sh` keeps the previous cache when a
131
+ refresh fails instead of serving an empty file for the TTL; `review-watch.sh
132
+ --watch` survives transient gh failures and its cursor can no longer move
133
+ backwards.
134
+ - **`issue-fetcher.sh`** classifies bare `#316` / `316` correctly (repo picker
135
+ path instead of a malformed `//issues/` URL).
136
+ - **`phase-tracker.sh`** read-modify-write is now lock-protected; parallel
137
+ Phase 4 reviewers no longer lose token/cost deltas.
138
+ - **Shadow-git**: forced excludes now land in `info/exclude` (they were dead
139
+ code, ballooning snapshots with DerivedData), and `restore --files` removes
140
+ files created after the snapshot instead of keeping half the change.
141
+ - **macOS bash 3.2 portability**: `${URL,,}` in `fetch-swagger.sh`, `date -r
142
+ <file>` in `audit-log-rotate.sh` (which also rotates atomically now).
143
+ - **Glob translation** in `triage-memory.mjs` / `test-gap-scan.mjs`: `**` no
144
+ longer breaks after the `*` pass (nested-path filters match again).
145
+ - **`migrate-prefs.mjs`** errors on unknown flags (a `--dry-rnu` typo used to
146
+ perform a real write) and reports the correct target version.
147
+ - Minor hardening: `write-state.mjs` lock release on early exits,
148
+ `post-pr-review.sh` / `plan-todos.sh` null-safe jq, `account-resolver.sh`
149
+ JSON escaping, `scan-skills.sh --json` spurious empty finding removed.
150
+
151
+ ### Performance
152
+
153
+ - **Install: ~94s -> ~6s.** `scan-skills.sh` rewritten from per-file grep loops
154
+ to combined single-pass scans (identical findings output, verified by diff);
155
+ `countFiles` no longer shells out to `find | wc -l`.
156
+
157
+ ### Added - gates and tooling
158
+
159
+ - **OIDC trusted publishing.** `release.yml` publishes tokenless (no more
160
+ `NPM_TOKEN`); one-time npmjs.com trusted-publisher setup required before the
161
+ next release (see workflow header). Release gates now also run the layout
162
+ smoke, the install leak audit, and the new pack-contents smoke.
163
+ - **New smokes**: `smoke-plugin-validate.sh` (`claude plugin validate --strict`),
164
+ `smoke-workflow-audit.sh` (zizmor + actionlint, offline, skip-if-missing),
165
+ `smoke-pack-contents.sh` (packed-payload deny-list derived from package.json
166
+ negations at runtime + file-count band), `smoke-fetchers-offline.sh`
167
+ (44 offline assertions covering every fetcher's parse/credential/network
168
+ stages - the class of hole that let fetch-confluence ship dead).
169
+ - **`check-md-links.mjs` now covers `pipeline/**`** (700+ skill/command files,
170
+ `$HOME/.claude/multi-agent-refs/` links validated against the repo source).
171
+ - **`lint-skills.mjs` spec-conformance warnings** (agent-skills name pattern,
172
+ routing-clause descriptions, body size budget).
173
+ - **Coverage floor**: c8 `--check-coverage` at the measured baseline (lines 80).
174
+ - **DevEx**: `test:quick` script, `index.js --version`, unknown commands list
175
+ valid ones, smoke-runner env knobs documented in CONTRIBUTING, `cache: npm` +
176
+ strict `npm ci` in all workflows.
177
+ - **Real-module tests**: `test/index.test.mjs` suites now import the actual
178
+ install helpers instead of inlined copies (the dead hook shipped because the
179
+ copy-based tests could not fail); new temp-HOME integration tests for the full
180
+ install -> update -> uninstall lifecycle and the JS gate scripts. Unit tests
181
+ 145 -> 197.
182
+
183
+ ### Docs
184
+
185
+ - ROADMAP moved to the 11.x line, SECURITY.md supported-versions table updated
186
+ (11.x active), README Scorecard badge, dead `files[]` negations removed,
187
+ internal working notes moved to `docs/internal/`.
188
+
17
189
  ## [11.4.0] - 2026-07-10
18
190
 
19
191
  Review PR-picker, two readiness-review commands, and durable local-only wrappers.
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-18%20%7C%2020%20%7C%2022-green)](https://nodejs.org)
6
6
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://github.com/mmerterden/multi-agent-pipeline/blob/main/package.json)
7
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/mmerterden/multi-agent-pipeline/badge)](https://scorecard.dev/viewer/?uri=github.com/mmerterden/multi-agent-pipeline)
7
8
 
8
9
  An 8-phase AI development pipeline for **Claude Code** and **Copilot CLI**. Drives a Jira issue or GitHub URL to a merged PR in one command — analysis → plan → TDD → review → test → commit → PR — with multi-repo orchestration, a plan-approval gate, CLI-aware parallel review, and store-compliance checks. Component and Figma-to-code work is dispatched to the per-stack marketplace plugins (iOS/SwiftUI, Android/Compose) rather than bundled, so component skills live in one place.
9
10
 
@@ -64,7 +64,7 @@ Each adapter exports a `{ name, install, uninstall }` contract. New adapters are
64
64
 
65
65
  ### Token-preserving uninstall
66
66
 
67
- Every install path has a documented opposite via `npx @mmerterden/multi-agent-pipeline uninstall` (or `delete` alias). Slash command: `/multi-agent:delete` with a Copilot CLI peer `multi-agent-delete`.
67
+ Every install path has a documented opposite via `npx @mmerterden/multi-agent-pipeline uninstall`. Slash command: `/multi-agent:uninstall` (renamed from `:delete` in v12.0.0) with a Copilot CLI peer `multi-agent-uninstall`. A `--all-data` flag extends it to also clear pipeline settings and the log root; tokens are never touched.
68
68
 
69
69
  The uninstaller is **contractually forbidden** from touching:
70
70
 
@@ -94,7 +94,7 @@ Positive:
94
94
  Negative / costs:
95
95
 
96
96
  - 6 adapters means 6 places where the format mapping can drift from upstream tool changes. Mitigation: round-trip smoke tests for each adapter exercise install + uninstall on every PR.
97
- - `--all-tools` writes ~900 files (174 cursor + ~50 windsurf concat + 173 cline + 161 copilot-chat + 161 continue + 1 zed) into the user's repo. Acceptable `.gitignore` patterns already cover most of these (`.cursor/`, `.continue/`, `.clinerules/`), and `:delete` cleans them all in one step.
97
+ - `--all-tools` writes ~900 files (174 cursor + ~50 windsurf concat + 173 cline + 161 copilot-chat + 161 continue + 1 zed) into the user's repo. Acceptable: `.gitignore` patterns already cover most of these (`.cursor/`, `.continue/`, `.clinerules/`), and `:uninstall` cleans them all in one step.
98
98
  - Per-tool format drift across tool versions (e.g. Cursor's `.mdc` format changes in 2027) requires per-adapter maintenance. Insulated from the orchestrator: bumping a Tier 2/3 tool format never touches Phase 1-7 logic.
99
99
 
100
100
  ## Alternatives Considered
package/index.js CHANGED
@@ -12,6 +12,7 @@
12
12
  import { pathToFileURL } from "url";
13
13
  import { join, dirname } from "path";
14
14
  import { fileURLToPath } from "url";
15
+ import { readFileSync } from "fs";
15
16
 
16
17
  /** @type {string} */
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -29,7 +30,10 @@ if (noColor) {
29
30
  process.env.NO_COLOR = "1";
30
31
  }
31
32
 
32
- if (!command || command === "install") {
33
+ if (command === "--version" || command === "-v" || command === "version") {
34
+ const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
35
+ console.log(pkg.version);
36
+ } else if (!command || command === "install") {
33
37
  await import(pathToFileURL(join(__dirname, "install.js")).href);
34
38
  } else if (command === "uninstall" || command === "delete") {
35
39
  await import(pathToFileURL(join(__dirname, "pipeline", "scripts", "uninstall.mjs")).href);
@@ -52,6 +56,7 @@ if (!command || command === "install") {
52
56
 
53
57
  Help:
54
58
  npx @mmerterden/multi-agent-pipeline help
59
+ npx @mmerterden/multi-agent-pipeline --version Print the installed version
55
60
 
56
61
  Options:
57
62
  --no-color Disable colored output
@@ -65,6 +70,8 @@ if (!command || command === "install") {
65
70
  More info: https://github.com/mmerterden/multi-agent-pipeline
66
71
  `);
67
72
  } else {
68
- console.error(`Unknown command: ${command}. Run with 'help' for usage.`);
73
+ console.error(`Unknown command: ${command}`);
74
+ console.error("Valid commands: install, uninstall, delete, help, version (--version/-v)");
75
+ console.error("Run with 'help' for usage.");
69
76
  process.exit(1);
70
77
  }
@@ -16,12 +16,12 @@ import {
16
16
  mkdirSync,
17
17
  readdirSync,
18
18
  rmSync,
19
+ statSync,
19
20
  symlinkSync,
20
21
  unlinkSync,
21
22
  writeFileSync,
22
23
  } from "fs";
23
24
  import { dirname } from "path";
24
- import { execSync } from "child_process";
25
25
  import { join } from "path";
26
26
 
27
27
  let dryRun = false;
@@ -51,6 +51,85 @@ export function ensureDir(dir) {
51
51
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
52
52
  }
53
53
 
54
+ /**
55
+ * Replace a symlinked path with a real (empty) directory before installing
56
+ * into it. After a `--link` install, an owned dir (or a shared parent like
57
+ * `~/.claude/commands`) can be a symlink into the developer's repo checkout;
58
+ * wiping or copying through it would land inside the repo and destroy the
59
+ * pipeline sources. Non-symlink paths are left untouched.
60
+ *
61
+ * @param {string} dir
62
+ * @returns {boolean} true when a symlink was replaced
63
+ */
64
+ export function ensureRealDir(dir) {
65
+ let st;
66
+ try {
67
+ st = lstatSync(dir);
68
+ } catch {
69
+ return false;
70
+ }
71
+ if (!st.isSymbolicLink()) return false;
72
+ if (dryRun) {
73
+ console.log(` [dry-run] would replace symlink ${dir} with a real directory`);
74
+ return true;
75
+ }
76
+ unlinkSync(dir);
77
+ mkdirSync(dir, { recursive: true });
78
+ console.log(` -> replaced symlink ${dir} with a real directory (was --link mode)`);
79
+ return true;
80
+ }
81
+
82
+ /**
83
+ * File names of the agent definitions the pipeline ships, derived from the
84
+ * source tree so the list can never go stale. Includes legacy names shipped
85
+ * by older releases so their stale installed copies are still cleaned up.
86
+ *
87
+ * @param {string} agentsSrc - pipeline/agents source directory
88
+ * @returns {Set<string>}
89
+ */
90
+ export function pipelineAgentFileNames(agentsSrc) {
91
+ const names = new Set();
92
+ if (existsSync(agentsSrc)) {
93
+ for (const entry of readdirSync(agentsSrc, { withFileTypes: true })) {
94
+ if (entry.isFile()) names.add(entry.name);
95
+ }
96
+ }
97
+ // Legacy agent file names from older releases go here when an agent is
98
+ // renamed or removed upstream (currently none beyond the shipped set).
99
+ return names;
100
+ }
101
+
102
+ /**
103
+ * Remove ONLY pipeline-owned agent files from an installed agents dir.
104
+ * The dir also holds user-authored agent definitions, so it must never be
105
+ * wiped wholesale (that destroyed user files pre-v11.4.1).
106
+ *
107
+ * @param {string} destDir - installed agents dir (~/.claude/agents etc.)
108
+ * @param {string} agentsSrc - pipeline/agents source directory
109
+ * @returns {number} count of files removed
110
+ */
111
+ export function removePipelineAgentFiles(destDir, agentsSrc) {
112
+ if (!existsSync(destDir)) return 0;
113
+ const owned = pipelineAgentFileNames(agentsSrc);
114
+ let removed = 0;
115
+ for (const entry of readdirSync(destDir, { withFileTypes: true })) {
116
+ if (!entry.isFile()) continue;
117
+ if (!owned.has(entry.name)) continue;
118
+ if (dryRun) {
119
+ console.log(` [dry-run] would remove ${join(destDir, entry.name)}`);
120
+ removed++;
121
+ continue;
122
+ }
123
+ try {
124
+ rmSync(join(destDir, entry.name), { force: true });
125
+ removed++;
126
+ } catch {
127
+ /* non-fatal */
128
+ }
129
+ }
130
+ return removed;
131
+ }
132
+
54
133
  /**
55
134
  * Recursively copy a directory (or symlink it in `--link` mode).
56
135
  *
@@ -85,7 +164,13 @@ export function copyDir(src, dest, opts = {}) {
85
164
  recursive: true,
86
165
  force: true,
87
166
  filter: (source) => {
88
- const rel = source.slice(src.length).replace(/^\//, "");
167
+ // Normalize to forward slashes so exclude patterns (written posix-style)
168
+ // also match on win32, where cpSync hands back backslash paths.
169
+ const rel = source
170
+ .slice(src.length)
171
+ .split("\\")
172
+ .join("/")
173
+ .replace(/^\//, "");
89
174
  return !exclude.some((pat) =>
90
175
  typeof pat === "string"
91
176
  ? rel === pat || rel.startsWith(pat + "/")
@@ -187,17 +272,34 @@ export function writeFile(path, content) {
187
272
  }
188
273
 
189
274
  /**
190
- * Count files in a directory tree.
275
+ * Count files in a directory tree (pure fs recursion - works on win32 where
276
+ * `find` is unavailable, and avoids a subprocess per call). A plain-file path
277
+ * counts as 1, matching the old `find <path> -type f` behavior relied on by
278
+ * the DEV_ONLY_SCRIPTS exclusion accounting.
191
279
  * @param {string} dir
192
280
  * @returns {number}
193
281
  */
194
282
  export function countFiles(dir) {
283
+ let st;
284
+ try {
285
+ st = statSync(dir);
286
+ } catch {
287
+ return 0;
288
+ }
289
+ if (st.isFile()) return 1;
290
+ if (!st.isDirectory()) return 0;
291
+ let count = 0;
292
+ let entries;
195
293
  try {
196
- const out = execSync(`find "${dir}" -type f | wc -l`, { encoding: "utf-8" });
197
- return parseInt(out.trim(), 10);
294
+ entries = readdirSync(dir, { withFileTypes: true });
198
295
  } catch {
199
296
  return 0;
200
297
  }
298
+ for (const entry of entries) {
299
+ if (entry.isDirectory()) count += countFiles(join(dir, entry.name));
300
+ else if (entry.isFile()) count++;
301
+ }
302
+ return count;
201
303
  }
202
304
 
203
305
  /**
@@ -4,29 +4,17 @@
4
4
  * Set `MULTI_AGENT_TELEMETRY=1` to enable. Nothing is sent otherwise.
5
5
  * Fire-and-forget; never throws and never delays the install.
6
6
  *
7
+ * The payload is genuinely anonymous: package name, version, install
8
+ * method, flags, platform/arch, and Node version. No usernames, emails,
9
+ * hostnames, or other machine/user identifiers are collected.
10
+ *
7
11
  * @module install/_telemetry
8
12
  */
9
13
 
10
- import { execSync } from "child_process";
11
14
  import { readFileSync } from "fs";
12
- import { hostname, platform, arch } from "os";
15
+ import { platform, arch } from "os";
13
16
  import { join } from "path";
14
17
 
15
- /**
16
- * Run a shell command silently and return its trimmed stdout, or undefined on error.
17
- * @param {string} cmd
18
- * @returns {string|undefined}
19
- */
20
- function safe(cmd) {
21
- try {
22
- return execSync(cmd, { stdio: ["ignore", "pipe", "ignore"], timeout: 3000 })
23
- .toString()
24
- .trim();
25
- } catch {
26
- return undefined;
27
- }
28
- }
29
-
30
18
  /**
31
19
  * @param {{ packageRoot: string, flags: string[] }} ctx
32
20
  * @returns {Promise<void>}
@@ -40,9 +28,6 @@ export async function sendInstallTelemetry(ctx) {
40
28
  else if (process.env.npm_config_global === "true") method = "global";
41
29
  else if (process.env.npm_lifecycle_event === "install") method = "npm-install";
42
30
 
43
- const githubUser = safe("gh api user --jq .login");
44
- const gitEmail = safe("git config user.email");
45
-
46
31
  let version = "unknown";
47
32
  try {
48
33
  const pkg = JSON.parse(readFileSync(join(ctx.packageRoot, "package.json"), "utf-8"));
@@ -56,9 +41,6 @@ export async function sendInstallTelemetry(ctx) {
56
41
  version,
57
42
  method,
58
43
  flags: ctx.flags.filter((f) => f.startsWith("--")),
59
- githubUser,
60
- gitEmail,
61
- hostname: hostname(),
62
44
  platform: `${platform()} ${arch()}`,
63
45
  nodeVersion: process.version,
64
46
  };
@@ -5,30 +5,43 @@
5
5
  * `CLAUDE.md` template, preferences template, and the secret-detection
6
6
  * pre-commit hook in `settings.json`.
7
7
  *
8
- * Behaviour MUST stay byte-equivalent to the pre-v8.0.0 monolithic install
9
- * path — install layout smoke (`smoke-install-layout.sh`) compares before/after
10
- * trees on every release.
8
+ * Layout MUST stay equivalent to the pre-v8.0.0 monolithic install path -
9
+ * install layout smoke (`smoke-install-layout.sh`) compares before/after
10
+ * trees on every release. (v11.4.1 intentionally changed two behaviours
11
+ * without changing the layout: agents are cleaned per-file instead of wiping
12
+ * the dir, and symlinked install targets are replaced before writing.)
11
13
  *
12
14
  * @module install/claude
13
15
  */
14
16
 
15
17
  import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
16
- import { join } from "path";
18
+ import { join, dirname } from "path";
17
19
  import { execSync } from "child_process";
18
20
 
21
+ import { localizeCommands } from "../pipeline/scripts/localize-commands.mjs";
22
+
19
23
  import {
20
24
  copyDir,
21
25
  copyFile,
22
26
  countFiles,
23
27
  ensureDir,
28
+ ensureRealDir,
24
29
  isDryRun,
25
30
  pruneLegacyMultiAgentSkills,
31
+ removePipelineAgentFiles,
26
32
  wipeDir,
27
33
  writeFile,
28
34
  } from "./_common.mjs";
29
35
  import { copyExternalSkillsFiltered } from "./_platform-filter.mjs";
30
36
  import { DEV_ONLY_SCRIPTS } from "./_dev-only-files.mjs";
31
37
 
38
+ /**
39
+ * Dead matcher written by pre-v11.4.1 installs. Hook matchers only ever see
40
+ * the tool name, so this string never matched and the hook never ran.
41
+ * Kept so installs can migrate it and uninstall can clean it.
42
+ */
43
+ export const LEGACY_PRE_COMMIT_MATCHER = "Bash(git commit:*)";
44
+
32
45
  /**
33
46
  * @param {{
34
47
  * home: string,
@@ -72,10 +85,27 @@ function installCommands(pipelineSrc, dest, useSymlinks) {
72
85
  const commandsSrc = join(pipelineSrc, "commands");
73
86
  if (!existsSync(commandsSrc)) return;
74
87
 
88
+ const ownedCmdDir = join(dest, "multi-agent");
89
+
90
+ if (useSymlinks) {
91
+ // --link (dev) mode: dest becomes a symlink into the repo. Never pre-wipe
92
+ // or restore wrappers through an existing link; both would delete from or
93
+ // write into the developer's repo checkout itself.
94
+ copyDir(commandsSrc, dest, { useSymlinks });
95
+ console.log(` -> ${countFiles(commandsSrc)} files linked at ${dest}`);
96
+ return;
97
+ }
98
+
99
+ // After a prior --link install, dest (or the owned subdir) is a symlink
100
+ // into the repo. Replace it with a real directory before any wipe/copy so
101
+ // the install can never land inside the repo checkout.
102
+ ensureRealDir(dest);
103
+ ensureRealDir(ownedCmdDir);
104
+
75
105
  // Wipe only our namespace dir so files renamed/removed in the source don't
76
106
  // linger as ghost slash commands. Other namespaces under
77
107
  // `~/.claude/commands/` are untouched.
78
- const ownedCmdDir = join(dest, "multi-agent");
108
+ //
79
109
  // Preserve local-only alias wrappers (frontmatter `local-only: true`) across
80
110
  // the wipe. They live ONLY under ~/.claude (never in pipeline/commands, and
81
111
  // never synced), so without this snapshot+restore they would be destroyed on
@@ -89,6 +119,28 @@ function installCommands(pipelineSrc, dest, useSymlinks) {
89
119
  if (restored > 0) {
90
120
  console.log(` -> preserved ${restored} local-only alias wrapper(s)`);
91
121
  }
122
+ localizeCommandDescriptions(pipelineSrc, ownedCmdDir);
123
+ }
124
+
125
+ // Picker descriptions follow prefs.global.outputLanguage: files shipping a
126
+ // description-tr frontmatter line get their description swapped in the
127
+ // INSTALLED tree only (repo and npm package stay English; /multi-agent:sync
128
+ // restores English before mirroring back). Failure is non-fatal.
129
+ function localizeCommandDescriptions(pipelineSrc, ownedCmdDir) {
130
+ if (isDryRun()) return;
131
+ try {
132
+ const home = dirname(dirname(ownedCmdDir));
133
+ const prefsPath = join(home, "multi-agent-preferences.json");
134
+ if (!existsSync(prefsPath)) return;
135
+ const prefs = JSON.parse(readFileSync(prefsPath, "utf-8"));
136
+ if (prefs?.global?.outputLanguage !== "tr") return;
137
+ const changed = localizeCommands(ownedCmdDir, "apply");
138
+ if (changed > 0) {
139
+ console.log(` -> localized ${changed} command description(s) to outputLanguage=tr`);
140
+ }
141
+ } catch {
142
+ // advisory only - installed tree simply stays English
143
+ }
92
144
  }
93
145
 
94
146
  // Snapshot single-level command dirs whose SKILL.md declares `local-only: true`
@@ -133,6 +185,7 @@ function installMultiAgentRefs(pipelineSrc, dest, useSymlinks) {
133
185
  const refsSrc = join(pipelineSrc, "multi-agent-refs");
134
186
  if (!existsSync(refsSrc)) return;
135
187
  console.log(" [Claude Code] Installing multi-agent refs (non-command)...");
188
+ if (!useSymlinks) ensureRealDir(dest);
136
189
  wipeDir(dest);
137
190
  copyDir(refsSrc, dest, { useSymlinks });
138
191
  console.log(` -> ${countFiles(refsSrc)} ref files copied to ${dest}`);
@@ -145,6 +198,7 @@ function installScripts(pipelineSrc, dest, useSymlinks) {
145
198
 
146
199
  // Wipe-before-copy — scripts/ is a 100% pipeline-managed tree, so files
147
200
  // removed upstream should not linger.
201
+ if (!useSymlinks) ensureRealDir(dest);
148
202
  wipeDir(dest);
149
203
  copyDir(scriptsSrc, dest, { exclude: DEV_ONLY_SCRIPTS, useSymlinks });
150
204
  // Count files actually excluded, not the array length: DEV_ONLY_SCRIPTS mixes
@@ -161,7 +215,13 @@ function installAgents(pipelineSrc, dest, useSymlinks) {
161
215
  console.log(" [Claude Code] Installing agent definitions...");
162
216
  const agentsSrc = join(pipelineSrc, "agents");
163
217
  if (!existsSync(agentsSrc)) return;
164
- wipeDir(dest);
218
+ // ~/.claude/agents also holds user-authored agent files. Never wipe the
219
+ // whole dir; remove only the pipeline-shipped agent files (name set derived
220
+ // from the source tree so it cannot go stale), then copy the fresh set in.
221
+ if (!useSymlinks) {
222
+ ensureRealDir(dest);
223
+ removePipelineAgentFiles(dest, agentsSrc);
224
+ }
165
225
  copyDir(agentsSrc, dest, { useSymlinks });
166
226
  console.log(` -> ${countFiles(agentsSrc)} files copied to ${dest}`);
167
227
  }
@@ -179,6 +239,7 @@ function installRules(pipelineSrc, dest, useSymlinks) {
179
239
  if (useSymlinks) {
180
240
  copyDir(rulesSrc, dest, { useSymlinks });
181
241
  } else {
242
+ ensureRealDir(dest);
182
243
  copyDir(rulesSrc, dest, { skipExisting: true });
183
244
  }
184
245
  console.log(` -> rules present (existing local rules preserved)`);
@@ -190,6 +251,7 @@ function installSchemas(pipelineSrc, dest, useSymlinks) {
190
251
  if (!existsSync(schemasSrc)) return;
191
252
  // Wipe-before-copy — schemas/ is a 100% pipeline-managed tree, so files
192
253
  // removed upstream should not linger.
254
+ if (!useSymlinks) ensureRealDir(dest);
193
255
  wipeDir(dest);
194
256
  copyDir(schemasSrc, dest, { useSymlinks });
195
257
  console.log(` -> ${countFiles(schemasSrc)} files copied to ${dest}`);
@@ -200,6 +262,7 @@ function installLib(pipelineSrc, dest, useSymlinks) {
200
262
  const libSrc = join(pipelineSrc, "lib");
201
263
  if (!existsSync(libSrc)) return;
202
264
  // Wipe-before-copy — lib/ is a 100% pipeline-managed tree.
265
+ if (!useSymlinks) ensureRealDir(dest);
203
266
  wipeDir(dest);
204
267
  copyDir(libSrc, dest, { useSymlinks });
205
268
  console.log(` -> ${countFiles(libSrc)} files copied to ${dest}`);
@@ -249,6 +312,10 @@ function installSkills(opts) {
249
312
  const { pipelineSrc, dest, indexOnly, useSymlinks, platformFlag } = opts;
250
313
  console.log(" [Claude Code] Installing skills...");
251
314
 
315
+ // Same symlink guard as the other owned trees: never prune or copy through
316
+ // a --link-era symlink into the repo checkout.
317
+ if (!useSymlinks) ensureRealDir(dest);
318
+
252
319
  if (indexOnly) {
253
320
  ensureDir(dest);
254
321
  const indexJson = join(pipelineSrc, "skills", ".skills-index.json");
@@ -354,7 +421,7 @@ function ensurePreferences(prefsPath, pipelineSrc) {
354
421
  }
355
422
  }
356
423
 
357
- function configureSettings(home) {
424
+ export function configureSettings(home) {
358
425
  console.log(" [Claude Code] Configuring hooks + context management...");
359
426
  const SETTINGS_PATH = join(home, ".claude", "settings.json");
360
427
  try {
@@ -372,12 +439,29 @@ function configureSettings(home) {
372
439
 
373
440
  if (!settings.hooks) settings.hooks = {};
374
441
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
375
- const hasPreCommitHook = settings.hooks.PreToolUse.some(
376
- (h) => h.matcher === "Bash(git commit:*)",
377
- );
442
+ // PreToolUse matchers are regexes over the TOOL NAME only, so the correct
443
+ // matcher is "Bash". The pre-v11.4.1 entry used "Bash(git commit:*)",
444
+ // which never matches any tool name - the secret gate never fired.
445
+ // Migrate that dead entry in place instead of stacking a duplicate.
446
+ // Command-level filtering (only scan on actual `git commit`) happens
447
+ // inside pre-commit-check.sh via the hook's stdin JSON.
448
+ const isPreCommitEntry = (h) =>
449
+ Array.isArray(h.hooks) &&
450
+ h.hooks.some(
451
+ (sub) => typeof sub.command === "string" && sub.command.includes("pre-commit-check.sh"),
452
+ );
453
+ let hasPreCommitHook = false;
454
+ for (const h of settings.hooks.PreToolUse) {
455
+ if (!isPreCommitEntry(h)) continue;
456
+ if (h.matcher === LEGACY_PRE_COMMIT_MATCHER) {
457
+ h.matcher = "Bash";
458
+ settingsChanged = true;
459
+ }
460
+ if (h.matcher === "Bash") hasPreCommitHook = true;
461
+ }
378
462
  if (!hasPreCommitHook) {
379
463
  settings.hooks.PreToolUse.push({
380
- matcher: "Bash(git commit:*)",
464
+ matcher: "Bash",
381
465
  hooks: [
382
466
  {
383
467
  type: "command",