@mmerterden/multi-agent-pipeline 11.3.2 → 11.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/README.md +1 -0
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +113 -11
- package/install/copilot.mjs +75 -6
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/review/SKILL.md +36 -4
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +22 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +22 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
- 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 +4 -4
- package/pipeline/multi-agent-refs/readiness-review.md +65 -0
- 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 +7 -7
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- 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/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
- package/pipeline/scripts/smoke-generate-issue.sh +3 -3
- 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-review-readiness.sh +91 -0
- package/pipeline/scripts/smoke-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
- package/pipeline/scripts/smoke-wrapper-preservation.sh +68 -0
- package/pipeline/scripts/test-gap-scan.mjs +12 -1
- package/pipeline/scripts/triage-memory.mjs +10 -1
- package/pipeline/scripts/uninstall.mjs +105 -36
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
- package/pipeline/skills/.skill-manifest.json +13 -5
- package/pipeline/skills/.skills-index.json +20 -2
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +6 -2
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -10
- package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +25 -0
- package/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md +25 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/skills-index.md +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,147 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## [11.5.0] - 2026-07-16
|
|
18
|
+
|
|
19
|
+
Full-project refactor round: 20 verified defect fixes (install lifecycle, shell
|
|
20
|
+
libs, fetchers, gate scripts), an install-time performance overhaul, and new
|
|
21
|
+
supply-chain / validation gates. Driven by a 4-band analysis (best-practices
|
|
22
|
+
research, bug hunt, category scoring, upstream-drift check).
|
|
23
|
+
|
|
24
|
+
### Fixed - data loss and destructive paths
|
|
25
|
+
|
|
26
|
+
- **Install no longer wipes user agent files.** `installAgents` wiped the whole
|
|
27
|
+
`~/.claude/agents` / `~/.copilot/agents` dir on every install/update, deleting
|
|
28
|
+
user-authored subagents. It now removes only pipeline-owned files (list derived
|
|
29
|
+
from `pipeline/agents/` so it cannot go stale).
|
|
30
|
+
- **Plain install after `--link` no longer destroys the dev checkout.** Every
|
|
31
|
+
wipe/copy target now goes through a symlink guard (`ensureRealDir`); a `--link`
|
|
32
|
+
re-install just re-links instead of wiping through the symlink into the repo.
|
|
33
|
+
- **Unguarded `rm -rf` paths locked down.** `shadow-git.sh` validates task ids
|
|
34
|
+
(`prune ..` refused); `multi-repo-pipeline.sh teardown` refuses `/`, `$HOME`,
|
|
35
|
+
the project root, and their ancestors after path resolution.
|
|
36
|
+
- **`update-issue-progress.sh` no longer eats issue bodies.** The Progress-table
|
|
37
|
+
rewrite is now bounded at the legend comment, the next heading, or EOF; bodies
|
|
38
|
+
without the legend comment keep all trailing sections.
|
|
39
|
+
- **Copilot instructions merge is now marker-bounded.** An explicit end marker
|
|
40
|
+
preserves user content appended after the pipeline section across install,
|
|
41
|
+
update, and uninstall (previously sliced to EOF).
|
|
42
|
+
- **Uninstall actually removes what install creates.** Agent files (dir-vs-file
|
|
43
|
+
bug meant none were ever removed), `multi-agent-refs/`, `schemas/`, `lib/`,
|
|
44
|
+
and the compliance core skills are now covered on both targets; token and
|
|
45
|
+
preferences guarantees unchanged.
|
|
46
|
+
|
|
47
|
+
### Fixed - security
|
|
48
|
+
|
|
49
|
+
- **Pre-commit secret-scan hook fired never.** The PreToolUse matcher used
|
|
50
|
+
permission-rule syntax that can never match the tool name; matcher is now
|
|
51
|
+
`Bash` with a fast stdin filter in `pre-commit-check.sh`, and existing installs
|
|
52
|
+
migrate the dead entry in place.
|
|
53
|
+
- **Secrets off argv.** OAuth refresh grants, bearer/PAT headers, Basic auth,
|
|
54
|
+
and keychain writes across nine call sites (fetchers, repo-cache, issue-fetcher,
|
|
55
|
+
figma-mcp-refresh, credential-store, keychain-save) moved to `curl -K <(...)`
|
|
56
|
+
configs and stdin so tokens never appear in `ps`.
|
|
57
|
+
- **Crashlytics service-account key hygiene.** The decoded Firebase SA JSON now
|
|
58
|
+
lives in a `mktemp` file created under `umask 077` with trap cleanup instead of
|
|
59
|
+
a predictable world-readable `/tmp` name.
|
|
60
|
+
- **Telemetry honesty.** The "anonymous" install ping no longer sends
|
|
61
|
+
`githubUser`, `gitEmail`, or `hostname`.
|
|
62
|
+
|
|
63
|
+
### Fixed - correctness
|
|
64
|
+
|
|
65
|
+
- **`fetch-confluence.sh` worked never.** An env-prefix-on-assignment bug meant
|
|
66
|
+
the URL parser always saw empty input; every invocation failed. Parsing now
|
|
67
|
+
goes through argv and the fetcher honors its documented exit codes.
|
|
68
|
+
- **`diff-risk-score.mjs` default invocation** resolved the diff base after
|
|
69
|
+
using it (`null...HEAD` fatal, gate fail-open); base now resolves first.
|
|
70
|
+
- **`evidence-gate.mjs`** no longer flags passing logs containing "0 tests
|
|
71
|
+
failed" as failures.
|
|
72
|
+
- **`grep -c || echo 0` double-output idiom** fixed at ~20 sites via a shared
|
|
73
|
+
`count-lib.sh` helper (bogus `* PR:` bullets in Jira/Confluence bodies, silently
|
|
74
|
+
zeroed convention counters in `extract-conventions.sh`).
|
|
75
|
+
- **False-success family.** `multi-repo-pipeline.sh` no longer prints
|
|
76
|
+
"committed/pushed" on failure; `repo-cache.sh` keeps the previous cache when a
|
|
77
|
+
refresh fails instead of serving an empty file for the TTL; `review-watch.sh
|
|
78
|
+
--watch` survives transient gh failures and its cursor can no longer move
|
|
79
|
+
backwards.
|
|
80
|
+
- **`issue-fetcher.sh`** classifies bare `#316` / `316` correctly (repo picker
|
|
81
|
+
path instead of a malformed `//issues/` URL).
|
|
82
|
+
- **`phase-tracker.sh`** read-modify-write is now lock-protected; parallel
|
|
83
|
+
Phase 4 reviewers no longer lose token/cost deltas.
|
|
84
|
+
- **Shadow-git**: forced excludes now land in `info/exclude` (they were dead
|
|
85
|
+
code, ballooning snapshots with DerivedData), and `restore --files` removes
|
|
86
|
+
files created after the snapshot instead of keeping half the change.
|
|
87
|
+
- **macOS bash 3.2 portability**: `${URL,,}` in `fetch-swagger.sh`, `date -r
|
|
88
|
+
<file>` in `audit-log-rotate.sh` (which also rotates atomically now).
|
|
89
|
+
- **Glob translation** in `triage-memory.mjs` / `test-gap-scan.mjs`: `**` no
|
|
90
|
+
longer breaks after the `*` pass (nested-path filters match again).
|
|
91
|
+
- **`migrate-prefs.mjs`** errors on unknown flags (a `--dry-rnu` typo used to
|
|
92
|
+
perform a real write) and reports the correct target version.
|
|
93
|
+
- Minor hardening: `write-state.mjs` lock release on early exits,
|
|
94
|
+
`post-pr-review.sh` / `plan-todos.sh` null-safe jq, `account-resolver.sh`
|
|
95
|
+
JSON escaping, `scan-skills.sh --json` spurious empty finding removed.
|
|
96
|
+
|
|
97
|
+
### Performance
|
|
98
|
+
|
|
99
|
+
- **Install: ~94s -> ~6s.** `scan-skills.sh` rewritten from per-file grep loops
|
|
100
|
+
to combined single-pass scans (identical findings output, verified by diff);
|
|
101
|
+
`countFiles` no longer shells out to `find | wc -l`.
|
|
102
|
+
|
|
103
|
+
### Added - gates and tooling
|
|
104
|
+
|
|
105
|
+
- **OIDC trusted publishing.** `release.yml` publishes tokenless (no more
|
|
106
|
+
`NPM_TOKEN`); one-time npmjs.com trusted-publisher setup required before the
|
|
107
|
+
next release (see workflow header). Release gates now also run the layout
|
|
108
|
+
smoke, the install leak audit, and the new pack-contents smoke.
|
|
109
|
+
- **New smokes**: `smoke-plugin-validate.sh` (`claude plugin validate --strict`),
|
|
110
|
+
`smoke-workflow-audit.sh` (zizmor + actionlint, offline, skip-if-missing),
|
|
111
|
+
`smoke-pack-contents.sh` (packed-payload deny-list derived from package.json
|
|
112
|
+
negations at runtime + file-count band), `smoke-fetchers-offline.sh`
|
|
113
|
+
(44 offline assertions covering every fetcher's parse/credential/network
|
|
114
|
+
stages - the class of hole that let fetch-confluence ship dead).
|
|
115
|
+
- **`check-md-links.mjs` now covers `pipeline/**`** (700+ skill/command files,
|
|
116
|
+
`$HOME/.claude/multi-agent-refs/` links validated against the repo source).
|
|
117
|
+
- **`lint-skills.mjs` spec-conformance warnings** (agent-skills name pattern,
|
|
118
|
+
routing-clause descriptions, body size budget).
|
|
119
|
+
- **Coverage floor**: c8 `--check-coverage` at the measured baseline (lines 80).
|
|
120
|
+
- **DevEx**: `test:quick` script, `index.js --version`, unknown commands list
|
|
121
|
+
valid ones, smoke-runner env knobs documented in CONTRIBUTING, `cache: npm` +
|
|
122
|
+
strict `npm ci` in all workflows.
|
|
123
|
+
- **Real-module tests**: `test/index.test.mjs` suites now import the actual
|
|
124
|
+
install helpers instead of inlined copies (the dead hook shipped because the
|
|
125
|
+
copy-based tests could not fail); new temp-HOME integration tests for the full
|
|
126
|
+
install -> update -> uninstall lifecycle and the JS gate scripts. Unit tests
|
|
127
|
+
145 -> 197.
|
|
128
|
+
|
|
129
|
+
### Docs
|
|
130
|
+
|
|
131
|
+
- ROADMAP moved to the 11.x line, SECURITY.md supported-versions table updated
|
|
132
|
+
(11.x active), README Scorecard badge, dead `files[]` negations removed,
|
|
133
|
+
internal working notes moved to `docs/internal/`.
|
|
134
|
+
|
|
135
|
+
## [11.4.0] - 2026-07-10
|
|
136
|
+
|
|
137
|
+
Review PR-picker, two readiness-review commands, and durable local-only wrappers.
|
|
138
|
+
|
|
139
|
+
- **`/multi-agent:review` no-URL PR picker.** With no argument (interactive), review
|
|
140
|
+
now lists open GitHub + Bitbucket PRs in one labeled list and multi-selects which
|
|
141
|
+
to review (per-PR inline comments + approve/needs-work), instead of silently
|
|
142
|
+
defaulting to the local branch diff. Autopilot / no open PRs falls back to the
|
|
143
|
+
local diff; explicit URL/`#N`/branch still work. The Copilot twin was brought to
|
|
144
|
+
parity (input-aware + PR posting).
|
|
145
|
+
- **New `/multi-agent:review-jira` + `/multi-agent:review-issue`.** Grade whether a
|
|
146
|
+
Jira issue / GitHub issue is ready for the pipeline (scope, acceptance criteria,
|
|
147
|
+
repro, design reference, API contract, stack signal, dependencies) by reusing the
|
|
148
|
+
Phase 0 maturity engine (`issue-fetcher.sh`) plus a readiness rubric, then post
|
|
149
|
+
the gaps back as a comment on the item (confirmed; autopilot auto-posts). Read-only
|
|
150
|
+
on code: no worktree, no branch, no creation. Command count 36 -> 38.
|
|
151
|
+
- **Corporate alias wrappers survive updates.** `install/claude.mjs` now preserves
|
|
152
|
+
command dirs whose SKILL.md is `local-only: true` across the namespace wipe, so
|
|
153
|
+
personal `multi-agent:<name>` aliases that delegate to a private marketplace plugin
|
|
154
|
+
are no longer destroyed on every install/update (they were, before). Covered by a
|
|
155
|
+
new `smoke-wrapper-preservation.sh`. (The wrappers themselves are local-only and
|
|
156
|
+
never ship in this package.)
|
|
157
|
+
|
|
17
158
|
## [11.3.2] - 2026-07-10
|
|
18
159
|
|
|
19
160
|
Command rename + website refresh.
|
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
|
|
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,14 +5,16 @@
|
|
|
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
|
-
import { existsSync, readFileSync, rmSync } from "fs";
|
|
17
|
+
import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
|
|
16
18
|
import { join } from "path";
|
|
17
19
|
import { execSync } from "child_process";
|
|
18
20
|
|
|
@@ -21,14 +23,23 @@ import {
|
|
|
21
23
|
copyFile,
|
|
22
24
|
countFiles,
|
|
23
25
|
ensureDir,
|
|
26
|
+
ensureRealDir,
|
|
24
27
|
isDryRun,
|
|
25
28
|
pruneLegacyMultiAgentSkills,
|
|
29
|
+
removePipelineAgentFiles,
|
|
26
30
|
wipeDir,
|
|
27
31
|
writeFile,
|
|
28
32
|
} from "./_common.mjs";
|
|
29
33
|
import { copyExternalSkillsFiltered } from "./_platform-filter.mjs";
|
|
30
34
|
import { DEV_ONLY_SCRIPTS } from "./_dev-only-files.mjs";
|
|
31
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Dead matcher written by pre-v11.4.1 installs. Hook matchers only ever see
|
|
38
|
+
* the tool name, so this string never matched and the hook never ran.
|
|
39
|
+
* Kept so installs can migrate it and uninstall can clean it.
|
|
40
|
+
*/
|
|
41
|
+
export const LEGACY_PRE_COMMIT_MATCHER = "Bash(git commit:*)";
|
|
42
|
+
|
|
32
43
|
/**
|
|
33
44
|
* @param {{
|
|
34
45
|
* home: string,
|
|
@@ -72,14 +83,73 @@ function installCommands(pipelineSrc, dest, useSymlinks) {
|
|
|
72
83
|
const commandsSrc = join(pipelineSrc, "commands");
|
|
73
84
|
if (!existsSync(commandsSrc)) return;
|
|
74
85
|
|
|
86
|
+
const ownedCmdDir = join(dest, "multi-agent");
|
|
87
|
+
|
|
88
|
+
if (useSymlinks) {
|
|
89
|
+
// --link (dev) mode: dest becomes a symlink into the repo. Never pre-wipe
|
|
90
|
+
// or restore wrappers through an existing link; both would delete from or
|
|
91
|
+
// write into the developer's repo checkout itself.
|
|
92
|
+
copyDir(commandsSrc, dest, { useSymlinks });
|
|
93
|
+
console.log(` -> ${countFiles(commandsSrc)} files linked at ${dest}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// After a prior --link install, dest (or the owned subdir) is a symlink
|
|
98
|
+
// into the repo. Replace it with a real directory before any wipe/copy so
|
|
99
|
+
// the install can never land inside the repo checkout.
|
|
100
|
+
ensureRealDir(dest);
|
|
101
|
+
ensureRealDir(ownedCmdDir);
|
|
102
|
+
|
|
75
103
|
// Wipe only our namespace dir so files renamed/removed in the source don't
|
|
76
104
|
// linger as ghost slash commands. Other namespaces under
|
|
77
105
|
// `~/.claude/commands/` are untouched.
|
|
78
|
-
|
|
106
|
+
//
|
|
107
|
+
// Preserve local-only alias wrappers (frontmatter `local-only: true`) across
|
|
108
|
+
// the wipe. They live ONLY under ~/.claude (never in pipeline/commands, and
|
|
109
|
+
// never synced), so without this snapshot+restore they would be destroyed on
|
|
110
|
+
// every install/update. Pipeline never ships these names, so restore is safe.
|
|
111
|
+
const preservedWrappers = snapshotLocalOnlyWrappers(ownedCmdDir);
|
|
79
112
|
wipeDir(ownedCmdDir);
|
|
80
113
|
|
|
81
114
|
copyDir(commandsSrc, dest, { useSymlinks });
|
|
115
|
+
const restored = restoreLocalOnlyWrappers(ownedCmdDir, preservedWrappers);
|
|
82
116
|
console.log(` -> ${countFiles(commandsSrc)} files copied to ${dest}`);
|
|
117
|
+
if (restored > 0) {
|
|
118
|
+
console.log(` -> preserved ${restored} local-only alias wrapper(s)`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Snapshot single-level command dirs whose SKILL.md declares `local-only: true`
|
|
123
|
+
// (their files are read into memory) so installCommands can restore them after
|
|
124
|
+
// the namespace wipe.
|
|
125
|
+
function snapshotLocalOnlyWrappers(ownedCmdDir) {
|
|
126
|
+
const out = [];
|
|
127
|
+
if (!existsSync(ownedCmdDir)) return out;
|
|
128
|
+
for (const name of readdirSync(ownedCmdDir)) {
|
|
129
|
+
const dir = join(ownedCmdDir, name);
|
|
130
|
+
const skill = join(dir, "SKILL.md");
|
|
131
|
+
if (!existsSync(skill)) continue;
|
|
132
|
+
if (!/^local-only:\s*true\s*$/m.test(readFileSync(skill, "utf-8"))) continue;
|
|
133
|
+
const files = readdirSync(dir)
|
|
134
|
+
.filter((f) => statSync(join(dir, f)).isFile())
|
|
135
|
+
.map((f) => ({ rel: f, content: readFileSync(join(dir, f), "utf-8") }));
|
|
136
|
+
out.push({ name, files });
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Restore snapshotted local-only wrappers, but never clobber a real command the
|
|
142
|
+
// pipeline just installed under the same name. Returns the count restored.
|
|
143
|
+
function restoreLocalOnlyWrappers(ownedCmdDir, preserved) {
|
|
144
|
+
let n = 0;
|
|
145
|
+
for (const w of preserved) {
|
|
146
|
+
const dir = join(ownedCmdDir, w.name);
|
|
147
|
+
if (existsSync(dir)) continue; // pipeline shipped a real command with this name
|
|
148
|
+
ensureDir(dir);
|
|
149
|
+
for (const f of w.files) writeFileSync(join(dir, f.rel), f.content);
|
|
150
|
+
n++;
|
|
151
|
+
}
|
|
152
|
+
return n;
|
|
83
153
|
}
|
|
84
154
|
|
|
85
155
|
// The pipeline's reference docs (refs/) + internal pickers live OUTSIDE the
|
|
@@ -91,6 +161,7 @@ function installMultiAgentRefs(pipelineSrc, dest, useSymlinks) {
|
|
|
91
161
|
const refsSrc = join(pipelineSrc, "multi-agent-refs");
|
|
92
162
|
if (!existsSync(refsSrc)) return;
|
|
93
163
|
console.log(" [Claude Code] Installing multi-agent refs (non-command)...");
|
|
164
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
94
165
|
wipeDir(dest);
|
|
95
166
|
copyDir(refsSrc, dest, { useSymlinks });
|
|
96
167
|
console.log(` -> ${countFiles(refsSrc)} ref files copied to ${dest}`);
|
|
@@ -103,6 +174,7 @@ function installScripts(pipelineSrc, dest, useSymlinks) {
|
|
|
103
174
|
|
|
104
175
|
// Wipe-before-copy — scripts/ is a 100% pipeline-managed tree, so files
|
|
105
176
|
// removed upstream should not linger.
|
|
177
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
106
178
|
wipeDir(dest);
|
|
107
179
|
copyDir(scriptsSrc, dest, { exclude: DEV_ONLY_SCRIPTS, useSymlinks });
|
|
108
180
|
// Count files actually excluded, not the array length: DEV_ONLY_SCRIPTS mixes
|
|
@@ -119,7 +191,13 @@ function installAgents(pipelineSrc, dest, useSymlinks) {
|
|
|
119
191
|
console.log(" [Claude Code] Installing agent definitions...");
|
|
120
192
|
const agentsSrc = join(pipelineSrc, "agents");
|
|
121
193
|
if (!existsSync(agentsSrc)) return;
|
|
122
|
-
|
|
194
|
+
// ~/.claude/agents also holds user-authored agent files. Never wipe the
|
|
195
|
+
// whole dir; remove only the pipeline-shipped agent files (name set derived
|
|
196
|
+
// from the source tree so it cannot go stale), then copy the fresh set in.
|
|
197
|
+
if (!useSymlinks) {
|
|
198
|
+
ensureRealDir(dest);
|
|
199
|
+
removePipelineAgentFiles(dest, agentsSrc);
|
|
200
|
+
}
|
|
123
201
|
copyDir(agentsSrc, dest, { useSymlinks });
|
|
124
202
|
console.log(` -> ${countFiles(agentsSrc)} files copied to ${dest}`);
|
|
125
203
|
}
|
|
@@ -137,6 +215,7 @@ function installRules(pipelineSrc, dest, useSymlinks) {
|
|
|
137
215
|
if (useSymlinks) {
|
|
138
216
|
copyDir(rulesSrc, dest, { useSymlinks });
|
|
139
217
|
} else {
|
|
218
|
+
ensureRealDir(dest);
|
|
140
219
|
copyDir(rulesSrc, dest, { skipExisting: true });
|
|
141
220
|
}
|
|
142
221
|
console.log(` -> rules present (existing local rules preserved)`);
|
|
@@ -148,6 +227,7 @@ function installSchemas(pipelineSrc, dest, useSymlinks) {
|
|
|
148
227
|
if (!existsSync(schemasSrc)) return;
|
|
149
228
|
// Wipe-before-copy — schemas/ is a 100% pipeline-managed tree, so files
|
|
150
229
|
// removed upstream should not linger.
|
|
230
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
151
231
|
wipeDir(dest);
|
|
152
232
|
copyDir(schemasSrc, dest, { useSymlinks });
|
|
153
233
|
console.log(` -> ${countFiles(schemasSrc)} files copied to ${dest}`);
|
|
@@ -158,6 +238,7 @@ function installLib(pipelineSrc, dest, useSymlinks) {
|
|
|
158
238
|
const libSrc = join(pipelineSrc, "lib");
|
|
159
239
|
if (!existsSync(libSrc)) return;
|
|
160
240
|
// Wipe-before-copy — lib/ is a 100% pipeline-managed tree.
|
|
241
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
161
242
|
wipeDir(dest);
|
|
162
243
|
copyDir(libSrc, dest, { useSymlinks });
|
|
163
244
|
console.log(` -> ${countFiles(libSrc)} files copied to ${dest}`);
|
|
@@ -207,6 +288,10 @@ function installSkills(opts) {
|
|
|
207
288
|
const { pipelineSrc, dest, indexOnly, useSymlinks, platformFlag } = opts;
|
|
208
289
|
console.log(" [Claude Code] Installing skills...");
|
|
209
290
|
|
|
291
|
+
// Same symlink guard as the other owned trees: never prune or copy through
|
|
292
|
+
// a --link-era symlink into the repo checkout.
|
|
293
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
294
|
+
|
|
210
295
|
if (indexOnly) {
|
|
211
296
|
ensureDir(dest);
|
|
212
297
|
const indexJson = join(pipelineSrc, "skills", ".skills-index.json");
|
|
@@ -312,7 +397,7 @@ function ensurePreferences(prefsPath, pipelineSrc) {
|
|
|
312
397
|
}
|
|
313
398
|
}
|
|
314
399
|
|
|
315
|
-
function configureSettings(home) {
|
|
400
|
+
export function configureSettings(home) {
|
|
316
401
|
console.log(" [Claude Code] Configuring hooks + context management...");
|
|
317
402
|
const SETTINGS_PATH = join(home, ".claude", "settings.json");
|
|
318
403
|
try {
|
|
@@ -330,12 +415,29 @@ function configureSettings(home) {
|
|
|
330
415
|
|
|
331
416
|
if (!settings.hooks) settings.hooks = {};
|
|
332
417
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
418
|
+
// PreToolUse matchers are regexes over the TOOL NAME only, so the correct
|
|
419
|
+
// matcher is "Bash". The pre-v11.4.1 entry used "Bash(git commit:*)",
|
|
420
|
+
// which never matches any tool name - the secret gate never fired.
|
|
421
|
+
// Migrate that dead entry in place instead of stacking a duplicate.
|
|
422
|
+
// Command-level filtering (only scan on actual `git commit`) happens
|
|
423
|
+
// inside pre-commit-check.sh via the hook's stdin JSON.
|
|
424
|
+
const isPreCommitEntry = (h) =>
|
|
425
|
+
Array.isArray(h.hooks) &&
|
|
426
|
+
h.hooks.some(
|
|
427
|
+
(sub) => typeof sub.command === "string" && sub.command.includes("pre-commit-check.sh"),
|
|
428
|
+
);
|
|
429
|
+
let hasPreCommitHook = false;
|
|
430
|
+
for (const h of settings.hooks.PreToolUse) {
|
|
431
|
+
if (!isPreCommitEntry(h)) continue;
|
|
432
|
+
if (h.matcher === LEGACY_PRE_COMMIT_MATCHER) {
|
|
433
|
+
h.matcher = "Bash";
|
|
434
|
+
settingsChanged = true;
|
|
435
|
+
}
|
|
436
|
+
if (h.matcher === "Bash") hasPreCommitHook = true;
|
|
437
|
+
}
|
|
336
438
|
if (!hasPreCommitHook) {
|
|
337
439
|
settings.hooks.PreToolUse.push({
|
|
338
|
-
matcher: "Bash
|
|
440
|
+
matcher: "Bash",
|
|
339
441
|
hooks: [
|
|
340
442
|
{
|
|
341
443
|
type: "command",
|