@mmerterden/multi-agent-pipeline 11.4.0 → 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 +118 -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 +70 -10
- package/install/copilot.mjs +75 -6
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- 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/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 +4 -4
- 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-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-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/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/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,124 @@ 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
|
+
|
|
17
135
|
## [11.4.0] - 2026-07-10
|
|
18
136
|
|
|
19
137
|
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
|
|
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,9 +5,11 @@
|
|
|
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
|
*/
|
|
@@ -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,10 +83,27 @@ 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
|
+
//
|
|
79
107
|
// Preserve local-only alias wrappers (frontmatter `local-only: true`) across
|
|
80
108
|
// the wipe. They live ONLY under ~/.claude (never in pipeline/commands, and
|
|
81
109
|
// never synced), so without this snapshot+restore they would be destroyed on
|
|
@@ -133,6 +161,7 @@ function installMultiAgentRefs(pipelineSrc, dest, useSymlinks) {
|
|
|
133
161
|
const refsSrc = join(pipelineSrc, "multi-agent-refs");
|
|
134
162
|
if (!existsSync(refsSrc)) return;
|
|
135
163
|
console.log(" [Claude Code] Installing multi-agent refs (non-command)...");
|
|
164
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
136
165
|
wipeDir(dest);
|
|
137
166
|
copyDir(refsSrc, dest, { useSymlinks });
|
|
138
167
|
console.log(` -> ${countFiles(refsSrc)} ref files copied to ${dest}`);
|
|
@@ -145,6 +174,7 @@ function installScripts(pipelineSrc, dest, useSymlinks) {
|
|
|
145
174
|
|
|
146
175
|
// Wipe-before-copy — scripts/ is a 100% pipeline-managed tree, so files
|
|
147
176
|
// removed upstream should not linger.
|
|
177
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
148
178
|
wipeDir(dest);
|
|
149
179
|
copyDir(scriptsSrc, dest, { exclude: DEV_ONLY_SCRIPTS, useSymlinks });
|
|
150
180
|
// Count files actually excluded, not the array length: DEV_ONLY_SCRIPTS mixes
|
|
@@ -161,7 +191,13 @@ function installAgents(pipelineSrc, dest, useSymlinks) {
|
|
|
161
191
|
console.log(" [Claude Code] Installing agent definitions...");
|
|
162
192
|
const agentsSrc = join(pipelineSrc, "agents");
|
|
163
193
|
if (!existsSync(agentsSrc)) return;
|
|
164
|
-
|
|
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
|
+
}
|
|
165
201
|
copyDir(agentsSrc, dest, { useSymlinks });
|
|
166
202
|
console.log(` -> ${countFiles(agentsSrc)} files copied to ${dest}`);
|
|
167
203
|
}
|
|
@@ -179,6 +215,7 @@ function installRules(pipelineSrc, dest, useSymlinks) {
|
|
|
179
215
|
if (useSymlinks) {
|
|
180
216
|
copyDir(rulesSrc, dest, { useSymlinks });
|
|
181
217
|
} else {
|
|
218
|
+
ensureRealDir(dest);
|
|
182
219
|
copyDir(rulesSrc, dest, { skipExisting: true });
|
|
183
220
|
}
|
|
184
221
|
console.log(` -> rules present (existing local rules preserved)`);
|
|
@@ -190,6 +227,7 @@ function installSchemas(pipelineSrc, dest, useSymlinks) {
|
|
|
190
227
|
if (!existsSync(schemasSrc)) return;
|
|
191
228
|
// Wipe-before-copy — schemas/ is a 100% pipeline-managed tree, so files
|
|
192
229
|
// removed upstream should not linger.
|
|
230
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
193
231
|
wipeDir(dest);
|
|
194
232
|
copyDir(schemasSrc, dest, { useSymlinks });
|
|
195
233
|
console.log(` -> ${countFiles(schemasSrc)} files copied to ${dest}`);
|
|
@@ -200,6 +238,7 @@ function installLib(pipelineSrc, dest, useSymlinks) {
|
|
|
200
238
|
const libSrc = join(pipelineSrc, "lib");
|
|
201
239
|
if (!existsSync(libSrc)) return;
|
|
202
240
|
// Wipe-before-copy — lib/ is a 100% pipeline-managed tree.
|
|
241
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
203
242
|
wipeDir(dest);
|
|
204
243
|
copyDir(libSrc, dest, { useSymlinks });
|
|
205
244
|
console.log(` -> ${countFiles(libSrc)} files copied to ${dest}`);
|
|
@@ -249,6 +288,10 @@ function installSkills(opts) {
|
|
|
249
288
|
const { pipelineSrc, dest, indexOnly, useSymlinks, platformFlag } = opts;
|
|
250
289
|
console.log(" [Claude Code] Installing skills...");
|
|
251
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
|
+
|
|
252
295
|
if (indexOnly) {
|
|
253
296
|
ensureDir(dest);
|
|
254
297
|
const indexJson = join(pipelineSrc, "skills", ".skills-index.json");
|
|
@@ -354,7 +397,7 @@ function ensurePreferences(prefsPath, pipelineSrc) {
|
|
|
354
397
|
}
|
|
355
398
|
}
|
|
356
399
|
|
|
357
|
-
function configureSettings(home) {
|
|
400
|
+
export function configureSettings(home) {
|
|
358
401
|
console.log(" [Claude Code] Configuring hooks + context management...");
|
|
359
402
|
const SETTINGS_PATH = join(home, ".claude", "settings.json");
|
|
360
403
|
try {
|
|
@@ -372,12 +415,29 @@ function configureSettings(home) {
|
|
|
372
415
|
|
|
373
416
|
if (!settings.hooks) settings.hooks = {};
|
|
374
417
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
+
}
|
|
378
438
|
if (!hasPreCommitHook) {
|
|
379
439
|
settings.hooks.PreToolUse.push({
|
|
380
|
-
matcher: "Bash
|
|
440
|
+
matcher: "Bash",
|
|
381
441
|
hooks: [
|
|
382
442
|
{
|
|
383
443
|
type: "command",
|