@mmerterden/multi-agent-pipeline 14.2.2 → 15.1.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 +186 -6
- package/README.md +19 -12
- package/README.tr.md +19 -12
- package/SECURITY.md +43 -0
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0007-multi-tool-adapter-framework.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 +13 -13
- package/docs/ecosystem.md +31 -31
- package/docs/features.md +5 -5
- package/index.js +6 -1
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +109 -3
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +54 -113
- package/install/_plugin-skills.mjs +36 -36
- package/install/claude.mjs +251 -61
- package/install/codex.mjs +28 -6
- package/install/copilot.mjs +69 -9
- package/install/index.mjs +9 -3
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +3 -3
- package/package.json +2 -3
- 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/refactor/SKILL.md +36 -1
- 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/scan/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +62 -40
- 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 +4 -4
- package/pipeline/lib/issue-fetcher.sh +1 -1
- package/pipeline/lib/parse-complaints.sh +316 -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/operations.md +7 -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/phases/phase-7-report.md +6 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +3 -2
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +18 -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 +296 -66
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/README.md +4 -3
- 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 +55 -28
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/match-skills.mjs +12 -2
- package/pipeline/scripts/migrate-prefs.mjs +33 -21
- package/pipeline/scripts/phase-tracker.sh +32 -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 +160 -11
- package/pipeline/scripts/usage-report.mjs +426 -0
- package/pipeline/scripts/validate-complaint-doc.mjs +250 -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-refactor/SKILL.md +153 -90
- 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 +89 -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/core/multi-agent-update/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
|
@@ -33,15 +33,15 @@ export const PLUGIN_SKILLS_MANIFEST = ".plugin-skills-manifest.json";
|
|
|
33
33
|
import { copyDir, countFiles, ensureDir, ensureRealDir, isDryRun, wipeDir } from "./_common.mjs";
|
|
34
34
|
|
|
35
35
|
/** Subtrees a plugin authors itself. `knowledge/` is generated, so it is excluded. */
|
|
36
|
-
|
|
36
|
+
const AUTHORED_GROUPS = Object.freeze(["index", "reference", "workflow", "tools"]);
|
|
37
37
|
|
|
38
38
|
/** Stack plugins, and the platform each belongs to. */
|
|
39
|
-
|
|
40
|
-
{ name: "ai-ios-
|
|
41
|
-
{ name: "ai-android-
|
|
42
|
-
{ name: "ai-frontend-
|
|
39
|
+
const STACK_PLUGINS = Object.freeze([
|
|
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
|
/**
|
|
@@ -71,7 +71,7 @@ function semverCompare(a, b) {
|
|
|
71
71
|
* @param {string} pluginName
|
|
72
72
|
* @returns {{path: string, source: string}|null}
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
function resolvePluginSkills(home, pluginName) {
|
|
75
75
|
const checkout = join(home, "multi-agent-plugins", "plugins", pluginName, "skills");
|
|
76
76
|
if (existsSync(checkout)) return { path: checkout, source: "local checkout" };
|
|
77
77
|
|
|
@@ -104,22 +104,27 @@ export function resolvePluginSkills(home, pluginName) {
|
|
|
104
104
|
* A flat copy of all of them is last-write-wins, so an iOS repo could end up running
|
|
105
105
|
* the Android `create-component`.
|
|
106
106
|
*
|
|
107
|
-
* So the enabled list in Claude Code's settings is the source of truth
|
|
108
|
-
*
|
|
109
|
-
* settings
|
|
107
|
+
* So the enabled list in Claude Code's settings is the source of truth, and it is
|
|
108
|
+
* read where `/multi-agent:stack` writes it: the invoking repo's
|
|
109
|
+
* `.claude/settings.json` first, the user-global `~/.claude/settings.json` second.
|
|
110
|
+
* The `--platform` flag is only the fallback for a machine with neither.
|
|
110
111
|
*
|
|
111
112
|
* @param {string} home
|
|
112
113
|
* @param {"ios"|"android"|"all"} platformFlag
|
|
113
114
|
* @returns {{names: string[], source: string}}
|
|
114
115
|
*/
|
|
115
116
|
export function pluginsToDeliver(home, platformFlag) {
|
|
116
|
-
const
|
|
117
|
-
|
|
117
|
+
const candidates = [
|
|
118
|
+
{ path: join(process.cwd(), ".claude", "settings.json"), label: "./.claude/settings.json" },
|
|
119
|
+
{ path: join(home, ".claude", "settings.json"), label: "~/.claude/settings.json" },
|
|
120
|
+
];
|
|
121
|
+
for (const { path: settingsPath, label } of candidates) {
|
|
122
|
+
if (!existsSync(settingsPath)) continue;
|
|
118
123
|
try {
|
|
119
124
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
120
125
|
const enabled = Object.entries(settings.enabledPlugins || {})
|
|
121
126
|
.filter(([, on]) => on === true)
|
|
122
|
-
// keys look like `ai-ios-
|
|
127
|
+
// keys look like `ai-ios-toolkit@multi-agent-plugins`
|
|
123
128
|
.map(([k]) => k.split("@")[0])
|
|
124
129
|
.filter((n) => STACK_PLUGINS.some((p) => p.name === n));
|
|
125
130
|
if (enabled.length > 0) {
|
|
@@ -127,10 +132,10 @@ export function pluginsToDeliver(home, platformFlag) {
|
|
|
127
132
|
// collision the first delivery wins, and the stack-specific version is the
|
|
128
133
|
// one the repo actually wants.
|
|
129
134
|
const ordered = STACK_PLUGINS.map((p) => p.name).filter((n) => enabled.includes(n));
|
|
130
|
-
return { names: ordered, source:
|
|
135
|
+
return { names: ordered, source: `enabledPlugins in ${label}` };
|
|
131
136
|
}
|
|
132
137
|
} catch {
|
|
133
|
-
/* unreadable settings fall through to the
|
|
138
|
+
/* unreadable settings fall through to the next candidate */
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
const names = STACK_PLUGINS.filter(
|
|
@@ -194,7 +199,7 @@ export function installAuthoredPluginSkills(opts) {
|
|
|
194
199
|
// That is not a duplicate: the pipeline's `architecture` is a generic ADR
|
|
195
200
|
// framework while the iOS plugin's is that stack's structural rules, and the
|
|
196
201
|
// same holds for `backlog`. Claude Code reaches both because its loader
|
|
197
|
-
// namespaces plugin skills (`ai-ios-
|
|
202
|
+
// namespaces plugin skills (`ai-ios-toolkit:architecture`); the
|
|
198
203
|
// copy hosts had no namespace, so the stack-specific version was silently
|
|
199
204
|
// unreachable on exactly the repos that need it most.
|
|
200
205
|
//
|
|
@@ -263,35 +268,30 @@ export function installAuthoredPluginSkills(opts) {
|
|
|
263
268
|
// this pass delivered so uninstall can remove precisely those, nothing more.
|
|
264
269
|
if (!isDryRun()) {
|
|
265
270
|
try {
|
|
266
|
-
writeFileSync(
|
|
271
|
+
writeFileSync(
|
|
272
|
+
join(dest, PLUGIN_SKILLS_MANIFEST),
|
|
273
|
+
JSON.stringify([...delivered], null, 2) + "\n",
|
|
274
|
+
);
|
|
267
275
|
} catch {
|
|
268
276
|
/* best-effort - a missing manifest just means uninstall skips this cleanup */
|
|
269
277
|
}
|
|
270
278
|
}
|
|
271
|
-
return {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
*/
|
|
281
|
-
export function existingSkillNames(dir) {
|
|
282
|
-
if (!existsSync(dir)) return new Set();
|
|
283
|
-
return new Set(
|
|
284
|
-
readdirSync(dir, { withFileTypes: true })
|
|
285
|
-
.filter((e) => e.isDirectory())
|
|
286
|
-
.map((e) => e.name),
|
|
287
|
-
);
|
|
279
|
+
return {
|
|
280
|
+
copied,
|
|
281
|
+
collided,
|
|
282
|
+
renamed,
|
|
283
|
+
plugins,
|
|
284
|
+
missing,
|
|
285
|
+
selectionSource,
|
|
286
|
+
deliveredNames: [...delivered],
|
|
287
|
+
};
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
/**
|
|
291
291
|
* Skill names the PIPELINE owns, derived from its source tree.
|
|
292
292
|
*
|
|
293
|
-
* This is what `skipNames` should be. Callers used to pass
|
|
294
|
-
*
|
|
293
|
+
* This is what `skipNames` should be. Callers used to pass a snapshot of
|
|
294
|
+
* whatever was already installed (a now-deleted `existingSkillNames(dest)`) - which
|
|
295
295
|
* did protect pipeline skills from being shadowed by a plugin, but also meant a
|
|
296
296
|
* plugin skill was copied exactly once and then frozen: on the second install its
|
|
297
297
|
* own name was "already there", so it was skipped, forever. A stale plugin skill
|
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
|
|
|
@@ -25,9 +25,11 @@ import {
|
|
|
25
25
|
copyFile,
|
|
26
26
|
copySkillsIndex,
|
|
27
27
|
countFiles,
|
|
28
|
+
dirsIdentical,
|
|
28
29
|
ensureDir,
|
|
29
30
|
ensureRealDir,
|
|
30
31
|
isDryRun,
|
|
32
|
+
isLocalOnlySkill,
|
|
31
33
|
pruneAbandonedTrees,
|
|
32
34
|
pruneLegacyMultiAgentSkills,
|
|
33
35
|
pruneOrphanSkillFiles,
|
|
@@ -35,7 +37,7 @@ import {
|
|
|
35
37
|
wipeDir,
|
|
36
38
|
writeFile,
|
|
37
39
|
} from "./_common.mjs";
|
|
38
|
-
import {
|
|
40
|
+
import { EXTERNAL_SKILLS_MANIFEST } from "./_platform-filter.mjs";
|
|
39
41
|
import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
|
|
40
42
|
import { registerMcpServer } from "./_mcp-register.mjs";
|
|
41
43
|
|
|
@@ -46,6 +48,16 @@ import { registerMcpServer } from "./_mcp-register.mjs";
|
|
|
46
48
|
*/
|
|
47
49
|
export const LEGACY_PRE_COMMIT_MATCHER = "Bash(git commit:*)";
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The only skills installed locally on Claude Code. Everything else reaches
|
|
53
|
+
* Claude Code through the marketplace plugins (`ai-<stack>-toolkit`), namespaced
|
|
54
|
+
* by the plugin loader. These two stay local by policy: they are pipeline-owned
|
|
55
|
+
* compliance catalogs that may carry audit rules not meant for the public
|
|
56
|
+
* marketplace, and `scripts/uninstall.mjs` (PIPELINE_CORE_SKILL_DIRS) already
|
|
57
|
+
* treats exactly this pair as pipeline-owned.
|
|
58
|
+
*/
|
|
59
|
+
export const PIPELINE_LOCAL_SKILLS = ["apple-archive-compliance", "google-play-compliance"];
|
|
60
|
+
|
|
49
61
|
/**
|
|
50
62
|
* @param {{
|
|
51
63
|
* home: string,
|
|
@@ -53,10 +65,11 @@ export const LEGACY_PRE_COMMIT_MATCHER = "Bash(git commit:*)";
|
|
|
53
65
|
* indexOnly: boolean,
|
|
54
66
|
* useSymlinks: boolean,
|
|
55
67
|
* platformFlag: "ios"|"android"|"all",
|
|
68
|
+
* pruneExternal?: boolean,
|
|
56
69
|
* }} ctx
|
|
57
70
|
*/
|
|
58
71
|
export function installClaude(ctx) {
|
|
59
|
-
const { home, pipelineSrc, indexOnly, useSymlinks, platformFlag } = ctx;
|
|
72
|
+
const { home, pipelineSrc, indexOnly, useSymlinks, platformFlag, pruneExternal } = ctx;
|
|
60
73
|
|
|
61
74
|
const CLAUDE_COMMANDS = join(home, ".claude", "commands");
|
|
62
75
|
const CLAUDE_AGENTS = join(home, ".claude", "agents");
|
|
@@ -84,7 +97,7 @@ export function installClaude(ctx) {
|
|
|
84
97
|
installSchemas(pipelineSrc, CLAUDE_SCHEMAS, useSymlinks);
|
|
85
98
|
installLib(pipelineSrc, CLAUDE_LIB, useSymlinks);
|
|
86
99
|
runPreDeployScans(pipelineSrc);
|
|
87
|
-
installSkills({ pipelineSrc, dest: CLAUDE_SKILLS, indexOnly, useSymlinks, platformFlag });
|
|
100
|
+
installSkills({ pipelineSrc, dest: CLAUDE_SKILLS, indexOnly, useSymlinks, platformFlag, pruneExternal });
|
|
88
101
|
ensureClaudeMd(home, pipelineSrc);
|
|
89
102
|
ensurePreferences(PREFS_PATH, pipelineSrc);
|
|
90
103
|
configureSettings(home);
|
|
@@ -128,13 +141,16 @@ function installCommands(pipelineSrc, dest, useSymlinks) {
|
|
|
128
141
|
//
|
|
129
142
|
// Preserve local-only alias wrappers (frontmatter `local-only: true`) across
|
|
130
143
|
// the wipe. They live ONLY under ~/.claude (never in pipeline/commands, and
|
|
131
|
-
// never synced), so without this
|
|
132
|
-
// every install/update.
|
|
133
|
-
|
|
144
|
+
// never synced), so without this stash+restore they would be destroyed on
|
|
145
|
+
// every install/update. The stash is an on-disk rename, not an in-memory
|
|
146
|
+
// snapshot: the only copy of user-authored content stays on disk at every
|
|
147
|
+
// instant, so an install interrupted between wipe and restore loses nothing,
|
|
148
|
+
// and the next run adopts any leftover stash.
|
|
149
|
+
const wrapperStash = stashLocalOnlyWrappers(ownedCmdDir);
|
|
134
150
|
wipeDir(ownedCmdDir);
|
|
135
151
|
|
|
136
152
|
copyDir(commandsSrc, dest, { useSymlinks });
|
|
137
|
-
const restored =
|
|
153
|
+
const restored = unstashLocalOnlyWrappers(ownedCmdDir, wrapperStash);
|
|
138
154
|
console.log(` -> ${countFiles(commandsSrc)} files copied to ${dest}`);
|
|
139
155
|
if (restored > 0) {
|
|
140
156
|
console.log(` -> preserved ${restored} local-only alias wrapper(s)`);
|
|
@@ -163,36 +179,51 @@ function localizeCommandDescriptions(pipelineSrc, ownedCmdDir) {
|
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
181
|
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
function
|
|
170
|
-
const
|
|
171
|
-
if (!existsSync(ownedCmdDir)) return
|
|
182
|
+
// Move command dirs whose SKILL.md declares `local-only: true` into an on-disk
|
|
183
|
+
// stash beside the namespace, so installCommands can restore them after the
|
|
184
|
+
// namespace wipe. Returns the stash path.
|
|
185
|
+
function stashLocalOnlyWrappers(ownedCmdDir) {
|
|
186
|
+
const stashDir = join(dirname(ownedCmdDir), ".multi-agent-wrapper-stash");
|
|
187
|
+
if (isDryRun() || !existsSync(ownedCmdDir)) return stashDir;
|
|
172
188
|
for (const name of readdirSync(ownedCmdDir)) {
|
|
173
189
|
const dir = join(ownedCmdDir, name);
|
|
174
190
|
const skill = join(dir, "SKILL.md");
|
|
175
191
|
if (!existsSync(skill)) continue;
|
|
176
192
|
if (!/^local-only:\s*true\s*$/m.test(readFileSync(skill, "utf-8"))) continue;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
ensureDir(stashDir);
|
|
194
|
+
const target = join(stashDir, name);
|
|
195
|
+
if (existsSync(target)) rmSync(target, { recursive: true, force: true });
|
|
196
|
+
renameSync(dir, target);
|
|
181
197
|
}
|
|
182
|
-
return
|
|
198
|
+
return stashDir;
|
|
183
199
|
}
|
|
184
200
|
|
|
185
|
-
// Restore
|
|
186
|
-
//
|
|
187
|
-
|
|
201
|
+
// Restore stashed local-only wrappers (including any leftover from an earlier
|
|
202
|
+
// interrupted run), but never clobber a real command the pipeline just
|
|
203
|
+
// installed under the same name. Returns the count restored.
|
|
204
|
+
function unstashLocalOnlyWrappers(ownedCmdDir, stashDir) {
|
|
205
|
+
if (isDryRun() || !existsSync(stashDir)) return 0;
|
|
188
206
|
let n = 0;
|
|
189
|
-
for (const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
for (const name of readdirSync(stashDir)) {
|
|
208
|
+
const from = join(stashDir, name);
|
|
209
|
+
const to = join(ownedCmdDir, name);
|
|
210
|
+
if (existsSync(to)) {
|
|
211
|
+
// The pipeline now ships a real command under a name the user's wrapper
|
|
212
|
+
// was using, so the wrapper cannot be restored. Say whose content is
|
|
213
|
+
// being dropped: /multi-agent:save writes exactly these, and a release
|
|
214
|
+
// claiming a new name would otherwise delete a saved routine in silence.
|
|
215
|
+
console.log(
|
|
216
|
+
` -> WARNING: dropped local-only wrapper '${name}' - this release ships a command ` +
|
|
217
|
+
`with that name. Re-save it under a different name if you still need it.`,
|
|
218
|
+
);
|
|
219
|
+
rmSync(from, { recursive: true, force: true });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
ensureDir(ownedCmdDir);
|
|
223
|
+
renameSync(from, to);
|
|
194
224
|
n++;
|
|
195
225
|
}
|
|
226
|
+
rmSync(stashDir, { recursive: true, force: true });
|
|
196
227
|
return n;
|
|
197
228
|
}
|
|
198
229
|
|
|
@@ -327,7 +358,7 @@ function runPreDeployScans(pipelineSrc) {
|
|
|
327
358
|
}
|
|
328
359
|
|
|
329
360
|
function installSkills(opts) {
|
|
330
|
-
const { pipelineSrc, dest, indexOnly, useSymlinks,
|
|
361
|
+
const { pipelineSrc, dest, indexOnly, useSymlinks, pruneExternal } = opts;
|
|
331
362
|
console.log(" [Claude Code] Installing skills...");
|
|
332
363
|
|
|
333
364
|
// Same symlink guard as the other owned trees: never prune or copy through
|
|
@@ -343,40 +374,57 @@ function installSkills(opts) {
|
|
|
343
374
|
}
|
|
344
375
|
|
|
345
376
|
const sharedCoreSrc = join(pipelineSrc, "skills", "shared", "core");
|
|
346
|
-
const sharedExternalSrc = join(pipelineSrc, "skills", "shared", "external");
|
|
347
377
|
|
|
348
378
|
let claudeSkillCount = 0;
|
|
349
379
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
380
|
+
// Claude Code no longer receives a local copy of the stack skills. The
|
|
381
|
+
// marketplace plugins (`ai-<stack>-toolkit`) are its only stack-skill source -
|
|
382
|
+
// a local copy duplicated ~110 skills against the enabled plugins (~10k
|
|
383
|
+
// tokens/session) and shadowed the plugin the moment it went stale. Only the
|
|
384
|
+
// two pipeline-owned compliance catalogs stay local (PIPELINE_LOCAL_SKILLS);
|
|
385
|
+
// they are deliberately not published to the marketplace.
|
|
386
|
+
ensureDir(dest);
|
|
387
|
+
for (const name of PIPELINE_LOCAL_SKILLS) {
|
|
388
|
+
const from = join(sharedCoreSrc, name);
|
|
389
|
+
if (!existsSync(from)) continue;
|
|
390
|
+
if (!useSymlinks) wipeDir(join(dest, name));
|
|
391
|
+
copyDir(from, join(dest, name), { useSymlinks });
|
|
392
|
+
claudeSkillCount += countFiles(from);
|
|
353
393
|
}
|
|
354
394
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
395
|
+
// Single-standard enforcement: Claude Code invokes pipeline commands via
|
|
396
|
+
// the `/multi-agent:*` slash-command namespace, so prune duplicate skill
|
|
397
|
+
// dirs left by older installs that copied shared/core wholesale. This used
|
|
398
|
+
// to live inside the external-copy branch; it must run unconditionally or
|
|
399
|
+
// the 51 multi-agent-* dirs return on the first install after a migration.
|
|
400
|
+
const pruned = pruneLegacyMultiAgentSkills(dest);
|
|
401
|
+
if (pruned > 0) {
|
|
402
|
+
console.log(
|
|
403
|
+
` -> pruned ${pruned} legacy multi-agent-* skill dirs (slash commands cover these)`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
366
406
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
);
|
|
376
|
-
}
|
|
407
|
+
// Migration prune: an older install delivered the full external catalog here.
|
|
408
|
+
// Its manifest names exactly what was delivered, so removal is manifest-scoped -
|
|
409
|
+
// user-authored skills in the same directory are untouched.
|
|
410
|
+
const migrated = pruneManifestDeliveredSkills(dest);
|
|
411
|
+
if (migrated > 0) {
|
|
412
|
+
console.log(
|
|
413
|
+
` -> migration: removed ${migrated} stack skill dir(s) previously copied here (now plugin-only)`,
|
|
414
|
+
);
|
|
377
415
|
}
|
|
378
416
|
|
|
379
|
-
//
|
|
417
|
+
// Pre-manifest installs (<= v14.x) never wrote that manifest, so the branch
|
|
418
|
+
// above cannot fire for them and the full 151-dir catalog would outlive every
|
|
419
|
+
// upgrade. For those, prune by proof instead of by name: a dir byte-identical
|
|
420
|
+
// to the shipped catalog contains nothing of the user's and goes now; a dir
|
|
421
|
+
// that differs is kept unless --prune-external explicitly opts in (and even
|
|
422
|
+
// then a `local-only: true` SKILL.md keeps it).
|
|
423
|
+
migratePreManifestExternalSkills(dest, join(pipelineSrc, "skills", "shared", "external"), {
|
|
424
|
+
pruneExternal: Boolean(pruneExternal),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Figma component skills moved to the ai-<platform>-toolkit marketplace
|
|
380
428
|
// plugin and are no longer bundled here. Prune any stale copies left by a
|
|
381
429
|
// pre-migration install so the installed tree stays consistent.
|
|
382
430
|
for (const sub of ["figma-ios", "figma-android", "figma-common", "figma-to-component"]) {
|
|
@@ -410,21 +458,163 @@ function installSkills(opts) {
|
|
|
410
458
|
console.log(` -> pruned ${orphans} stale flat skill file(s) from an older layout`);
|
|
411
459
|
}
|
|
412
460
|
|
|
413
|
-
console.log(` -> ${claudeSkillCount} skill files installed to ${dest}`);
|
|
461
|
+
console.log(` -> ${claudeSkillCount} skill files installed to ${dest} (stack skills: marketplace plugins)`);
|
|
414
462
|
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
463
|
+
// The shared skills README described the full local catalog; with the catalog
|
|
464
|
+
// gone it would document skills that are not here. Drop it, including copies
|
|
465
|
+
// an older install left behind.
|
|
466
|
+
const claudeReadme = join(dest, "README.md");
|
|
467
|
+
if (existsSync(claudeReadme) && !isDryRun()) {
|
|
419
468
|
try {
|
|
420
|
-
|
|
421
|
-
console.log(` ->
|
|
469
|
+
rmSync(claudeReadme);
|
|
470
|
+
console.log(` -> removed ${claudeReadme} (local catalog no longer installed)`);
|
|
422
471
|
} catch {
|
|
423
472
|
/* non-fatal */
|
|
424
473
|
}
|
|
425
474
|
}
|
|
426
475
|
}
|
|
427
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Remove the external stack skills an older install copied into `dest`, scoped
|
|
479
|
+
* strictly to the delivery manifest that install wrote. NOTICE files rode along
|
|
480
|
+
* with the external catalog, so they go with it.
|
|
481
|
+
*
|
|
482
|
+
* @param {string} dest - the installed skills directory
|
|
483
|
+
* @returns {number} directories removed
|
|
484
|
+
*/
|
|
485
|
+
function pruneManifestDeliveredSkills(dest) {
|
|
486
|
+
const manifestPath = join(dest, EXTERNAL_SKILLS_MANIFEST);
|
|
487
|
+
if (!existsSync(manifestPath)) return 0;
|
|
488
|
+
if (isDryRun()) {
|
|
489
|
+
console.log(` [dry-run] would remove externally-delivered skills named in ${manifestPath}`);
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
492
|
+
let removed = 0;
|
|
493
|
+
try {
|
|
494
|
+
const names = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
495
|
+
for (const name of Array.isArray(names) ? names : []) {
|
|
496
|
+
// The manifest is untrusted input feeding a recursive rmSync: a name
|
|
497
|
+
// carrying a path separator or leading dot would escape the skills dir
|
|
498
|
+
// (join normalizes "../x"), so only plain directory names pass.
|
|
499
|
+
if (typeof name !== "string" || /[/\\]/.test(name) || name.startsWith(".")) continue;
|
|
500
|
+
// The two local compliance catalogs are never external-delivered, but a
|
|
501
|
+
// corrupted manifest must not be able to take them out.
|
|
502
|
+
if (PIPELINE_LOCAL_SKILLS.includes(name)) continue;
|
|
503
|
+
const dir = join(dest, name);
|
|
504
|
+
if (!existsSync(dir)) continue;
|
|
505
|
+
try {
|
|
506
|
+
rmSync(dir, { recursive: true, force: true });
|
|
507
|
+
removed++;
|
|
508
|
+
} catch {
|
|
509
|
+
/* non-fatal */
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
for (const entry of readdirSync(dest)) {
|
|
513
|
+
if (entry.startsWith("NOTICE-") && entry.endsWith(".md")) {
|
|
514
|
+
try {
|
|
515
|
+
rmSync(join(dest, entry));
|
|
516
|
+
} catch {
|
|
517
|
+
/* non-fatal */
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
rmSync(manifestPath, { force: true });
|
|
522
|
+
} catch {
|
|
523
|
+
/* unreadable manifest: leave everything in place rather than guess */
|
|
524
|
+
}
|
|
525
|
+
return removed;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Migration for pre-manifest installs (<= v14.x): the external catalog was
|
|
530
|
+
* copied here with no delivery manifest, so ours and the user's are told apart
|
|
531
|
+
* by content, not by name. Byte-identical to the shipped catalog = provably
|
|
532
|
+
* ours, removed. Different = kept, reported, and removable only via the
|
|
533
|
+
* explicit `--prune-external` flag (which still honors `local-only: true`).
|
|
534
|
+
*
|
|
535
|
+
* @param {string} dest - the installed skills directory
|
|
536
|
+
* @param {string} externalSrc - absolute path to `pipeline/skills/shared/external/`
|
|
537
|
+
* @param {{ pruneExternal: boolean }} opts
|
|
538
|
+
*/
|
|
539
|
+
function migratePreManifestExternalSkills(dest, externalSrc, opts) {
|
|
540
|
+
if (!existsSync(externalSrc) || !existsSync(dest)) return;
|
|
541
|
+
const { pruneExternal } = opts;
|
|
542
|
+
let identical = 0;
|
|
543
|
+
let flagged = 0;
|
|
544
|
+
const leftover = [];
|
|
545
|
+
for (const entry of readdirSync(externalSrc, { withFileTypes: true })) {
|
|
546
|
+
const name = entry.name;
|
|
547
|
+
if (PIPELINE_LOCAL_SKILLS.includes(name)) continue;
|
|
548
|
+
const stale = join(dest, name);
|
|
549
|
+
if (!existsSync(stale)) continue;
|
|
550
|
+
|
|
551
|
+
// NOTICE-*.md files rode along with the catalog as plain files.
|
|
552
|
+
if (!entry.isDirectory()) {
|
|
553
|
+
if (!/^NOTICE-.*\.md$/.test(name)) continue;
|
|
554
|
+
if (isDryRun()) {
|
|
555
|
+
console.log(` [dry-run] would prune stale ${name} (pre-manifest catalog copy)`);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
rmSync(stale, { force: true });
|
|
560
|
+
identical++;
|
|
561
|
+
} catch {
|
|
562
|
+
/* non-fatal */
|
|
563
|
+
}
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let provablyOurs;
|
|
568
|
+
try {
|
|
569
|
+
provablyOurs = dirsIdentical(join(externalSrc, name), stale);
|
|
570
|
+
} catch {
|
|
571
|
+
provablyOurs = false;
|
|
572
|
+
}
|
|
573
|
+
if (provablyOurs) {
|
|
574
|
+
if (isDryRun()) {
|
|
575
|
+
console.log(` [dry-run] would prune ${name}/ (identical to shipped catalog)`);
|
|
576
|
+
identical++;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
rmSync(stale, { recursive: true, force: true });
|
|
581
|
+
identical++;
|
|
582
|
+
} catch {
|
|
583
|
+
/* non-fatal */
|
|
584
|
+
}
|
|
585
|
+
} else if (pruneExternal && !isLocalOnlySkill(stale)) {
|
|
586
|
+
if (isDryRun()) {
|
|
587
|
+
console.log(` [dry-run] would prune ${name}/ (--prune-external)`);
|
|
588
|
+
flagged++;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
rmSync(stale, { recursive: true, force: true });
|
|
593
|
+
flagged++;
|
|
594
|
+
} catch {
|
|
595
|
+
/* non-fatal */
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
leftover.push(name);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (identical > 0) {
|
|
602
|
+
console.log(
|
|
603
|
+
` -> migration: pruned ${identical} pre-manifest catalog entr(ies) identical to the shipped catalog (now plugin-only)`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
if (flagged > 0) {
|
|
607
|
+
console.log(` -> --prune-external: removed ${flagged} catalog-named skill dir(s) that had local differences`);
|
|
608
|
+
}
|
|
609
|
+
if (leftover.length > 0) {
|
|
610
|
+
console.log(
|
|
611
|
+
` note: ${leftover.length} skill dir(s) share catalog names but differ from the shipped catalog - kept ` +
|
|
612
|
+
`(cannot tell an older delivery from user edits). Remove them too with: node install.js --claude --prune-external`,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
|
|
428
618
|
function ensureClaudeMd(home, pipelineSrc) {
|
|
429
619
|
console.log(" [Claude Code] Checking CLAUDE.md template...");
|
|
430
620
|
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
|
|
|
@@ -44,9 +52,6 @@ export const AGENTS_MD_START_MARKER = "# Multi-Agent Development Pipeline";
|
|
|
44
52
|
/** Explicit end marker written after the pipeline section. */
|
|
45
53
|
export const AGENTS_MD_END_MARKER = "<!-- multi-agent-pipeline:codex-instructions:end -->";
|
|
46
54
|
|
|
47
|
-
/** Re-exported for the uninstaller and the Codex install smoke. */
|
|
48
|
-
export { MCP_SERVER_NAME } from "./_mcp-register.mjs";
|
|
49
|
-
|
|
50
55
|
/**
|
|
51
56
|
* `$HOME/.claude/...` path rewrites applied to every file installed into the
|
|
52
57
|
* Codex tree.
|
|
@@ -269,7 +274,7 @@ function pruneLegacyCommandSkills(skillsDir) {
|
|
|
269
274
|
}
|
|
270
275
|
|
|
271
276
|
/**
|
|
272
|
-
* Install the reference tree: the shared `multi-agent-refs/` docs plus
|
|
277
|
+
* Install the reference tree: the shared `multi-agent-refs/` docs plus every
|
|
273
278
|
* sub-command specs, all path-rewritten. These are read on demand and never
|
|
274
279
|
* enter the skills block.
|
|
275
280
|
*/
|
|
@@ -394,7 +399,24 @@ function installSkillRefs(pipelineSrc, dest, home, platformFlag) {
|
|
|
394
399
|
wipeDir(dest);
|
|
395
400
|
|
|
396
401
|
let count = 0;
|
|
397
|
-
if (existsSync(externalSrc))
|
|
402
|
+
if (existsSync(externalSrc)) {
|
|
403
|
+
// Active-stack filter (same contract as Copilot): Codex used to receive all
|
|
404
|
+
// 151 external skills regardless of stack - platformFlag only shaped the
|
|
405
|
+
// authored-plugin delivery below. The enabled-plugin set from
|
|
406
|
+
// ~/.claude/settings.json now bounds this copy too, so a Claude+Codex user
|
|
407
|
+
// sees one consistent stack surface across both hosts. The manifest makes
|
|
408
|
+
// the delivery inspectable; the wholesale wipeDir above already handles
|
|
409
|
+
// staleness on every install.
|
|
410
|
+
const { names: enabled, source: selectionSource } = pluginsToDeliver(home, platformFlag);
|
|
411
|
+
const { keep, skipped } = partitionExternalSkillsByPlugins(externalSrc, enabled);
|
|
412
|
+
for (const name of keep) {
|
|
413
|
+
count += copyTreeRewritten(join(externalSrc, name), join(dest, name));
|
|
414
|
+
}
|
|
415
|
+
writeExternalSkillsManifest(dest, keep);
|
|
416
|
+
console.log(
|
|
417
|
+
` -> stack filter (${selectionSource}): ${keep.length} kept, ${skipped.length} skipped`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
398
420
|
|
|
399
421
|
// `shared/core` also holds two skills that are NOT pipeline sub-commands:
|
|
400
422
|
// apple-archive-compliance and google-play-compliance. Copying only `external`
|