@mmerterden/multi-agent-pipeline 20.8.1 → 20.8.2
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 +36 -0
- package/docs/facts.json +3 -3
- package/index.js +1 -0
- package/install/_codex-agents.mjs +1 -1
- package/install/_common.mjs +486 -53
- package/install/_mcp-register.mjs +173 -117
- package/install/claude.mjs +281 -220
- package/install/codex.mjs +7 -7
- package/install/copilot.mjs +13 -11
- package/install/index.mjs +92 -27
- package/install/templates/claude-hooks.json +9 -9
- package/manifest.json +75 -78
- package/package.json +4 -1
- package/pipeline/commands/multi-agent/update/SKILL.md +28 -17
- package/pipeline/lib/confusables.json +79 -33
- package/pipeline/lib/extract-conventions.sh +3 -3
- package/pipeline/lib/json-file-lock.mjs +27 -7
- package/pipeline/lib/normalize-text.mjs +86 -17
- package/pipeline/lib/outbound-gate.mjs +13 -4
- package/pipeline/lib/redact.mjs +87 -14
- package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
- package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
- package/pipeline/multi-agent-refs/conventions-defaults.md +1 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
- package/pipeline/scripts/agent-guard.py +150 -25
- package/pipeline/scripts/audit-log.sh +3 -4
- package/pipeline/scripts/autopilot-runner.mjs +14 -5
- package/pipeline/scripts/doctor.mjs +8 -2
- package/pipeline/scripts/log-metric.sh +9 -3
- package/pipeline/scripts/migrate-prefs.mjs +18 -4
- package/pipeline/scripts/pre-commit-check.sh +119 -31
- package/pipeline/scripts/scan-agent-config.sh +9 -9
- package/pipeline/scripts/unattended_policy.py +12 -3
- package/pipeline/scripts/uninstall.mjs +88 -1
- package/pipeline/scripts/usage-identity.mjs +1 -1
- package/pipeline/scripts/usage-register.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +11 -11
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +14 -13
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +7 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +105 -4
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +20 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -8
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +20 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +15 -8
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +17 -9
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -17
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +28 -7
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/skill-creator/template.md +1 -1
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
- package/pipeline/scripts/gen-ref-toc.mjs +0 -279
- package/pipeline/scripts/make-manifest.mjs +0 -199
- package/pipeline/scripts/scorecard-snapshot.mjs +0 -178
package/install/claude.mjs
CHANGED
|
@@ -5,22 +5,20 @@
|
|
|
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. (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.)
|
|
8
|
+
* The install layout smoke (`smoke-install-layout.sh`) compares the installed
|
|
9
|
+
* tree against a committed fixture on every release.
|
|
13
10
|
*
|
|
14
11
|
* @module install/claude
|
|
15
12
|
*/
|
|
16
13
|
|
|
17
|
-
import { existsSync, readFileSync, readdirSync,
|
|
14
|
+
import { cpSync, existsSync, readFileSync, readdirSync, rmSync } from "fs";
|
|
18
15
|
import { join, dirname } from "path";
|
|
19
16
|
import { execSync } from "child_process";
|
|
20
17
|
|
|
21
18
|
import { localizeCommands } from "../pipeline/scripts/localize-commands.mjs";
|
|
22
19
|
|
|
23
20
|
import {
|
|
21
|
+
classifyInstallDir,
|
|
24
22
|
copyDir,
|
|
25
23
|
copyFile,
|
|
26
24
|
copySkillsIndex,
|
|
@@ -30,21 +28,26 @@ import {
|
|
|
30
28
|
ensureRealDir,
|
|
31
29
|
isDryRun,
|
|
32
30
|
isLocalOnlySkill,
|
|
31
|
+
listFiles,
|
|
33
32
|
pruneAbandonedTrees,
|
|
34
33
|
pruneLegacyMultiAgentSkills,
|
|
35
34
|
pruneOrphanSkillFiles,
|
|
35
|
+
readInstallManifest,
|
|
36
36
|
removePipelineAgentFiles,
|
|
37
|
+
stageAndSwapTrees,
|
|
38
|
+
syncTrackedFiles,
|
|
37
39
|
wipeDir,
|
|
38
40
|
writeFile,
|
|
41
|
+
writeInstallManifest,
|
|
39
42
|
} from "./_common.mjs";
|
|
40
43
|
import { EXTERNAL_SKILLS_MANIFEST } from "./_platform-filter.mjs";
|
|
41
44
|
import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
|
|
42
45
|
import { registerMcpServer } from "./_mcp-register.mjs";
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
+
* A matcher an older install wrote. Hook matchers only ever see the tool name,
|
|
49
|
+
* so this string never matches and the hook never runs. Kept so installs can
|
|
50
|
+
* migrate it and uninstall can clean it.
|
|
48
51
|
*/
|
|
49
52
|
export const LEGACY_PRE_COMMIT_MATCHER = "Bash(git commit:*)";
|
|
50
53
|
export const AGENT_GUARD_FILE_MATCHER = "Edit|Write|NotebookEdit";
|
|
@@ -87,33 +90,35 @@ export const PIPELINE_LOCAL_SKILLS = ["apple-archive-compliance", "google-play-c
|
|
|
87
90
|
export function installClaude(ctx) {
|
|
88
91
|
const { home, pipelineSrc, indexOnly, useSymlinks, platformFlag, pruneExternal } = ctx;
|
|
89
92
|
|
|
90
|
-
const
|
|
91
|
-
const CLAUDE_AGENTS = join(
|
|
92
|
-
const PREFS_PATH = join(
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const CLAUDE_SKILLS = join(home, ".claude", "skills");
|
|
96
|
-
const CLAUDE_SCHEMAS = join(home, ".claude", "schemas");
|
|
97
|
-
const CLAUDE_LIB = join(home, ".claude", "lib");
|
|
98
|
-
const CLAUDE_MA_REFS = join(home, ".claude", "multi-agent-refs");
|
|
99
|
-
const CLAUDE_TEMPLATES = join(home, ".claude", "templates");
|
|
93
|
+
const CLAUDE_DIR = join(home, ".claude");
|
|
94
|
+
const CLAUDE_AGENTS = join(CLAUDE_DIR, "agents");
|
|
95
|
+
const PREFS_PATH = join(CLAUDE_DIR, "multi-agent-preferences.json");
|
|
96
|
+
const CLAUDE_RULES = join(CLAUDE_DIR, "rules");
|
|
97
|
+
const CLAUDE_SKILLS = join(CLAUDE_DIR, "skills");
|
|
100
98
|
|
|
101
99
|
// Before laying anything down: drop trees an older install created that no
|
|
102
100
|
// current installer manages. Wipe-before-copy only protects trees still being
|
|
103
|
-
// written, so an abandoned one
|
|
104
|
-
const prunedTrees = pruneAbandonedTrees(
|
|
101
|
+
// written, so an abandoned one would otherwise stay on disk indefinitely.
|
|
102
|
+
const prunedTrees = pruneAbandonedTrees(CLAUDE_DIR);
|
|
105
103
|
if (prunedTrees > 0) {
|
|
106
104
|
console.log(` [Claude Code] Removed ${prunedTrees} abandoned tree(s) from an older install`);
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
const manifest = readInstallManifest(CLAUDE_DIR);
|
|
108
|
+
// An install without a manifest overwrote every file it shipped on each run,
|
|
109
|
+
// so after one (the version stamp says one happened) a file carrying a
|
|
110
|
+
// shipped name is that installer's copy. On a machine with no prior install
|
|
111
|
+
// the same file is the user's.
|
|
112
|
+
const adopt = !manifest.present && existsSync(join(CLAUDE_DIR, ".pipeline-version"));
|
|
113
|
+
|
|
114
|
+
if (useSymlinks) {
|
|
115
|
+
linkOwnedTrees(pipelineSrc, CLAUDE_DIR);
|
|
116
|
+
} else {
|
|
117
|
+
installOwnedTrees({ pipelineSrc, claudeDir: CLAUDE_DIR, manifest, adopt });
|
|
118
|
+
installTopLevelCommands({ pipelineSrc, claudeDir: CLAUDE_DIR, manifest, adopt });
|
|
119
|
+
}
|
|
112
120
|
installAgents(pipelineSrc, CLAUDE_AGENTS, useSymlinks);
|
|
113
121
|
installRules(pipelineSrc, CLAUDE_RULES, useSymlinks);
|
|
114
|
-
installSchemas(pipelineSrc, CLAUDE_SCHEMAS, useSymlinks);
|
|
115
|
-
installLib(pipelineSrc, CLAUDE_LIB, useSymlinks);
|
|
116
|
-
installTemplates(pipelineSrc, CLAUDE_TEMPLATES, useSymlinks);
|
|
117
122
|
runPreDeployScans(pipelineSrc);
|
|
118
123
|
installSkills({
|
|
119
124
|
pipelineSrc,
|
|
@@ -126,72 +131,179 @@ export function installClaude(ctx) {
|
|
|
126
131
|
ensureClaudeMd(home, pipelineSrc);
|
|
127
132
|
ensurePreferences(PREFS_PATH, pipelineSrc);
|
|
128
133
|
configureSettings(home);
|
|
134
|
+
if (!useSymlinks) writeInstallManifest(CLAUDE_DIR, manifest);
|
|
129
135
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// reaches for one of them. `--scope user` is not optional here: `claude mcp add`
|
|
134
|
-
// defaults to project scope, which would register the server only for whichever
|
|
135
|
-
// directory the installer ran in.
|
|
136
|
+
// The multi-agent-toolkit server backs design-check, every ios_* / android_*
|
|
137
|
+
// simulator call and the archive audits; the skills fail only when a run
|
|
138
|
+
// reaches for one of its tools, so the installer registers it up front.
|
|
136
139
|
registerMcpServer("claude", "Claude Code");
|
|
137
140
|
|
|
138
141
|
console.log("");
|
|
139
142
|
}
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
/**
|
|
145
|
+
* The trees under ~/.claude the pipeline owns outright, laid down as one
|
|
146
|
+
* transaction: each is built beside its destination and all are swapped in
|
|
147
|
+
* together, so a failed copy leaves the previous install whole.
|
|
148
|
+
*
|
|
149
|
+
* `commands/multi-agent` is the pipeline's namespace; local-only alias
|
|
150
|
+
* wrappers (frontmatter `local-only: true`, written by /multi-agent:save and
|
|
151
|
+
* never shipped) are carried into the new copy. `lib/` and `templates/` can
|
|
152
|
+
* hold files the user put there, so they are built from the current copy and
|
|
153
|
+
* updated file by file against the install manifest.
|
|
154
|
+
*/
|
|
155
|
+
function installOwnedTrees({ pipelineSrc, claudeDir, manifest, adopt }) {
|
|
156
|
+
const commandsDir = ensureRealDir(join(claudeDir, "commands"), {
|
|
157
|
+
pipelineSrc,
|
|
158
|
+
userOwned: true,
|
|
159
|
+
});
|
|
160
|
+
const nsDest = join(commandsDir, "multi-agent");
|
|
161
|
+
const stashDir = join(commandsDir, ".multi-agent-wrapper-stash");
|
|
162
|
+
const trees = [];
|
|
163
|
+
const logs = [];
|
|
164
|
+
const add = (dest, label, src, build) => {
|
|
165
|
+
if (!src || !existsSync(src)) return;
|
|
166
|
+
if (classifyInstallDir(dest, pipelineSrc) === "foreign-link") {
|
|
167
|
+
ensureRealDir(dest, { pipelineSrc });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
trees.push({ dest, label, build });
|
|
171
|
+
};
|
|
145
172
|
|
|
146
|
-
|
|
173
|
+
console.log(
|
|
174
|
+
" [Claude Code] Installing pipeline commands, refs, scripts, schemas, lib, templates...",
|
|
175
|
+
);
|
|
147
176
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
177
|
+
const nsSrc = join(pipelineSrc, "commands", "multi-agent");
|
|
178
|
+
add(nsDest, "commands", nsSrc, (staged) => {
|
|
179
|
+
copyDir(nsSrc, staged);
|
|
180
|
+
const restored = carryLocalOnlyWrappers(nsDest, stashDir, staged, pipelineSrc);
|
|
181
|
+
localizeCommandDescriptions(claudeDir, staged);
|
|
182
|
+
logs.push(` -> ${countFiles(nsSrc)} files copied to ${nsDest}`);
|
|
183
|
+
if (restored > 0) logs.push(` -> preserved ${restored} local-only alias wrapper(s)`);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// refs/ and the internal pickers live outside commands/ so Claude Code does
|
|
187
|
+
// not register them as `/multi-agent:refs:*` slash commands; commands read
|
|
188
|
+
// them by absolute path.
|
|
189
|
+
const refsSrc = join(pipelineSrc, "multi-agent-refs");
|
|
190
|
+
const refsDest = join(claudeDir, "multi-agent-refs");
|
|
191
|
+
add(refsDest, "refs", refsSrc, (staged) => {
|
|
192
|
+
copyDir(refsSrc, staged);
|
|
193
|
+
logs.push(` -> ${countFiles(refsSrc)} ref files copied to ${refsDest}`);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const scriptsSrc = join(pipelineSrc, "scripts");
|
|
197
|
+
const scriptsDest = join(claudeDir, "scripts");
|
|
198
|
+
add(scriptsDest, "scripts", scriptsSrc, (staged) => {
|
|
199
|
+
copyDir(scriptsSrc, staged, { exclude: DEV_ONLY_SCRIPTS });
|
|
200
|
+
// DEV_ONLY_SCRIPTS mixes literal names, directories and regexes, so the
|
|
201
|
+
// excluded count comes from walking the tree.
|
|
202
|
+
const excluded = countDevOnlyFiles(scriptsSrc);
|
|
203
|
+
logs.push(
|
|
204
|
+
` -> ${countFiles(scriptsSrc) - excluded} files copied to ${scriptsDest} (${excluded} dev-only excluded)`,
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const schemasSrc = join(pipelineSrc, "schemas");
|
|
209
|
+
const schemasDest = join(claudeDir, "schemas");
|
|
210
|
+
add(schemasDest, "schemas", schemasSrc, (staged) => {
|
|
211
|
+
copyDir(schemasSrc, staged);
|
|
212
|
+
logs.push(` -> ${countFiles(schemasSrc)} files copied to ${schemasDest}`);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
for (const [name, src] of [
|
|
216
|
+
["lib", join(pipelineSrc, "lib")],
|
|
217
|
+
["templates", join(dirname(pipelineSrc), "install", "templates")],
|
|
218
|
+
]) {
|
|
219
|
+
const dest = join(claudeDir, name);
|
|
220
|
+
add(dest, name, src, (staged) => {
|
|
221
|
+
if (classifyInstallDir(dest, pipelineSrc) === "real") {
|
|
222
|
+
cpSync(dest, staged, { recursive: true });
|
|
223
|
+
} else {
|
|
224
|
+
ensureDir(staged);
|
|
225
|
+
}
|
|
226
|
+
const res = syncTrackedFiles({
|
|
227
|
+
srcDir: src,
|
|
228
|
+
destDir: staged,
|
|
229
|
+
displayDir: dest,
|
|
230
|
+
rels: listFiles(src),
|
|
231
|
+
prefix: `${name}/`,
|
|
232
|
+
manifest,
|
|
233
|
+
adopt,
|
|
234
|
+
reportUnknown: true,
|
|
235
|
+
label: "shipped",
|
|
236
|
+
});
|
|
237
|
+
logs.push(
|
|
238
|
+
` -> ${countFiles(src) - res.kept.length} files in place at ${dest}` +
|
|
239
|
+
(res.removed ? ` (${res.removed} retired file(s) removed)` : ""),
|
|
240
|
+
);
|
|
241
|
+
});
|
|
155
242
|
}
|
|
156
243
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
244
|
+
stageAndSwapTrees(trees, { swapRoot: join(claudeDir, ".pipeline-swap") });
|
|
245
|
+
for (const line of logs) console.log(line);
|
|
246
|
+
if (!isDryRun() && existsSync(stashDir) && trees.some((t) => t.dest === nsDest)) {
|
|
247
|
+
rmSync(stashDir, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Top-level command files (`commands/*.md` outside the multi-agent namespace)
|
|
253
|
+
* share `~/.claude/commands/` with the user's own commands, so each is written
|
|
254
|
+
* only when absent or unchanged since the last install (install manifest).
|
|
255
|
+
*/
|
|
256
|
+
function installTopLevelCommands({ pipelineSrc, claudeDir, manifest, adopt }) {
|
|
257
|
+
const commandsSrc = join(pipelineSrc, "commands");
|
|
258
|
+
if (!existsSync(commandsSrc)) return;
|
|
259
|
+
const commandsDir = ensureRealDir(join(claudeDir, "commands"), {
|
|
260
|
+
pipelineSrc,
|
|
261
|
+
userOwned: true,
|
|
262
|
+
});
|
|
263
|
+
const rels = listFiles(commandsSrc).filter((r) => !r.startsWith("multi-agent/"));
|
|
264
|
+
if (!isDryRun()) ensureDir(commandsDir);
|
|
265
|
+
const res = syncTrackedFiles({
|
|
266
|
+
srcDir: commandsSrc,
|
|
267
|
+
destDir: commandsDir,
|
|
268
|
+
rels,
|
|
269
|
+
prefix: "commands/",
|
|
270
|
+
manifest,
|
|
271
|
+
adopt,
|
|
272
|
+
label: "shipped",
|
|
273
|
+
});
|
|
274
|
+
if (res.written || res.removed) {
|
|
275
|
+
console.log(
|
|
276
|
+
` -> ${res.written} top-level command file(s) written, ${res.removed} retired one(s) removed`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** `--link` (dev) mode: every owned tree becomes a symlink into the checkout. */
|
|
282
|
+
function linkOwnedTrees(pipelineSrc, claudeDir) {
|
|
283
|
+
const links = [
|
|
284
|
+
["commands", join(pipelineSrc, "commands")],
|
|
285
|
+
["multi-agent-refs", join(pipelineSrc, "multi-agent-refs")],
|
|
286
|
+
["scripts", join(pipelineSrc, "scripts")],
|
|
287
|
+
["schemas", join(pipelineSrc, "schemas")],
|
|
288
|
+
["lib", join(pipelineSrc, "lib")],
|
|
289
|
+
["templates", join(dirname(pipelineSrc), "install", "templates")],
|
|
290
|
+
];
|
|
291
|
+
console.log(" [Claude Code] Linking pipeline trees (--link)...");
|
|
292
|
+
for (const [name, src] of links) {
|
|
293
|
+
if (!existsSync(src)) continue;
|
|
294
|
+
copyDir(src, join(claudeDir, name), { useSymlinks: true });
|
|
295
|
+
console.log(` -> ${join(claudeDir, name)} -> ${src}`);
|
|
182
296
|
}
|
|
183
|
-
localizeCommandDescriptions(pipelineSrc, ownedCmdDir);
|
|
184
297
|
}
|
|
185
298
|
|
|
186
299
|
// Picker descriptions follow prefs.global.outputLanguage: files shipping a
|
|
187
300
|
// description-tr frontmatter line get their description swapped in the
|
|
188
301
|
// INSTALLED tree only (repo and npm package stay English; /multi-agent:sync
|
|
189
302
|
// restores English before mirroring back). Failure is non-fatal.
|
|
190
|
-
function localizeCommandDescriptions(
|
|
303
|
+
function localizeCommandDescriptions(claudeDir, ownedCmdDir) {
|
|
191
304
|
if (isDryRun()) return;
|
|
192
305
|
try {
|
|
193
|
-
const
|
|
194
|
-
const prefsPath = join(home, "multi-agent-preferences.json");
|
|
306
|
+
const prefsPath = join(claudeDir, "multi-agent-preferences.json");
|
|
195
307
|
if (!existsSync(prefsPath)) return;
|
|
196
308
|
const prefs = JSON.parse(readFileSync(prefsPath, "utf-8"));
|
|
197
309
|
if (prefs?.global?.outputLanguage !== "tr") return;
|
|
@@ -204,87 +316,45 @@ function localizeCommandDescriptions(pipelineSrc, ownedCmdDir) {
|
|
|
204
316
|
}
|
|
205
317
|
}
|
|
206
318
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const name of readdirSync(ownedCmdDir)) {
|
|
214
|
-
const dir = join(ownedCmdDir, name);
|
|
215
|
-
const skill = join(dir, "SKILL.md");
|
|
216
|
-
if (!existsSync(skill)) continue;
|
|
217
|
-
if (!/^local-only:\s*true\s*$/m.test(readFileSync(skill, "utf-8"))) continue;
|
|
218
|
-
ensureDir(stashDir);
|
|
219
|
-
const target = join(stashDir, name);
|
|
220
|
-
if (existsSync(target)) rmSync(target, { recursive: true, force: true });
|
|
221
|
-
renameSync(dir, target);
|
|
319
|
+
const isLocalOnlyDir = (dir) => {
|
|
320
|
+
const skill = join(dir, "SKILL.md");
|
|
321
|
+
try {
|
|
322
|
+
return existsSync(skill) && /^local-only:\s*true\s*$/m.test(readFileSync(skill, "utf-8"));
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
222
325
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
function
|
|
230
|
-
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Copy local-only wrappers from the current namespace, and from a stash an
|
|
329
|
+
// older installer's interrupted run left beside it, into the staged namespace.
|
|
330
|
+
// A wrapper whose name the release now ships cannot be carried; say whose
|
|
331
|
+
// content is dropped, since /multi-agent:save writes exactly these.
|
|
332
|
+
function carryLocalOnlyWrappers(nsDest, stashDir, staged, pipelineSrc) {
|
|
333
|
+
const sources = [];
|
|
334
|
+
if (classifyInstallDir(nsDest, pipelineSrc) === "real") sources.push(nsDest);
|
|
335
|
+
if (existsSync(stashDir)) sources.push(stashDir);
|
|
231
336
|
let n = 0;
|
|
232
|
-
for (const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
337
|
+
for (const from of sources) {
|
|
338
|
+
for (const name of readdirSync(from)) {
|
|
339
|
+
const dir = join(from, name);
|
|
340
|
+
if (from === nsDest && !isLocalOnlyDir(dir)) continue;
|
|
341
|
+
const to = join(staged, name);
|
|
342
|
+
if (existsSync(to)) {
|
|
343
|
+
if (from === nsDest && existsSync(join(pipelineSrc, "commands", "multi-agent", name))) {
|
|
344
|
+
console.log(
|
|
345
|
+
` -> WARNING: dropped local-only wrapper '${name}' - this release ships a command ` +
|
|
346
|
+
`with that name. Re-save it under a different name if you still need it.`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
cpSync(dir, to, { recursive: true });
|
|
352
|
+
n++;
|
|
246
353
|
}
|
|
247
|
-
ensureDir(ownedCmdDir);
|
|
248
|
-
renameSync(from, to);
|
|
249
|
-
n++;
|
|
250
354
|
}
|
|
251
|
-
rmSync(stashDir, { recursive: true, force: true });
|
|
252
355
|
return n;
|
|
253
356
|
}
|
|
254
357
|
|
|
255
|
-
// The pipeline's reference docs (refs/) + internal pickers live OUTSIDE the
|
|
256
|
-
// commands/ tree so Claude Code does NOT register them as invocable
|
|
257
|
-
// `/multi-agent:refs:*` slash commands. Commands read them by absolute path
|
|
258
|
-
// ($HOME/.claude/multi-agent-refs/...). Wipe-before-copy so renamed/removed
|
|
259
|
-
// refs don't linger.
|
|
260
|
-
function installMultiAgentRefs(pipelineSrc, dest, useSymlinks) {
|
|
261
|
-
const refsSrc = join(pipelineSrc, "multi-agent-refs");
|
|
262
|
-
if (!existsSync(refsSrc)) return;
|
|
263
|
-
console.log(" [Claude Code] Installing multi-agent refs (non-command)...");
|
|
264
|
-
if (!useSymlinks) ensureRealDir(dest);
|
|
265
|
-
wipeDir(dest);
|
|
266
|
-
copyDir(refsSrc, dest, { useSymlinks });
|
|
267
|
-
console.log(` -> ${countFiles(refsSrc)} ref files copied to ${dest}`);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function installScripts(pipelineSrc, dest, useSymlinks) {
|
|
271
|
-
console.log(" [Claude Code] Installing scripts...");
|
|
272
|
-
const scriptsSrc = join(pipelineSrc, "scripts");
|
|
273
|
-
if (!existsSync(scriptsSrc)) return;
|
|
274
|
-
|
|
275
|
-
// Wipe-before-copy - scripts/ is a 100% pipeline-managed tree, so files
|
|
276
|
-
// removed upstream should not linger.
|
|
277
|
-
if (!useSymlinks) ensureRealDir(dest);
|
|
278
|
-
wipeDir(dest);
|
|
279
|
-
copyDir(scriptsSrc, dest, { exclude: DEV_ONLY_SCRIPTS, useSymlinks });
|
|
280
|
-
// Count files actually excluded by walking the tree: DEV_ONLY_SCRIPTS mixes
|
|
281
|
-
// literal names, whole directories, and regexes (the maintainer-smoke rule),
|
|
282
|
-
// so summing countFiles() over the array reported 0 for every regex entry.
|
|
283
|
-
const excludedCount = countDevOnlyFiles(scriptsSrc);
|
|
284
|
-
const scriptCount = countFiles(scriptsSrc) - excludedCount;
|
|
285
|
-
console.log(` -> ${scriptCount} files copied to ${dest} (${excludedCount} dev-only excluded)`);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
358
|
function installAgents(pipelineSrc, dest, useSymlinks) {
|
|
289
359
|
console.log(" [Claude Code] Installing agent definitions...");
|
|
290
360
|
const agentsSrc = join(pipelineSrc, "agents");
|
|
@@ -292,11 +362,12 @@ function installAgents(pipelineSrc, dest, useSymlinks) {
|
|
|
292
362
|
// ~/.claude/agents also holds user-authored agent files. Never wipe the
|
|
293
363
|
// whole dir; remove only the pipeline-shipped agent files (name set derived
|
|
294
364
|
// from the source tree so it cannot go stale), then copy the fresh set in.
|
|
365
|
+
let target = dest;
|
|
295
366
|
if (!useSymlinks) {
|
|
296
|
-
ensureRealDir(dest);
|
|
297
|
-
removePipelineAgentFiles(
|
|
367
|
+
target = ensureRealDir(dest, { pipelineSrc, userOwned: true });
|
|
368
|
+
removePipelineAgentFiles(target, agentsSrc);
|
|
298
369
|
}
|
|
299
|
-
copyDir(agentsSrc,
|
|
370
|
+
copyDir(agentsSrc, target, { useSymlinks });
|
|
300
371
|
console.log(` -> ${countFiles(agentsSrc)} files copied to ${dest}`);
|
|
301
372
|
}
|
|
302
373
|
|
|
@@ -304,59 +375,19 @@ function installRules(pipelineSrc, dest, useSymlinks) {
|
|
|
304
375
|
console.log(" [Claude Code] Installing rules...");
|
|
305
376
|
const rulesSrc = join(pipelineSrc, "rules");
|
|
306
377
|
if (!existsSync(rulesSrc)) return;
|
|
307
|
-
// rules/ is
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// user's existing rules untouched. (Symlink mode is a dev choice and keeps its
|
|
312
|
-
// link-replace behavior.)
|
|
378
|
+
// rules/ is user-owned: users evolve their global rules locally and the sync
|
|
379
|
+
// never pushes rules/ back to the repo. An update installs only files that
|
|
380
|
+
// do not exist yet and leaves the user's rules untouched. (Symlink mode is a
|
|
381
|
+
// dev choice and keeps its link-replace behavior.)
|
|
313
382
|
if (useSymlinks) {
|
|
314
383
|
copyDir(rulesSrc, dest, { useSymlinks });
|
|
315
384
|
} else {
|
|
316
|
-
ensureRealDir(dest);
|
|
317
|
-
copyDir(rulesSrc,
|
|
385
|
+
const target = ensureRealDir(dest, { pipelineSrc, userOwned: true });
|
|
386
|
+
copyDir(rulesSrc, target, { skipExisting: true });
|
|
318
387
|
}
|
|
319
388
|
console.log(` -> rules present (existing local rules preserved)`);
|
|
320
389
|
}
|
|
321
390
|
|
|
322
|
-
function installSchemas(pipelineSrc, dest, useSymlinks) {
|
|
323
|
-
console.log(" [Claude Code] Installing JSON schemas...");
|
|
324
|
-
const schemasSrc = join(pipelineSrc, "schemas");
|
|
325
|
-
if (!existsSync(schemasSrc)) return;
|
|
326
|
-
// Wipe-before-copy - schemas/ is a 100% pipeline-managed tree, so files
|
|
327
|
-
// removed upstream should not linger.
|
|
328
|
-
if (!useSymlinks) ensureRealDir(dest);
|
|
329
|
-
wipeDir(dest);
|
|
330
|
-
copyDir(schemasSrc, dest, { useSymlinks });
|
|
331
|
-
console.log(` -> ${countFiles(schemasSrc)} files copied to ${dest}`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function installLib(pipelineSrc, dest, useSymlinks) {
|
|
335
|
-
console.log(" [Claude Code] Installing shell libraries...");
|
|
336
|
-
const libSrc = join(pipelineSrc, "lib");
|
|
337
|
-
if (!existsSync(libSrc)) return;
|
|
338
|
-
// Wipe-before-copy - lib/ is a 100% pipeline-managed tree.
|
|
339
|
-
if (!useSymlinks) ensureRealDir(dest);
|
|
340
|
-
wipeDir(dest);
|
|
341
|
-
copyDir(libSrc, dest, { useSymlinks });
|
|
342
|
-
console.log(` -> ${countFiles(libSrc)} files copied to ${dest}`);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function installTemplates(pipelineSrc, dest, useSymlinks) {
|
|
346
|
-
// The templates shipped in the tarball but never reached the user tree, so
|
|
347
|
-
// `setup` told people to merge `install/templates/claude-hooks.json` - a path
|
|
348
|
-
// that exists only in a checkout. From an install the instruction named a file
|
|
349
|
-
// the reader did not have, and nothing said so.
|
|
350
|
-
console.log(" [Claude Code] Installing templates...");
|
|
351
|
-
const templatesSrc = join(dirname(pipelineSrc), "install", "templates");
|
|
352
|
-
if (!existsSync(templatesSrc)) return;
|
|
353
|
-
// Wipe-before-copy - templates/ is a 100% pipeline-managed tree.
|
|
354
|
-
if (!useSymlinks) ensureRealDir(dest);
|
|
355
|
-
wipeDir(dest);
|
|
356
|
-
copyDir(templatesSrc, dest, { useSymlinks });
|
|
357
|
-
console.log(` -> ${countFiles(templatesSrc)} files copied to ${dest}`);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
391
|
function runPreDeployScans(pipelineSrc) {
|
|
361
392
|
// Pre-deploy security scan - warn-only, never halts install on its own.
|
|
362
393
|
const scanScript = join(pipelineSrc, "scripts", "scan-skills.sh");
|
|
@@ -398,12 +429,13 @@ function runPreDeployScans(pipelineSrc) {
|
|
|
398
429
|
}
|
|
399
430
|
|
|
400
431
|
function installSkills(opts) {
|
|
401
|
-
const { pipelineSrc,
|
|
432
|
+
const { pipelineSrc, indexOnly, useSymlinks, pruneExternal } = opts;
|
|
402
433
|
console.log(" [Claude Code] Installing skills...");
|
|
403
434
|
|
|
404
435
|
// Same symlink guard as the other owned trees: never prune or copy through
|
|
405
|
-
// a --link-era symlink into the repo checkout.
|
|
406
|
-
|
|
436
|
+
// a --link-era symlink into the repo checkout. The skills root also holds
|
|
437
|
+
// the user's own skills, so a symlink elsewhere is written through.
|
|
438
|
+
const dest = useSymlinks ? opts.dest : ensureRealDir(opts.dest, { pipelineSrc, userOwned: true });
|
|
407
439
|
|
|
408
440
|
if (indexOnly) {
|
|
409
441
|
ensureDir(dest);
|
|
@@ -417,12 +449,11 @@ function installSkills(opts) {
|
|
|
417
449
|
|
|
418
450
|
let claudeSkillCount = 0;
|
|
419
451
|
|
|
420
|
-
// Claude Code
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
// they are deliberately not published to the marketplace.
|
|
452
|
+
// Claude Code gets its stack skills from the marketplace plugins
|
|
453
|
+
// (`ai-<stack>-toolkit`) only: a local copy would duplicate them against the
|
|
454
|
+
// enabled plugins and shadow a plugin once the copy went stale. Only the two
|
|
455
|
+
// pipeline-owned compliance catalogs stay local (PIPELINE_LOCAL_SKILLS); they
|
|
456
|
+
// are deliberately not published to the marketplace.
|
|
426
457
|
ensureDir(dest);
|
|
427
458
|
for (const name of PIPELINE_LOCAL_SKILLS) {
|
|
428
459
|
const from = join(sharedCoreSrc, name);
|
|
@@ -434,9 +465,8 @@ function installSkills(opts) {
|
|
|
434
465
|
|
|
435
466
|
// Single-standard enforcement: Claude Code invokes pipeline commands via
|
|
436
467
|
// the `/multi-agent:*` slash-command namespace, so prune duplicate skill
|
|
437
|
-
// dirs left by older installs that copied shared/core wholesale.
|
|
438
|
-
//
|
|
439
|
-
// the 51 multi-agent-* dirs return on the first install after a migration.
|
|
468
|
+
// dirs left by older installs that copied shared/core wholesale. It runs on
|
|
469
|
+
// every install, so such dirs cannot survive a migration.
|
|
440
470
|
const pruned = pruneLegacyMultiAgentSkills(dest);
|
|
441
471
|
if (pruned > 0) {
|
|
442
472
|
console.log(
|
|
@@ -482,12 +512,9 @@ function installSkills(opts) {
|
|
|
482
512
|
}
|
|
483
513
|
}
|
|
484
514
|
|
|
485
|
-
// The index ships on
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
// --index-only copied it, so flipping the pref on a normal install produced an
|
|
489
|
-
// exit-1 "cannot read index" on the first dispatch. Two small files, and the
|
|
490
|
-
// feature is either wired or it is not.
|
|
515
|
+
// The index ships on full installs too: `match-skills.mjs` reads it when
|
|
516
|
+
// prefs.global.dynamicSkillLoading is on, and the preferences schema states
|
|
517
|
+
// that a full install provides it.
|
|
491
518
|
copySkillsIndex(pipelineSrc, dest);
|
|
492
519
|
|
|
493
520
|
// Same pre-directory-layout leftovers Copilot accumulated. Claude Code's tree is
|
|
@@ -718,6 +745,39 @@ export function collapseDuplicateHooks(hooks) {
|
|
|
718
745
|
return changed;
|
|
719
746
|
}
|
|
720
747
|
|
|
748
|
+
/**
|
|
749
|
+
* The hook command for a script installed under ~/.claude/scripts. The path is
|
|
750
|
+
* quoted: Claude Code runs the command through a shell, and an unquoted $HOME
|
|
751
|
+
* containing a space splits into two words, bash exits 126/127 and the gate
|
|
752
|
+
* stops running without anyone seeing it.
|
|
753
|
+
*
|
|
754
|
+
* @param {string} script file name under ~/.claude/scripts
|
|
755
|
+
*/
|
|
756
|
+
export const pipelineHookCommand = (script) => `bash "$HOME/.claude/scripts/${script}"`;
|
|
757
|
+
|
|
758
|
+
const UNQUOTED_PIPELINE_HOOK =
|
|
759
|
+
/^bash (?:\$HOME|\$\{HOME\})\/\.claude\/scripts\/([\w.-]+\.sh)(\s.*)?$/;
|
|
760
|
+
|
|
761
|
+
// Rewrite pipeline hook commands registered with an unquoted $HOME (by this
|
|
762
|
+
// installer or a merged hooks template) to the quoted form, in place, so a
|
|
763
|
+
// re-install fixes them instead of adding a second copy.
|
|
764
|
+
export function quotePipelineHookCommands(hooks) {
|
|
765
|
+
if (!hooks || typeof hooks !== "object") return false;
|
|
766
|
+
let changed = false;
|
|
767
|
+
for (const groups of Object.values(hooks)) {
|
|
768
|
+
if (!Array.isArray(groups)) continue;
|
|
769
|
+
for (const group of groups) {
|
|
770
|
+
for (const h of Array.isArray(group?.hooks) ? group.hooks : []) {
|
|
771
|
+
const m = typeof h?.command === "string" ? UNQUOTED_PIPELINE_HOOK.exec(h.command) : null;
|
|
772
|
+
if (!m) continue;
|
|
773
|
+
h.command = `${pipelineHookCommand(m[1])}${m[2] || ""}`;
|
|
774
|
+
changed = true;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return changed;
|
|
779
|
+
}
|
|
780
|
+
|
|
721
781
|
export function configureSettings(home) {
|
|
722
782
|
console.log(" [Claude Code] Configuring hooks + context management...");
|
|
723
783
|
const SETTINGS_PATH = join(home, ".claude", "settings.json");
|
|
@@ -736,10 +796,11 @@ export function configureSettings(home) {
|
|
|
736
796
|
|
|
737
797
|
if (!settings.hooks) settings.hooks = {};
|
|
738
798
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
799
|
+
if (quotePipelineHookCommands(settings.hooks)) settingsChanged = true;
|
|
739
800
|
// PreToolUse matchers are regexes over the TOOL NAME only, so the correct
|
|
740
|
-
// matcher is "Bash".
|
|
741
|
-
//
|
|
742
|
-
//
|
|
801
|
+
// matcher is "Bash". A "Bash(git commit:*)" matcher (LEGACY_PRE_COMMIT_MATCHER)
|
|
802
|
+
// never matches a tool name, so the gate would never fire; such an entry
|
|
803
|
+
// is migrated in place instead of stacking a duplicate.
|
|
743
804
|
// Command-level filtering (only scan on actual `git commit`) happens
|
|
744
805
|
// inside pre-commit-check.sh via the hook's stdin JSON.
|
|
745
806
|
const isPreCommitEntry = (h) =>
|
|
@@ -762,7 +823,7 @@ export function configureSettings(home) {
|
|
|
762
823
|
hooks: [
|
|
763
824
|
{
|
|
764
825
|
type: "command",
|
|
765
|
-
command: "
|
|
826
|
+
command: pipelineHookCommand("pre-commit-check.sh"),
|
|
766
827
|
timeout: 15,
|
|
767
828
|
statusMessage: "Scanning staged changes for secrets...",
|
|
768
829
|
},
|
|
@@ -813,7 +874,7 @@ export function configureSettings(home) {
|
|
|
813
874
|
hooks: [
|
|
814
875
|
{
|
|
815
876
|
type: "command",
|
|
816
|
-
command: "
|
|
877
|
+
command: pipelineHookCommand("agent-guard.sh"),
|
|
817
878
|
timeout: AGENT_GUARD_TIMEOUT,
|
|
818
879
|
statusMessage,
|
|
819
880
|
},
|