@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.
- package/CHANGELOG.md +172 -0
- package/README.md +1 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +95 -11
- package/install/copilot.mjs +75 -6
- package/install/index.mjs +34 -1
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +2 -1
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/finish/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +29 -3
- package/pipeline/commands/multi-agent/help/SKILL.md +3 -2
- package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/language/SKILL.md +20 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/log/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/purge/SKILL.md +27 -24
- package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/search/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/stack/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/status/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +20 -2
- package/pipeline/commands/multi-agent/test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +87 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -0
- package/pipeline/lib/account-resolver.sh +61 -23
- package/pipeline/lib/channels-multi-repo.sh +7 -2
- package/pipeline/lib/count-lib.sh +27 -0
- package/pipeline/lib/credential-store.sh +23 -6
- package/pipeline/lib/extract-conventions.sh +27 -21
- package/pipeline/lib/fetch-confluence.sh +25 -13
- package/pipeline/lib/fetch-crashlytics.sh +30 -8
- package/pipeline/lib/fetch-fortify.sh +11 -2
- package/pipeline/lib/fetch-swagger.sh +18 -7
- package/pipeline/lib/figma-mcp-refresh.sh +26 -11
- package/pipeline/lib/figma-screenshot.sh +11 -2
- package/pipeline/lib/issue-fetcher.sh +31 -6
- package/pipeline/lib/multi-repo-pipeline.sh +84 -20
- package/pipeline/lib/plan-todos.sh +12 -3
- package/pipeline/lib/post-pr-review.sh +5 -3
- package/pipeline/lib/repo-cache.sh +53 -9
- package/pipeline/lib/review-watch.sh +26 -6
- package/pipeline/lib/shadow-git.sh +45 -4
- package/pipeline/lib/vercel-deploy.sh +10 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +12 -1
- package/pipeline/scripts/audit-log-rotate.sh +38 -10
- package/pipeline/scripts/check-md-links.mjs +34 -9
- package/pipeline/scripts/diff-risk-score.mjs +4 -1
- package/pipeline/scripts/evidence-gate.mjs +10 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +6 -4
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/gc-tmp.sh +67 -8
- package/pipeline/scripts/gc-worktrees.sh +140 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- package/pipeline/scripts/localize-commands.mjs +130 -0
- package/pipeline/scripts/migrate-prefs.mjs +26 -7
- package/pipeline/scripts/phase-tracker.sh +71 -0
- package/pipeline/scripts/pre-commit-check.sh +39 -0
- package/pipeline/scripts/prune-logs.sh +100 -15
- package/pipeline/scripts/purge.sh +235 -0
- package/pipeline/scripts/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-description-tr.sh +82 -0
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
- package/pipeline/scripts/smoke-gc-tmp.sh +50 -4
- package/pipeline/scripts/smoke-gc-worktrees.sh +125 -0
- package/pipeline/scripts/smoke-install-layout.sh +11 -3
- package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
- package/pipeline/scripts/smoke-pack-contents.sh +140 -0
- package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
- package/pipeline/scripts/smoke-prune-logs.sh +57 -7
- package/pipeline/scripts/smoke-purge.sh +138 -0
- package/pipeline/scripts/smoke-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- package/pipeline/scripts/smoke-update-check.sh +13 -0
- package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
- package/pipeline/scripts/test-gap-scan.mjs +12 -1
- package/pipeline/scripts/triage-memory.mjs +10 -1
- package/pipeline/scripts/uninstall.mjs +146 -42
- package/pipeline/scripts/update-check.sh +9 -0
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
- package/pipeline/skills/.skill-manifest.json +7 -7
- package/pipeline/skills/.skills-index.json +9 -9
- package/pipeline/skills/shared/README.md +19 -10
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +74 -0
- package/pipeline/skills/skills-index.md +1 -1
- package/pipeline/commands/multi-agent/delete/SKILL.md +0 -67
- 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
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
[](https://github.com/mmerterden/multi-agent-pipeline/blob/main/package.json)
|
|
7
|
+
[](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
|
|
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
|
|
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 (
|
|
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}
|
|
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
|
}
|
package/install/_common.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/install/_telemetry.mjs
CHANGED
|
@@ -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 {
|
|
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
|
};
|
package/install/claude.mjs
CHANGED
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
|
464
|
+
matcher: "Bash",
|
|
381
465
|
hooks: [
|
|
382
466
|
{
|
|
383
467
|
type: "command",
|