@mmerterden/multi-agent-pipeline 14.2.2 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -6
- package/README.md +15 -8
- package/README.tr.md +15 -8
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +7 -7
- package/docs/ecosystem.md +28 -28
- package/docs/features.md +5 -5
- package/index.js +2 -0
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +65 -1
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +73 -7
- package/install/_plugin-skills.mjs +19 -8
- package/install/claude.mjs +144 -59
- package/install/codex.mjs +28 -3
- package/install/copilot.mjs +36 -11
- package/install/index.mjs +6 -2
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +3 -3
- package/package.json +1 -2
- package/pipeline/commands/multi-agent/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
- package/pipeline/commands/multi-agent/dev/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +8 -8
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/lib/issue-fetcher.sh +1 -1
- package/pipeline/lib/parse-complaints.sh +306 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +3 -3
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
- package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
- package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
- package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
- package/pipeline/multi-agent-refs/generate-issue.md +1 -1
- package/pipeline/multi-agent-refs/phases/modes.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +13 -5
- package/pipeline/rules/figma-pipeline.md +2 -2
- package/pipeline/schemas/agent-state.schema.json +1 -1
- package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
- package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
- package/pipeline/schemas/prefs.schema.json +276 -66
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_stack-routing.mjs +79 -0
- package/pipeline/scripts/audit-log-rotate.sh +4 -1
- package/pipeline/scripts/build-skills-index.mjs +11 -0
- package/pipeline/scripts/build-stack-plugins.mjs +28 -60
- package/pipeline/scripts/check-derived-drift.mjs +52 -28
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/match-skills.mjs +8 -2
- package/pipeline/scripts/migrate-prefs.mjs +28 -20
- package/pipeline/scripts/phase-tracker.sh +13 -5
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
- package/pipeline/scripts/run-aggregator.mjs +7 -2
- package/pipeline/scripts/scan-agent-config.sh +1 -1
- package/pipeline/scripts/skill-conformance.mjs +165 -30
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
- package/pipeline/scripts/test-gap-rules/android.json +25 -0
- package/pipeline/scripts/test-gap-rules/ios.json +34 -0
- package/pipeline/scripts/test-gap-rules/node.json +29 -0
- package/pipeline/scripts/test-gap-rules/python.json +25 -0
- package/pipeline/scripts/uninstall.mjs +158 -11
- package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
- package/pipeline/scripts/validate-reviewer.mjs +9 -3
- package/pipeline/skills/.skill-manifest.json +156 -108
- package/pipeline/skills/.skills-index.json +449 -12
- package/pipeline/skills/shared/README.md +14 -10
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
- package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
- package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
- package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
- package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
- package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +53 -10
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +4 -3
- package/pipeline/skills/skills-index.md +7 -4
package/install/_common.mjs
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
+
chmodSync,
|
|
13
14
|
cpSync,
|
|
14
15
|
existsSync,
|
|
15
16
|
lstatSync,
|
|
16
17
|
mkdirSync,
|
|
17
18
|
readdirSync,
|
|
19
|
+
realpathSync,
|
|
20
|
+
renameSync,
|
|
18
21
|
rmSync,
|
|
19
22
|
statSync,
|
|
20
23
|
symlinkSync,
|
|
@@ -279,6 +282,24 @@ export const ABANDONED_TREES = Object.freeze([
|
|
|
279
282
|
},
|
|
280
283
|
]);
|
|
281
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Registry of command renames. Command names are an interface: users type
|
|
287
|
+
* them, docs and saved routines reference them, and Copilot installs derive
|
|
288
|
+
* `multi-agent-<name>` skill dirs from them. A shipped name may only ever
|
|
289
|
+
* disappear by being recorded here as `oldName: newName` - the
|
|
290
|
+
* install-lifecycle test compares the shipped tree against its committed
|
|
291
|
+
* snapshot and fails on any removal that has no rename entry, so a command
|
|
292
|
+
* cannot vanish between versions by accident.
|
|
293
|
+
*
|
|
294
|
+
* @type {Readonly<Record<string, string>>}
|
|
295
|
+
*/
|
|
296
|
+
export const COMMAND_RENAMES = Object.freeze({
|
|
297
|
+
delete: "uninstall",
|
|
298
|
+
// v15.0.0: "continue local work through the pipeline tail" reads as a resume
|
|
299
|
+
// variant, not a shipping action (the command opens the PR but does not merge).
|
|
300
|
+
ship: "resume-local",
|
|
301
|
+
});
|
|
302
|
+
|
|
282
303
|
/**
|
|
283
304
|
* Remove trees an older install left behind.
|
|
284
305
|
*
|
|
@@ -404,7 +425,50 @@ export function writeFile(path, content) {
|
|
|
404
425
|
console.log(` [dry-run] would write ${path}`);
|
|
405
426
|
return;
|
|
406
427
|
}
|
|
407
|
-
|
|
428
|
+
atomicWrite(path, content);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* tmp + rename so a crash mid-write can never leave a truncated file behind -
|
|
433
|
+
* several call sites rewrite host config the CLI needs in order to start
|
|
434
|
+
* (`~/.claude/settings.json`, `copilot-instructions.md`).
|
|
435
|
+
*
|
|
436
|
+
* Two properties a naive rename would silently destroy on co-owned files:
|
|
437
|
+
*
|
|
438
|
+
* - **symlinks**: users keep these files in a dotfiles repo and symlink them
|
|
439
|
+
* in. Renaming over the link replaces it with a regular file, so the
|
|
440
|
+
* dotfiles copy goes stale and the next dotfiles apply reverts our edits.
|
|
441
|
+
* Resolve the link first and write through it.
|
|
442
|
+
* - **mode**: a rename installs the tmp file's default 0644 over whatever the
|
|
443
|
+
* user set (0600 on a settings file holding tokens is a real case). Carry
|
|
444
|
+
* the existing mode across.
|
|
445
|
+
*
|
|
446
|
+
* @param {string} path
|
|
447
|
+
* @param {string} content
|
|
448
|
+
*/
|
|
449
|
+
export function atomicWrite(path, content) {
|
|
450
|
+
let target = path;
|
|
451
|
+
try {
|
|
452
|
+
if (lstatSync(path).isSymbolicLink()) target = realpathSync(path);
|
|
453
|
+
} catch {
|
|
454
|
+
// Missing file: nothing to preserve, write the path as given.
|
|
455
|
+
}
|
|
456
|
+
let mode;
|
|
457
|
+
try {
|
|
458
|
+
mode = statSync(target).mode & 0o777;
|
|
459
|
+
} catch {
|
|
460
|
+
mode = undefined;
|
|
461
|
+
}
|
|
462
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
463
|
+
writeFileSync(tmp, content);
|
|
464
|
+
if (mode !== undefined) {
|
|
465
|
+
try {
|
|
466
|
+
chmodSync(tmp, mode);
|
|
467
|
+
} catch {
|
|
468
|
+
// Best effort: a failed chmod must not lose the write.
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
renameSync(tmp, target);
|
|
408
472
|
}
|
|
409
473
|
|
|
410
474
|
/**
|
|
@@ -64,7 +64,6 @@ const DEV_ONLY_TOOLING = Object.freeze([
|
|
|
64
64
|
"validate-schemas.mjs", // validates the repo's own schema files, needs ajv
|
|
65
65
|
"sync-parity-check.sh",
|
|
66
66
|
"benchmark-phase-0.sh",
|
|
67
|
-
"test-gap-rules", // rule corpus for the repo's own test-gap gate
|
|
68
67
|
]);
|
|
69
68
|
|
|
70
69
|
/** Eval harnesses (`eval-*.mjs`) read `pipeline/eval/**`, which never ships. */
|
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
* @module install/_platform-filter
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { readdirSync } from "fs";
|
|
10
|
+
import { readdirSync, writeFileSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
|
-
import { copyDir, countFiles, ensureDir } from "./_common.mjs";
|
|
12
|
+
import { copyDir, countFiles, ensureDir, isDryRun } from "./_common.mjs";
|
|
13
|
+
import { routeSkill } from "../pipeline/scripts/_stack-routing.mjs";
|
|
14
|
+
|
|
15
|
+
/** Uninstall reads this to know exactly which external skill dirs are ours to remove. */
|
|
16
|
+
export const EXTERNAL_SKILLS_MANIFEST = ".external-skills-manifest.json";
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Heuristic prefix lists - intentionally loose so new skills added to
|
|
@@ -107,26 +111,88 @@ export function classifyExternalSkill(skillName) {
|
|
|
107
111
|
*/
|
|
108
112
|
export function copyExternalSkillsFiltered(externalSrc, dest, opts) {
|
|
109
113
|
const { platformFlag, useSymlinks = false } = opts;
|
|
114
|
+
const allNames = readdirSync(externalSrc, { withFileTypes: true })
|
|
115
|
+
.filter((e) => e.isDirectory())
|
|
116
|
+
.map((e) => e.name);
|
|
117
|
+
|
|
110
118
|
if (platformFlag === "all") {
|
|
111
119
|
copyDir(externalSrc, dest, { useSymlinks });
|
|
120
|
+
writeExternalSkillsManifest(dest, allNames);
|
|
112
121
|
return { copied: countFiles(externalSrc), skipped: 0 };
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
ensureDir(dest);
|
|
116
125
|
let copied = 0;
|
|
117
126
|
let skipped = 0;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const classification = classifyExternalSkill(
|
|
127
|
+
const delivered = [];
|
|
128
|
+
for (const name of allNames) {
|
|
129
|
+
const classification = classifyExternalSkill(name);
|
|
121
130
|
const shouldCopy = classification === "generic" || classification === platformFlag;
|
|
122
|
-
const src = join(externalSrc,
|
|
123
|
-
const dst = join(dest,
|
|
131
|
+
const src = join(externalSrc, name);
|
|
132
|
+
const dst = join(dest, name);
|
|
124
133
|
if (shouldCopy) {
|
|
125
134
|
copyDir(src, dst, { useSymlinks });
|
|
126
135
|
copied += countFiles(src);
|
|
136
|
+
delivered.push(name);
|
|
127
137
|
} else {
|
|
128
138
|
skipped += 1;
|
|
129
139
|
}
|
|
130
140
|
}
|
|
141
|
+
writeExternalSkillsManifest(dest, delivered);
|
|
131
142
|
return { copied, skipped };
|
|
132
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Enabled-stack partition of the external skill catalog.
|
|
147
|
+
*
|
|
148
|
+
* The prefix classifier above only knows ios/android/generic, so it cannot
|
|
149
|
+
* express "backend toolkit not enabled". Routing can: `routeSkill` is the same
|
|
150
|
+
* table `build-stack-plugins.mjs` ships plugins with, so filtering by it keeps
|
|
151
|
+
* a host's local copy byte-aligned with what the enabled plugins would serve on
|
|
152
|
+
* Claude Code. Unrouted skills are kept - dropping a skill no table claims
|
|
153
|
+
* would make it unreachable everywhere.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} externalSrc - absolute path to `pipeline/skills/shared/external/`
|
|
156
|
+
* @param {string[]} enabledPluginNames - e.g. ["ai-ios-toolkit", "ai-common-toolkit"]
|
|
157
|
+
* @returns {{ keep: string[], skipped: string[] }}
|
|
158
|
+
*/
|
|
159
|
+
export function partitionExternalSkillsByPlugins(externalSrc, enabledPluginNames) {
|
|
160
|
+
const keep = [];
|
|
161
|
+
const skipped = [];
|
|
162
|
+
for (const e of readdirSync(externalSrc, { withFileTypes: true })) {
|
|
163
|
+
if (!e.isDirectory()) continue;
|
|
164
|
+
const plugins = routeSkill(e.name);
|
|
165
|
+
if (plugins.length === 0 || plugins.some((p) => enabledPluginNames.includes(p))) {
|
|
166
|
+
keep.push(e.name);
|
|
167
|
+
} else {
|
|
168
|
+
skipped.push(e.name);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { keep, skipped };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Record exactly which external skill dirs THIS install delivered.
|
|
176
|
+
*
|
|
177
|
+
* Uninstall needs to remove the distributed catalog without touching
|
|
178
|
+
* user-authored skill dirs that happen to share a name. The shipped
|
|
179
|
+
* `.skills-index.json` cannot answer that: it is the full catalog, so it lists
|
|
180
|
+
* platform-filtered skills that were never installed, and a newer package's
|
|
181
|
+
* tree lists skills the installed version never shipped. Both cases would make
|
|
182
|
+
* uninstall delete a user's own directory. Same contract as
|
|
183
|
+
* `.plugin-skills-manifest.json` for plugin-delivered skills.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} dest
|
|
186
|
+
* @param {string[]} names
|
|
187
|
+
*/
|
|
188
|
+
export function writeExternalSkillsManifest(dest, names) {
|
|
189
|
+
if (isDryRun()) return;
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync(
|
|
192
|
+
join(dest, EXTERNAL_SKILLS_MANIFEST),
|
|
193
|
+
JSON.stringify([...names].sort(), null, 2) + "\n",
|
|
194
|
+
);
|
|
195
|
+
} catch {
|
|
196
|
+
/* best-effort - a missing manifest just means uninstall falls back */
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -37,11 +37,11 @@ export const AUTHORED_GROUPS = Object.freeze(["index", "reference", "workflow",
|
|
|
37
37
|
|
|
38
38
|
/** Stack plugins, and the platform each belongs to. */
|
|
39
39
|
export const STACK_PLUGINS = Object.freeze([
|
|
40
|
-
{ name: "ai-ios-
|
|
41
|
-
{ name: "ai-android-
|
|
42
|
-
{ name: "ai-frontend-
|
|
40
|
+
{ name: "ai-ios-toolkit", platform: "ios" },
|
|
41
|
+
{ name: "ai-android-toolkit", platform: "android" },
|
|
42
|
+
{ name: "ai-frontend-toolkit", platform: "all" },
|
|
43
43
|
{ name: "ai-backend-toolkit", platform: "all" },
|
|
44
|
-
{ name: "ai-common-
|
|
44
|
+
{ name: "ai-common-toolkit", platform: "all" },
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -119,7 +119,7 @@ export function pluginsToDeliver(home, platformFlag) {
|
|
|
119
119
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
120
120
|
const enabled = Object.entries(settings.enabledPlugins || {})
|
|
121
121
|
.filter(([, on]) => on === true)
|
|
122
|
-
// keys look like `ai-ios-
|
|
122
|
+
// keys look like `ai-ios-toolkit@multi-agent-plugins`
|
|
123
123
|
.map(([k]) => k.split("@")[0])
|
|
124
124
|
.filter((n) => STACK_PLUGINS.some((p) => p.name === n));
|
|
125
125
|
if (enabled.length > 0) {
|
|
@@ -194,7 +194,7 @@ export function installAuthoredPluginSkills(opts) {
|
|
|
194
194
|
// That is not a duplicate: the pipeline's `architecture` is a generic ADR
|
|
195
195
|
// framework while the iOS plugin's is that stack's structural rules, and the
|
|
196
196
|
// same holds for `backlog`. Claude Code reaches both because its loader
|
|
197
|
-
// namespaces plugin skills (`ai-ios-
|
|
197
|
+
// namespaces plugin skills (`ai-ios-toolkit:architecture`); the
|
|
198
198
|
// copy hosts had no namespace, so the stack-specific version was silently
|
|
199
199
|
// unreachable on exactly the repos that need it most.
|
|
200
200
|
//
|
|
@@ -263,12 +263,23 @@ export function installAuthoredPluginSkills(opts) {
|
|
|
263
263
|
// this pass delivered so uninstall can remove precisely those, nothing more.
|
|
264
264
|
if (!isDryRun()) {
|
|
265
265
|
try {
|
|
266
|
-
writeFileSync(
|
|
266
|
+
writeFileSync(
|
|
267
|
+
join(dest, PLUGIN_SKILLS_MANIFEST),
|
|
268
|
+
JSON.stringify([...delivered], null, 2) + "\n",
|
|
269
|
+
);
|
|
267
270
|
} catch {
|
|
268
271
|
/* best-effort - a missing manifest just means uninstall skips this cleanup */
|
|
269
272
|
}
|
|
270
273
|
}
|
|
271
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
copied,
|
|
276
|
+
collided,
|
|
277
|
+
renamed,
|
|
278
|
+
plugins,
|
|
279
|
+
missing,
|
|
280
|
+
selectionSource,
|
|
281
|
+
deliveredNames: [...delivered],
|
|
282
|
+
};
|
|
272
283
|
}
|
|
273
284
|
|
|
274
285
|
/**
|
package/install/claude.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module install/claude
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { existsSync, readFileSync, readdirSync,
|
|
17
|
+
import { existsSync, readFileSync, readdirSync, renameSync, rmSync } from "fs";
|
|
18
18
|
import { join, dirname } from "path";
|
|
19
19
|
import { execSync } from "child_process";
|
|
20
20
|
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
wipeDir,
|
|
36
36
|
writeFile,
|
|
37
37
|
} from "./_common.mjs";
|
|
38
|
-
import {
|
|
38
|
+
import { EXTERNAL_SKILLS_MANIFEST } from "./_platform-filter.mjs";
|
|
39
39
|
import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
|
|
40
40
|
import { registerMcpServer } from "./_mcp-register.mjs";
|
|
41
41
|
|
|
@@ -46,6 +46,16 @@ import { registerMcpServer } from "./_mcp-register.mjs";
|
|
|
46
46
|
*/
|
|
47
47
|
export const LEGACY_PRE_COMMIT_MATCHER = "Bash(git commit:*)";
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The only skills installed locally on Claude Code. Everything else reaches
|
|
51
|
+
* Claude Code through the marketplace plugins (`ai-<stack>-toolkit`), namespaced
|
|
52
|
+
* by the plugin loader. These two stay local by policy: they are pipeline-owned
|
|
53
|
+
* compliance catalogs that may carry audit rules not meant for the public
|
|
54
|
+
* marketplace, and `scripts/uninstall.mjs` (PIPELINE_CORE_SKILL_DIRS) already
|
|
55
|
+
* treats exactly this pair as pipeline-owned.
|
|
56
|
+
*/
|
|
57
|
+
export const PIPELINE_LOCAL_SKILLS = ["apple-archive-compliance", "google-play-compliance"];
|
|
58
|
+
|
|
49
59
|
/**
|
|
50
60
|
* @param {{
|
|
51
61
|
* home: string,
|
|
@@ -128,13 +138,16 @@ function installCommands(pipelineSrc, dest, useSymlinks) {
|
|
|
128
138
|
//
|
|
129
139
|
// Preserve local-only alias wrappers (frontmatter `local-only: true`) across
|
|
130
140
|
// the wipe. They live ONLY under ~/.claude (never in pipeline/commands, and
|
|
131
|
-
// never synced), so without this
|
|
132
|
-
// every install/update.
|
|
133
|
-
|
|
141
|
+
// never synced), so without this stash+restore they would be destroyed on
|
|
142
|
+
// every install/update. The stash is an on-disk rename, not an in-memory
|
|
143
|
+
// snapshot: the only copy of user-authored content stays on disk at every
|
|
144
|
+
// instant, so an install interrupted between wipe and restore loses nothing,
|
|
145
|
+
// and the next run adopts any leftover stash.
|
|
146
|
+
const wrapperStash = stashLocalOnlyWrappers(ownedCmdDir);
|
|
134
147
|
wipeDir(ownedCmdDir);
|
|
135
148
|
|
|
136
149
|
copyDir(commandsSrc, dest, { useSymlinks });
|
|
137
|
-
const restored =
|
|
150
|
+
const restored = unstashLocalOnlyWrappers(ownedCmdDir, wrapperStash);
|
|
138
151
|
console.log(` -> ${countFiles(commandsSrc)} files copied to ${dest}`);
|
|
139
152
|
if (restored > 0) {
|
|
140
153
|
console.log(` -> preserved ${restored} local-only alias wrapper(s)`);
|
|
@@ -163,36 +176,51 @@ function localizeCommandDescriptions(pipelineSrc, ownedCmdDir) {
|
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
function
|
|
170
|
-
const
|
|
171
|
-
if (!existsSync(ownedCmdDir)) return
|
|
179
|
+
// Move command dirs whose SKILL.md declares `local-only: true` into an on-disk
|
|
180
|
+
// stash beside the namespace, so installCommands can restore them after the
|
|
181
|
+
// namespace wipe. Returns the stash path.
|
|
182
|
+
function stashLocalOnlyWrappers(ownedCmdDir) {
|
|
183
|
+
const stashDir = join(dirname(ownedCmdDir), ".multi-agent-wrapper-stash");
|
|
184
|
+
if (isDryRun() || !existsSync(ownedCmdDir)) return stashDir;
|
|
172
185
|
for (const name of readdirSync(ownedCmdDir)) {
|
|
173
186
|
const dir = join(ownedCmdDir, name);
|
|
174
187
|
const skill = join(dir, "SKILL.md");
|
|
175
188
|
if (!existsSync(skill)) continue;
|
|
176
189
|
if (!/^local-only:\s*true\s*$/m.test(readFileSync(skill, "utf-8"))) continue;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
ensureDir(stashDir);
|
|
191
|
+
const target = join(stashDir, name);
|
|
192
|
+
if (existsSync(target)) rmSync(target, { recursive: true, force: true });
|
|
193
|
+
renameSync(dir, target);
|
|
181
194
|
}
|
|
182
|
-
return
|
|
195
|
+
return stashDir;
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
// Restore
|
|
186
|
-
//
|
|
187
|
-
|
|
198
|
+
// Restore stashed local-only wrappers (including any leftover from an earlier
|
|
199
|
+
// interrupted run), but never clobber a real command the pipeline just
|
|
200
|
+
// installed under the same name. Returns the count restored.
|
|
201
|
+
function unstashLocalOnlyWrappers(ownedCmdDir, stashDir) {
|
|
202
|
+
if (isDryRun() || !existsSync(stashDir)) return 0;
|
|
188
203
|
let n = 0;
|
|
189
|
-
for (const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
204
|
+
for (const name of readdirSync(stashDir)) {
|
|
205
|
+
const from = join(stashDir, name);
|
|
206
|
+
const to = join(ownedCmdDir, name);
|
|
207
|
+
if (existsSync(to)) {
|
|
208
|
+
// The pipeline now ships a real command under a name the user's wrapper
|
|
209
|
+
// was using, so the wrapper cannot be restored. Say whose content is
|
|
210
|
+
// being dropped: /multi-agent:save writes exactly these, and a release
|
|
211
|
+
// claiming a new name would otherwise delete a saved routine in silence.
|
|
212
|
+
console.log(
|
|
213
|
+
` -> WARNING: dropped local-only wrapper '${name}' - this release ships a command ` +
|
|
214
|
+
`with that name. Re-save it under a different name if you still need it.`,
|
|
215
|
+
);
|
|
216
|
+
rmSync(from, { recursive: true, force: true });
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
ensureDir(ownedCmdDir);
|
|
220
|
+
renameSync(from, to);
|
|
194
221
|
n++;
|
|
195
222
|
}
|
|
223
|
+
rmSync(stashDir, { recursive: true, force: true });
|
|
196
224
|
return n;
|
|
197
225
|
}
|
|
198
226
|
|
|
@@ -327,7 +355,7 @@ function runPreDeployScans(pipelineSrc) {
|
|
|
327
355
|
}
|
|
328
356
|
|
|
329
357
|
function installSkills(opts) {
|
|
330
|
-
const { pipelineSrc, dest, indexOnly, useSymlinks
|
|
358
|
+
const { pipelineSrc, dest, indexOnly, useSymlinks } = opts;
|
|
331
359
|
console.log(" [Claude Code] Installing skills...");
|
|
332
360
|
|
|
333
361
|
// Same symlink guard as the other owned trees: never prune or copy through
|
|
@@ -343,40 +371,49 @@ function installSkills(opts) {
|
|
|
343
371
|
}
|
|
344
372
|
|
|
345
373
|
const sharedCoreSrc = join(pipelineSrc, "skills", "shared", "core");
|
|
346
|
-
const sharedExternalSrc = join(pipelineSrc, "skills", "shared", "external");
|
|
347
374
|
|
|
348
375
|
let claudeSkillCount = 0;
|
|
349
376
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
377
|
+
// Claude Code no longer receives a local copy of the stack skills. The
|
|
378
|
+
// marketplace plugins (`ai-<stack>-toolkit`) are its only stack-skill source -
|
|
379
|
+
// a local copy duplicated ~110 skills against the enabled plugins (~10k
|
|
380
|
+
// tokens/session) and shadowed the plugin the moment it went stale. Only the
|
|
381
|
+
// two pipeline-owned compliance catalogs stay local (PIPELINE_LOCAL_SKILLS);
|
|
382
|
+
// they are deliberately not published to the marketplace.
|
|
383
|
+
ensureDir(dest);
|
|
384
|
+
for (const name of PIPELINE_LOCAL_SKILLS) {
|
|
385
|
+
const from = join(sharedCoreSrc, name);
|
|
386
|
+
if (!existsSync(from)) continue;
|
|
387
|
+
if (!useSymlinks) wipeDir(join(dest, name));
|
|
388
|
+
copyDir(from, join(dest, name), { useSymlinks });
|
|
389
|
+
claudeSkillCount += countFiles(from);
|
|
353
390
|
}
|
|
354
391
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
392
|
+
// Single-standard enforcement: Claude Code invokes pipeline commands via
|
|
393
|
+
// the `/multi-agent:*` slash-command namespace, so prune duplicate skill
|
|
394
|
+
// dirs left by older installs that copied shared/core wholesale. This used
|
|
395
|
+
// to live inside the external-copy branch; it must run unconditionally or
|
|
396
|
+
// the 51 multi-agent-* dirs return on the first install after a migration.
|
|
397
|
+
const pruned = pruneLegacyMultiAgentSkills(dest);
|
|
398
|
+
if (pruned > 0) {
|
|
399
|
+
console.log(
|
|
400
|
+
` -> pruned ${pruned} legacy multi-agent-* skill dirs (slash commands cover these)`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
366
403
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
)
|
|
376
|
-
|
|
404
|
+
// Migration prune: an older install delivered the full external catalog here.
|
|
405
|
+
// Its manifest names exactly what was delivered, so removal is manifest-scoped -
|
|
406
|
+
// user-authored skills in the same directory are untouched. Without a manifest
|
|
407
|
+
// nothing is guessed; the stale copy then outlives this install and the user
|
|
408
|
+
// can clean it via uninstall --claude.
|
|
409
|
+
const migrated = pruneManifestDeliveredSkills(dest);
|
|
410
|
+
if (migrated > 0) {
|
|
411
|
+
console.log(
|
|
412
|
+
` -> migration: removed ${migrated} stack skill dir(s) previously copied here (now plugin-only)`,
|
|
413
|
+
);
|
|
377
414
|
}
|
|
378
415
|
|
|
379
|
-
// Figma component skills moved to the ai-<platform>-
|
|
416
|
+
// Figma component skills moved to the ai-<platform>-toolkit marketplace
|
|
380
417
|
// plugin and are no longer bundled here. Prune any stale copies left by a
|
|
381
418
|
// pre-migration install so the installed tree stays consistent.
|
|
382
419
|
for (const sub of ["figma-ios", "figma-android", "figma-common", "figma-to-component"]) {
|
|
@@ -410,21 +447,69 @@ function installSkills(opts) {
|
|
|
410
447
|
console.log(` -> pruned ${orphans} stale flat skill file(s) from an older layout`);
|
|
411
448
|
}
|
|
412
449
|
|
|
413
|
-
console.log(` -> ${claudeSkillCount} skill files installed to ${dest}`);
|
|
450
|
+
console.log(` -> ${claudeSkillCount} skill files installed to ${dest} (stack skills: marketplace plugins)`);
|
|
414
451
|
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
452
|
+
// The shared skills README described the full local catalog; with the catalog
|
|
453
|
+
// gone it would document skills that are not here. Drop it, including copies
|
|
454
|
+
// an older install left behind.
|
|
455
|
+
const claudeReadme = join(dest, "README.md");
|
|
456
|
+
if (existsSync(claudeReadme) && !isDryRun()) {
|
|
419
457
|
try {
|
|
420
|
-
|
|
421
|
-
console.log(` ->
|
|
458
|
+
rmSync(claudeReadme);
|
|
459
|
+
console.log(` -> removed ${claudeReadme} (local catalog no longer installed)`);
|
|
422
460
|
} catch {
|
|
423
461
|
/* non-fatal */
|
|
424
462
|
}
|
|
425
463
|
}
|
|
426
464
|
}
|
|
427
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Remove the external stack skills an older install copied into `dest`, scoped
|
|
468
|
+
* strictly to the delivery manifest that install wrote. NOTICE files rode along
|
|
469
|
+
* with the external catalog, so they go with it.
|
|
470
|
+
*
|
|
471
|
+
* @param {string} dest - the installed skills directory
|
|
472
|
+
* @returns {number} directories removed
|
|
473
|
+
*/
|
|
474
|
+
function pruneManifestDeliveredSkills(dest) {
|
|
475
|
+
const manifestPath = join(dest, EXTERNAL_SKILLS_MANIFEST);
|
|
476
|
+
if (!existsSync(manifestPath)) return 0;
|
|
477
|
+
if (isDryRun()) {
|
|
478
|
+
console.log(` [dry-run] would remove externally-delivered skills named in ${manifestPath}`);
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
let removed = 0;
|
|
482
|
+
try {
|
|
483
|
+
const names = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
484
|
+
for (const name of Array.isArray(names) ? names : []) {
|
|
485
|
+
// The two local compliance catalogs are never external-delivered, but a
|
|
486
|
+
// corrupted manifest must not be able to take them out.
|
|
487
|
+
if (PIPELINE_LOCAL_SKILLS.includes(name)) continue;
|
|
488
|
+
const dir = join(dest, name);
|
|
489
|
+
if (!existsSync(dir)) continue;
|
|
490
|
+
try {
|
|
491
|
+
rmSync(dir, { recursive: true, force: true });
|
|
492
|
+
removed++;
|
|
493
|
+
} catch {
|
|
494
|
+
/* non-fatal */
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const entry of readdirSync(dest)) {
|
|
498
|
+
if (entry.startsWith("NOTICE-") && entry.endsWith(".md")) {
|
|
499
|
+
try {
|
|
500
|
+
rmSync(join(dest, entry));
|
|
501
|
+
} catch {
|
|
502
|
+
/* non-fatal */
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
rmSync(manifestPath, { force: true });
|
|
507
|
+
} catch {
|
|
508
|
+
/* unreadable manifest: leave everything in place rather than guess */
|
|
509
|
+
}
|
|
510
|
+
return removed;
|
|
511
|
+
}
|
|
512
|
+
|
|
428
513
|
function ensureClaudeMd(home, pipelineSrc) {
|
|
429
514
|
console.log(" [Claude Code] Checking CLAUDE.md template...");
|
|
430
515
|
const CLAUDE_MD = join(home, ".claude", "CLAUDE.md");
|
package/install/codex.mjs
CHANGED
|
@@ -34,7 +34,15 @@ import {
|
|
|
34
34
|
import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
|
|
35
35
|
import { registerMcpServer } from "./_mcp-register.mjs";
|
|
36
36
|
import { installCodexAgents } from "./_codex-agents.mjs";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
installAuthoredPluginSkills,
|
|
39
|
+
pipelineOwnedSkillNames,
|
|
40
|
+
pluginsToDeliver,
|
|
41
|
+
} from "./_plugin-skills.mjs";
|
|
42
|
+
import {
|
|
43
|
+
partitionExternalSkillsByPlugins,
|
|
44
|
+
writeExternalSkillsManifest,
|
|
45
|
+
} from "./_platform-filter.mjs";
|
|
38
46
|
import { generateCodexInstructions } from "./_codex-instructions.mjs";
|
|
39
47
|
import { mergeManagedBlock } from "./_managed-block.mjs";
|
|
40
48
|
|
|
@@ -269,7 +277,7 @@ function pruneLegacyCommandSkills(skillsDir) {
|
|
|
269
277
|
}
|
|
270
278
|
|
|
271
279
|
/**
|
|
272
|
-
* Install the reference tree: the shared `multi-agent-refs/` docs plus
|
|
280
|
+
* Install the reference tree: the shared `multi-agent-refs/` docs plus every
|
|
273
281
|
* sub-command specs, all path-rewritten. These are read on demand and never
|
|
274
282
|
* enter the skills block.
|
|
275
283
|
*/
|
|
@@ -394,7 +402,24 @@ function installSkillRefs(pipelineSrc, dest, home, platformFlag) {
|
|
|
394
402
|
wipeDir(dest);
|
|
395
403
|
|
|
396
404
|
let count = 0;
|
|
397
|
-
if (existsSync(externalSrc))
|
|
405
|
+
if (existsSync(externalSrc)) {
|
|
406
|
+
// Active-stack filter (same contract as Copilot): Codex used to receive all
|
|
407
|
+
// 151 external skills regardless of stack - platformFlag only shaped the
|
|
408
|
+
// authored-plugin delivery below. The enabled-plugin set from
|
|
409
|
+
// ~/.claude/settings.json now bounds this copy too, so a Claude+Codex user
|
|
410
|
+
// sees one consistent stack surface across both hosts. The manifest makes
|
|
411
|
+
// the delivery inspectable; the wholesale wipeDir above already handles
|
|
412
|
+
// staleness on every install.
|
|
413
|
+
const { names: enabled, source: selectionSource } = pluginsToDeliver(home, platformFlag);
|
|
414
|
+
const { keep, skipped } = partitionExternalSkillsByPlugins(externalSrc, enabled);
|
|
415
|
+
for (const name of keep) {
|
|
416
|
+
count += copyTreeRewritten(join(externalSrc, name), join(dest, name));
|
|
417
|
+
}
|
|
418
|
+
writeExternalSkillsManifest(dest, keep);
|
|
419
|
+
console.log(
|
|
420
|
+
` -> stack filter (${selectionSource}): ${keep.length} kept, ${skipped.length} skipped`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
398
423
|
|
|
399
424
|
// `shared/core` also holds two skills that are NOT pipeline sub-commands:
|
|
400
425
|
// apple-archive-compliance and google-play-compliance. Copying only `external`
|