@mmerterden/multi-agent-pipeline 15.0.0 → 15.2.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 +127 -0
- package/README.md +9 -9
- package/README.tr.md +9 -9
- package/SECURITY.md +43 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +1 -1
- package/docs/architecture.md +9 -9
- package/docs/ecosystem.md +10 -10
- package/index.js +4 -1
- package/install/_common.mjs +44 -2
- package/install/_platform-filter.mjs +6 -131
- package/install/_plugin-skills.mjs +17 -28
- package/install/claude.mjs +111 -6
- package/install/codex.mjs +0 -3
- package/install/copilot.mjs +36 -1
- package/install/index.mjs +3 -1
- package/package.json +2 -2
- package/pipeline/commands/multi-agent/refactor/SKILL.md +36 -1
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/stack/SKILL.md +18 -8
- package/pipeline/commands/multi-agent/update/SKILL.md +31 -4
- package/pipeline/lib/parse-complaints.sh +12 -2
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +1 -1
- package/pipeline/multi-agent-refs/phases/operations.md +7 -1
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +6 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +2 -1
- package/pipeline/preferences-template.json +6 -0
- package/pipeline/schemas/prefs.schema.json +27 -0
- package/pipeline/scripts/README.md +4 -3
- package/pipeline/scripts/check-derived-drift.mjs +5 -2
- package/pipeline/scripts/match-skills.mjs +4 -0
- package/pipeline/scripts/migrate-prefs.mjs +5 -1
- package/pipeline/scripts/phase-tracker.sh +19 -0
- package/pipeline/scripts/uninstall.mjs +3 -1
- package/pipeline/scripts/usage-report.mjs +457 -0
- package/pipeline/scripts/validate-complaint-doc.mjs +32 -11
- package/pipeline/skills/.skill-manifest.json +4 -4
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +153 -90
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +18 -8
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
|
@@ -1,147 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Stack partition + delivery manifest for the external skill catalog.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
4
|
+
* The pre-v15 prefix classifier and its filtered-copy path
|
|
5
|
+
* (`classifyExternalSkill` / `copyExternalSkillsFiltered`) were removed once the
|
|
6
|
+
* routing table (`_stack-routing.mjs`) became the single source of stack
|
|
7
|
+
* affinity - `partitionExternalSkillsByPlugins` is what every installer uses.
|
|
6
8
|
*
|
|
7
9
|
* @module install/_platform-filter
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import { readdirSync, writeFileSync } from "fs";
|
|
11
13
|
import { join } from "path";
|
|
12
|
-
import {
|
|
14
|
+
import { isDryRun } from "./_common.mjs";
|
|
13
15
|
import { routeSkill } from "../pipeline/scripts/_stack-routing.mjs";
|
|
14
16
|
|
|
15
17
|
/** Uninstall reads this to know exactly which external skill dirs are ours to remove. */
|
|
16
18
|
export const EXTERNAL_SKILLS_MANIFEST = ".external-skills-manifest.json";
|
|
17
19
|
|
|
18
|
-
/**
|
|
19
|
-
* Heuristic prefix lists - intentionally loose so new skills added to
|
|
20
|
-
* `shared/external/` are classified by name (no manifest required). A skill
|
|
21
|
-
* that doesn't match either list is treated as "generic" and copies for every
|
|
22
|
-
* platform.
|
|
23
|
-
*/
|
|
24
|
-
const IOS_PREFIXES = [
|
|
25
|
-
"swift",
|
|
26
|
-
"swiftui",
|
|
27
|
-
"ios",
|
|
28
|
-
"apple",
|
|
29
|
-
"xcode",
|
|
30
|
-
"uikit",
|
|
31
|
-
"watchkit",
|
|
32
|
-
"passkit",
|
|
33
|
-
"storekit",
|
|
34
|
-
"healthkit",
|
|
35
|
-
"activitykit",
|
|
36
|
-
"weatherkit",
|
|
37
|
-
"pencilkit",
|
|
38
|
-
"callkit",
|
|
39
|
-
"eventkit",
|
|
40
|
-
"homekit",
|
|
41
|
-
"mapkit",
|
|
42
|
-
"musickit",
|
|
43
|
-
"permissionkit",
|
|
44
|
-
"speech",
|
|
45
|
-
"metrickit",
|
|
46
|
-
"widgetkit",
|
|
47
|
-
"vision",
|
|
48
|
-
"app-store",
|
|
49
|
-
"app-clips",
|
|
50
|
-
"app-intents",
|
|
51
|
-
"app-tracking",
|
|
52
|
-
"hig-",
|
|
53
|
-
"coreml",
|
|
54
|
-
"core-nfc",
|
|
55
|
-
"core-bluetooth",
|
|
56
|
-
"core-motion",
|
|
57
|
-
"cloudkit",
|
|
58
|
-
"live-activities",
|
|
59
|
-
"realitykit",
|
|
60
|
-
"tipkit",
|
|
61
|
-
"alarmkit",
|
|
62
|
-
"energykit",
|
|
63
|
-
"natural-language",
|
|
64
|
-
"authentication",
|
|
65
|
-
"background-processing",
|
|
66
|
-
"contacts",
|
|
67
|
-
"device-integrity",
|
|
68
|
-
"debugging-instruments",
|
|
69
|
-
"macos-",
|
|
70
|
-
"ios-",
|
|
71
|
-
"photos-camera",
|
|
72
|
-
"push-notifications",
|
|
73
|
-
"shareplay",
|
|
74
|
-
"apple-on-device",
|
|
75
|
-
"swiftdata",
|
|
76
|
-
"swift-",
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
const ANDROID_PREFIXES = [
|
|
80
|
-
"android",
|
|
81
|
-
"kotlin",
|
|
82
|
-
"jetpack",
|
|
83
|
-
"compose",
|
|
84
|
-
"room",
|
|
85
|
-
"retrofit",
|
|
86
|
-
"gradle",
|
|
87
|
-
"material",
|
|
88
|
-
"play-store",
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Classify an external skill directory name into its platform affinity.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} skillName - directory name under `shared/external/`
|
|
95
|
-
* @returns {"ios" | "android" | "generic"}
|
|
96
|
-
*/
|
|
97
|
-
export function classifyExternalSkill(skillName) {
|
|
98
|
-
const lower = skillName.toLowerCase();
|
|
99
|
-
if (IOS_PREFIXES.some((p) => lower.startsWith(p))) return "ios";
|
|
100
|
-
if (ANDROID_PREFIXES.some((p) => lower.startsWith(p))) return "android";
|
|
101
|
-
return "generic";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Platform-filtered copy of `shared/external/` skills.
|
|
106
|
-
*
|
|
107
|
-
* @param {string} externalSrc - absolute path to `pipeline/skills/shared/external/`
|
|
108
|
-
* @param {string} dest - absolute path to target install dir
|
|
109
|
-
* @param {{ platformFlag: "ios"|"android"|"all", useSymlinks?: boolean }} opts
|
|
110
|
-
* @returns {{ copied: number, skipped: number }}
|
|
111
|
-
*/
|
|
112
|
-
export function copyExternalSkillsFiltered(externalSrc, dest, opts) {
|
|
113
|
-
const { platformFlag, useSymlinks = false } = opts;
|
|
114
|
-
const allNames = readdirSync(externalSrc, { withFileTypes: true })
|
|
115
|
-
.filter((e) => e.isDirectory())
|
|
116
|
-
.map((e) => e.name);
|
|
117
|
-
|
|
118
|
-
if (platformFlag === "all") {
|
|
119
|
-
copyDir(externalSrc, dest, { useSymlinks });
|
|
120
|
-
writeExternalSkillsManifest(dest, allNames);
|
|
121
|
-
return { copied: countFiles(externalSrc), skipped: 0 };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
ensureDir(dest);
|
|
125
|
-
let copied = 0;
|
|
126
|
-
let skipped = 0;
|
|
127
|
-
const delivered = [];
|
|
128
|
-
for (const name of allNames) {
|
|
129
|
-
const classification = classifyExternalSkill(name);
|
|
130
|
-
const shouldCopy = classification === "generic" || classification === platformFlag;
|
|
131
|
-
const src = join(externalSrc, name);
|
|
132
|
-
const dst = join(dest, name);
|
|
133
|
-
if (shouldCopy) {
|
|
134
|
-
copyDir(src, dst, { useSymlinks });
|
|
135
|
-
copied += countFiles(src);
|
|
136
|
-
delivered.push(name);
|
|
137
|
-
} else {
|
|
138
|
-
skipped += 1;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
writeExternalSkillsManifest(dest, delivered);
|
|
142
|
-
return { copied, skipped };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
20
|
/**
|
|
146
21
|
* Enabled-stack partition of the external skill catalog.
|
|
147
22
|
*
|
|
@@ -33,10 +33,10 @@ 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
|
-
|
|
39
|
+
const STACK_PLUGINS = Object.freeze([
|
|
40
40
|
{ name: "ai-ios-toolkit", platform: "ios" },
|
|
41
41
|
{ name: "ai-android-toolkit", platform: "android" },
|
|
42
42
|
{ name: "ai-frontend-toolkit", platform: "all" },
|
|
@@ -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,17 +104,22 @@ 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 || {})
|
|
@@ -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(
|
|
@@ -282,27 +287,11 @@ export function installAuthoredPluginSkills(opts) {
|
|
|
282
287
|
};
|
|
283
288
|
}
|
|
284
289
|
|
|
285
|
-
/**
|
|
286
|
-
* Names already present in a flat skills directory, so a second delivery pass does
|
|
287
|
-
* not overwrite what the pipeline itself installed.
|
|
288
|
-
*
|
|
289
|
-
* @param {string} dir
|
|
290
|
-
* @returns {Set<string>}
|
|
291
|
-
*/
|
|
292
|
-
export function existingSkillNames(dir) {
|
|
293
|
-
if (!existsSync(dir)) return new Set();
|
|
294
|
-
return new Set(
|
|
295
|
-
readdirSync(dir, { withFileTypes: true })
|
|
296
|
-
.filter((e) => e.isDirectory())
|
|
297
|
-
.map((e) => e.name),
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
290
|
/**
|
|
302
291
|
* Skill names the PIPELINE owns, derived from its source tree.
|
|
303
292
|
*
|
|
304
|
-
* This is what `skipNames` should be. Callers used to pass
|
|
305
|
-
*
|
|
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
|
|
306
295
|
* did protect pipeline skills from being shadowed by a plugin, but also meant a
|
|
307
296
|
* plugin skill was copied exactly once and then frozen: on the second install its
|
|
308
297
|
* own name was "already there", so it was skipped, forever. A stale plugin skill
|
package/install/claude.mjs
CHANGED
|
@@ -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,
|
|
@@ -63,10 +65,11 @@ export const PIPELINE_LOCAL_SKILLS = ["apple-archive-compliance", "google-play-c
|
|
|
63
65
|
* indexOnly: boolean,
|
|
64
66
|
* useSymlinks: boolean,
|
|
65
67
|
* platformFlag: "ios"|"android"|"all",
|
|
68
|
+
* pruneExternal?: boolean,
|
|
66
69
|
* }} ctx
|
|
67
70
|
*/
|
|
68
71
|
export function installClaude(ctx) {
|
|
69
|
-
const { home, pipelineSrc, indexOnly, useSymlinks, platformFlag } = ctx;
|
|
72
|
+
const { home, pipelineSrc, indexOnly, useSymlinks, platformFlag, pruneExternal } = ctx;
|
|
70
73
|
|
|
71
74
|
const CLAUDE_COMMANDS = join(home, ".claude", "commands");
|
|
72
75
|
const CLAUDE_AGENTS = join(home, ".claude", "agents");
|
|
@@ -94,7 +97,7 @@ export function installClaude(ctx) {
|
|
|
94
97
|
installSchemas(pipelineSrc, CLAUDE_SCHEMAS, useSymlinks);
|
|
95
98
|
installLib(pipelineSrc, CLAUDE_LIB, useSymlinks);
|
|
96
99
|
runPreDeployScans(pipelineSrc);
|
|
97
|
-
installSkills({ pipelineSrc, dest: CLAUDE_SKILLS, indexOnly, useSymlinks, platformFlag });
|
|
100
|
+
installSkills({ pipelineSrc, dest: CLAUDE_SKILLS, indexOnly, useSymlinks, platformFlag, pruneExternal });
|
|
98
101
|
ensureClaudeMd(home, pipelineSrc);
|
|
99
102
|
ensurePreferences(PREFS_PATH, pipelineSrc);
|
|
100
103
|
configureSettings(home);
|
|
@@ -355,7 +358,7 @@ function runPreDeployScans(pipelineSrc) {
|
|
|
355
358
|
}
|
|
356
359
|
|
|
357
360
|
function installSkills(opts) {
|
|
358
|
-
const { pipelineSrc, dest, indexOnly, useSymlinks } = opts;
|
|
361
|
+
const { pipelineSrc, dest, indexOnly, useSymlinks, pruneExternal } = opts;
|
|
359
362
|
console.log(" [Claude Code] Installing skills...");
|
|
360
363
|
|
|
361
364
|
// Same symlink guard as the other owned trees: never prune or copy through
|
|
@@ -403,9 +406,7 @@ function installSkills(opts) {
|
|
|
403
406
|
|
|
404
407
|
// Migration prune: an older install delivered the full external catalog here.
|
|
405
408
|
// Its manifest names exactly what was delivered, so removal is manifest-scoped -
|
|
406
|
-
// user-authored skills in the same directory are untouched.
|
|
407
|
-
// nothing is guessed; the stale copy then outlives this install and the user
|
|
408
|
-
// can clean it via uninstall --claude.
|
|
409
|
+
// user-authored skills in the same directory are untouched.
|
|
409
410
|
const migrated = pruneManifestDeliveredSkills(dest);
|
|
410
411
|
if (migrated > 0) {
|
|
411
412
|
console.log(
|
|
@@ -413,6 +414,16 @@ function installSkills(opts) {
|
|
|
413
414
|
);
|
|
414
415
|
}
|
|
415
416
|
|
|
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
|
+
|
|
416
427
|
// Figma component skills moved to the ai-<platform>-toolkit marketplace
|
|
417
428
|
// plugin and are no longer bundled here. Prune any stale copies left by a
|
|
418
429
|
// pre-migration install so the installed tree stays consistent.
|
|
@@ -482,6 +493,10 @@ function pruneManifestDeliveredSkills(dest) {
|
|
|
482
493
|
try {
|
|
483
494
|
const names = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
484
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;
|
|
485
500
|
// The two local compliance catalogs are never external-delivered, but a
|
|
486
501
|
// corrupted manifest must not be able to take them out.
|
|
487
502
|
if (PIPELINE_LOCAL_SKILLS.includes(name)) continue;
|
|
@@ -510,6 +525,96 @@ function pruneManifestDeliveredSkills(dest) {
|
|
|
510
525
|
return removed;
|
|
511
526
|
}
|
|
512
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
|
+
|
|
513
618
|
function ensureClaudeMd(home, pipelineSrc) {
|
|
514
619
|
console.log(" [Claude Code] Checking CLAUDE.md template...");
|
|
515
620
|
const CLAUDE_MD = join(home, ".claude", "CLAUDE.md");
|
package/install/codex.mjs
CHANGED
|
@@ -52,9 +52,6 @@ export const AGENTS_MD_START_MARKER = "# Multi-Agent Development Pipeline";
|
|
|
52
52
|
/** Explicit end marker written after the pipeline section. */
|
|
53
53
|
export const AGENTS_MD_END_MARKER = "<!-- multi-agent-pipeline:codex-instructions:end -->";
|
|
54
54
|
|
|
55
|
-
/** Re-exported for the uninstaller and the Codex install smoke. */
|
|
56
|
-
export { MCP_SERVER_NAME } from "./_mcp-register.mjs";
|
|
57
|
-
|
|
58
55
|
/**
|
|
59
56
|
* `$HOME/.claude/...` path rewrites applied to every file installed into the
|
|
60
57
|
* Codex tree.
|
package/install/copilot.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
copyFile,
|
|
17
17
|
copySkillsIndex,
|
|
18
18
|
countFiles,
|
|
19
|
+
dirsIdentical,
|
|
19
20
|
ensureDir,
|
|
20
21
|
ensureRealDir,
|
|
21
22
|
isDryRun,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
writeFile,
|
|
26
27
|
} from "./_common.mjs";
|
|
27
28
|
import {
|
|
29
|
+
EXTERNAL_SKILLS_MANIFEST,
|
|
28
30
|
partitionExternalSkillsByPlugins,
|
|
29
31
|
writeExternalSkillsManifest,
|
|
30
32
|
} from "./_platform-filter.mjs";
|
|
@@ -372,6 +374,15 @@ function installSkills(opts) {
|
|
|
372
374
|
const { names: enabled, source: selectionSource } = pluginsToDeliver(home, platformFlag);
|
|
373
375
|
const { keep, skipped } = partitionExternalSkillsByPlugins(sharedExternalSrc, enabled);
|
|
374
376
|
ensureDir(dest);
|
|
377
|
+
// Read the PREVIOUS delivery manifest before this install overwrites it -
|
|
378
|
+
// it is what makes the stale prune below manifest-scoped.
|
|
379
|
+
let prevDelivered = null;
|
|
380
|
+
try {
|
|
381
|
+
const parsed = JSON.parse(readFileSync(join(dest, EXTERNAL_SKILLS_MANIFEST), "utf-8"));
|
|
382
|
+
if (Array.isArray(parsed)) prevDelivered = new Set(parsed.filter((n) => typeof n === "string"));
|
|
383
|
+
} catch {
|
|
384
|
+
prevDelivered = null;
|
|
385
|
+
}
|
|
375
386
|
for (const name of keep) {
|
|
376
387
|
const src = join(sharedExternalSrc, name);
|
|
377
388
|
const dst = join(dest, name);
|
|
@@ -380,10 +391,29 @@ function installSkills(opts) {
|
|
|
380
391
|
copilotSkillCount += countFiles(src);
|
|
381
392
|
}
|
|
382
393
|
// Stale prune: a skill delivered by a previous, wider stack selection must
|
|
383
|
-
// leave when the selection narrows, or the old stack lingers forever.
|
|
394
|
+
// leave when the selection narrows, or the old stack lingers forever. Scope:
|
|
395
|
+
// only dirs the previous manifest names (delivered by us), or - on a
|
|
396
|
+
// pre-manifest install - dirs byte-identical to the shipped catalog. A
|
|
397
|
+
// user-authored dir that happens to share a catalog name matches neither
|
|
398
|
+
// and is never touched.
|
|
399
|
+
let keptForeign = 0;
|
|
384
400
|
for (const name of skipped) {
|
|
385
401
|
const stale = join(dest, name);
|
|
386
402
|
if (!existsSync(stale) || isDryRun()) continue;
|
|
403
|
+
let ours;
|
|
404
|
+
if (prevDelivered) {
|
|
405
|
+
ours = prevDelivered.has(name);
|
|
406
|
+
} else {
|
|
407
|
+
try {
|
|
408
|
+
ours = dirsIdentical(join(sharedExternalSrc, name), stale);
|
|
409
|
+
} catch {
|
|
410
|
+
ours = false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (!ours) {
|
|
414
|
+
keptForeign++;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
387
417
|
try {
|
|
388
418
|
rmSync(stale, { recursive: true, force: true });
|
|
389
419
|
} catch {
|
|
@@ -392,6 +422,11 @@ function installSkills(opts) {
|
|
|
392
422
|
}
|
|
393
423
|
writeExternalSkillsManifest(dest, keep);
|
|
394
424
|
console.log(` -> stack filter (${selectionSource}): ${keep.length} kept, ${skipped.length} skipped`);
|
|
425
|
+
if (keptForeign > 0) {
|
|
426
|
+
console.log(
|
|
427
|
+
` note: ${keptForeign} skipped-stack dir(s) kept - not delivered by this pipeline (no manifest entry, differs from catalog)`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
395
430
|
}
|
|
396
431
|
|
|
397
432
|
// The pre-migration figma-* skill copies are gone; the plugin owns component work
|
package/install/index.mjs
CHANGED
|
@@ -45,7 +45,7 @@ export async function runInstall(argv) {
|
|
|
45
45
|
// `multi-agent-pipeline install --all` (via bin, argv[2]="install")
|
|
46
46
|
const flags = argv.slice(2).filter((a) => a !== "install");
|
|
47
47
|
|
|
48
|
-
const KNOWN_FLAGS = [...TOOL_FLAGS, "--all", "--link", "--index-only", "--dry-run"];
|
|
48
|
+
const KNOWN_FLAGS = [...TOOL_FLAGS, "--all", "--link", "--index-only", "--dry-run", "--prune-external"];
|
|
49
49
|
const KNOWN_PREFIXES = ["--target=", "--platform="];
|
|
50
50
|
const unknown = flags.filter(
|
|
51
51
|
(f) =>
|
|
@@ -74,6 +74,7 @@ export async function runInstall(argv) {
|
|
|
74
74
|
|
|
75
75
|
const useSymlinks = flags.includes("--link");
|
|
76
76
|
const indexOnly = flags.includes("--index-only");
|
|
77
|
+
const pruneExternal = flags.includes("--prune-external");
|
|
77
78
|
const platformFlag = parsePlatformFlag(flags);
|
|
78
79
|
|
|
79
80
|
const installerCtx = {
|
|
@@ -82,6 +83,7 @@ export async function runInstall(argv) {
|
|
|
82
83
|
indexOnly,
|
|
83
84
|
useSymlinks,
|
|
84
85
|
platformFlag,
|
|
86
|
+
pruneExternal,
|
|
85
87
|
};
|
|
86
88
|
|
|
87
89
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.2.0",
|
|
4
4
|
"description": "8-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -68,11 +68,11 @@
|
|
|
68
68
|
"install.js",
|
|
69
69
|
"install/**/*",
|
|
70
70
|
"pipeline/**/*",
|
|
71
|
-
"pipeline/scripts/**/*",
|
|
72
71
|
"docs/**/*",
|
|
73
72
|
"README.md",
|
|
74
73
|
"CHANGELOG.md",
|
|
75
74
|
"LICENSE",
|
|
75
|
+
"SECURITY.md",
|
|
76
76
|
"!pipeline/scripts/smoke-figma-config-schema.sh",
|
|
77
77
|
"!pipeline/scripts/smoke-personal-data.sh",
|
|
78
78
|
"!pipeline/scripts/validate-prefs.mjs",
|
|
@@ -18,10 +18,11 @@ Deep-analyses the current project, extracts the global best-practices worth adop
|
|
|
18
18
|
Step 0: BEST-PRACTICES Research the field, extract the best approaches, ADAPT them to our stack -> plan band A
|
|
19
19
|
Step 0b: DRIFT Check upstream-derived skills for updates we have not pulled -> plan band D
|
|
20
20
|
Step 0c: DEV-TOOLKIT Research current MCP practice + audit the companion dev-toolkit repo -> plan band E
|
|
21
|
+
Step 0d: RUN-ERRORS Read the local run-error ledger, rank recurring failures -> plan band F
|
|
21
22
|
Step 1: SCAN Walk the project structure (files, LOC, dependencies, CI, tests)
|
|
22
23
|
Step 2: ANALYZE 10 categories + an explicit BUG HUNT (real defects, not just scores) -> plan bands B, C
|
|
23
24
|
Step 3: SCORE Each category /10, overall /100
|
|
24
|
-
Step 4: PLAN Merge bands A (best-practice) + B (bugs) + C (improvements) + D (drift) + E (dev-toolkit)
|
|
25
|
+
Step 4: PLAN Merge bands A (best-practice) + B (bugs) + C (improvements) + D (drift) + E (dev-toolkit) + F (run-errors)
|
|
25
26
|
Step 5: ASK "Start the work?" - wait for user approval
|
|
26
27
|
Step 6: IMPLEMENT Apply approved items one by one (lint, test, commit)
|
|
27
28
|
Step 7: VERIFY Confirm every test + lint passes
|
|
@@ -34,6 +35,7 @@ Plan bands (all five feed the single Step 4 table):
|
|
|
34
35
|
- **C - Improvement**: quality/perf/DX gaps surfaced by the 10-category analysis.
|
|
35
36
|
- **D - Drift**: upstream updates to skills we derived from an external source.
|
|
36
37
|
- **E - Dev-toolkit**: current-practice gaps in the companion dev-toolkit MCP server (the pipeline's device and browser hands), applied in that repo.
|
|
38
|
+
- **F - Run-errors**: recurring real failures the pipeline actually hit across past runs, read from the local run-error ledger. These are lived evidence, not speculation - a failure that recurs across users or tasks is a prioritized improvement area.
|
|
37
39
|
|
|
38
40
|
## Step 0: BEST-PRACTICES - research the field, adapt to us
|
|
39
41
|
|
|
@@ -178,6 +180,39 @@ Rules for this band:
|
|
|
178
180
|
- A finding that changes the tool surface (new / renamed / removed tool) pairs with a pipeline-side item: bump the minimum toolkit version wherever a pipeline skill declares one.
|
|
179
181
|
- If the current working directory IS the toolkit repo, skip band E and let bands A/B/C cover it - never report the same finding twice.
|
|
180
182
|
|
|
183
|
+
## Step 0d: RUN-ERRORS - what the pipeline actually failed on
|
|
184
|
+
|
|
185
|
+
Before speculating about improvements, read what real runs already failed on. When usage logging is enabled, every terminated run appends its errors to a local ledger:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
LEDGER="$HOME/.claude/logs/multi-agent/errors-ledger.jsonl"
|
|
189
|
+
[ -f "$LEDGER" ] || echo "no run-error ledger yet - skip band F"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Each line is one run: `{ t, id, u, c, rp, ph, v, errs[] }`. The `errs[]` entries are cause tags (`<phase>:<cause>` halt reasons, `phase-<id>-failed`, `run-failed`).
|
|
193
|
+
|
|
194
|
+
1. Read the ledger (best-effort; a missing or unreadable file means skip band F, not a failure).
|
|
195
|
+
2. Group by error tag. For each tag compute: occurrences, distinct users affected, distinct repos, the phase it usually strikes, first + last seen, and which pipeline versions it spans (a tag that persists across versions is unfixed; one that stopped at a version is already resolved - do not re-raise it).
|
|
196
|
+
3. Rank by `occurrences x users-affected`. A failure that recurs across users or tasks is lived evidence, not a hypothesis - it outranks a speculative improvement.
|
|
197
|
+
4. For each surviving tag, trace it to the phase doc / script that emits that cause and propose the concrete fix.
|
|
198
|
+
|
|
199
|
+
This band mirrors the admin dashboard's "Gelişim alanları" panel, but reads the local ledger so it needs no auth and works offline. If `$ARGUMENTS` names a focus area, still read the ledger - a recurring run error in that area is the strongest possible signal.
|
|
200
|
+
|
|
201
|
+
Output (plan band F):
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
| # | Error tag | Occurrences | Users | Usual phase | Versions | Root cause (file) | Fix | In plan? |
|
|
205
|
+
|---|-----------|-------------|-------|-------------|----------|-------------------|-----|----------|
|
|
206
|
+
| 1 | 4:reviewer-json-invalid | 12 | 3 | 4 | 14.x-15.x | reviewer prompt lets prose leak | tighten schema instruction in phase-4-review.md | Yes (P0) |
|
|
207
|
+
| 2 | phase-3-failed | 5 | 2 | 3 | 15.0.x | build step misses a stack toolchain | add preflight in phase-3-dev.md | Yes (P1) |
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Rules for this band:
|
|
211
|
+
|
|
212
|
+
- The ledger is evidence of the past, not a spec. A tag that stopped recurring after a version bump is resolved - report it as resolved, do not add a plan item.
|
|
213
|
+
- Never quote a user's identity as blame. The `u` field is for counting distinct affected users, not for naming anyone in the plan.
|
|
214
|
+
- If the ledger is empty or absent, skip band F silently - it is additive signal, never a gate.
|
|
215
|
+
|
|
181
216
|
## Step 1: SCAN
|
|
182
217
|
|
|
183
218
|
```
|
|
@@ -66,7 +66,7 @@ Known legitimate domains are embedded in the scanner (github, anthropic, figma,
|
|
|
66
66
|
|
|
67
67
|
## Smoke - self-verification
|
|
68
68
|
|
|
69
|
-
`pipeline/scripts/smoke-skill-scan.sh` confirms the scanner triggers on positive fixtures for every severity and produces no false positives on the real tree.
|
|
69
|
+
`pipeline/scripts/smoke-skill-scan.sh` confirms the scanner triggers on positive fixtures for every severity and produces no false positives on the real tree. This is a maintainer-repo gate (smoke scripts are excluded from the npm package): it runs from a pipeline checkout and in CI, where it must be 13/13 green before a team rollout.
|
|
70
70
|
|
|
71
71
|
## Integration points
|
|
72
72
|
|
|
@@ -47,7 +47,8 @@ Multiple args union their plugin sets: `ios backend` → common + iOS + backend.
|
|
|
47
47
|
- `question` (in `outputLanguage`): which stacks should be active in this repo, noting the currently enabled ones
|
|
48
48
|
- `header`: "Stacks" (English, UI contract)
|
|
49
49
|
- `options` (4): `ios` / `android` / `frontend (web)` / `backend`, each `description` (in `outputLanguage`) naming the plugin it enables and marking the ones already enabled with "(currently on)" / "(şu an açık)"
|
|
50
|
-
-
|
|
50
|
+
- Map each selected label back to its canonical arg before step 2: the token before any parenthesis (`frontend (web)` → `frontend`).
|
|
51
|
+
- Empty selection or cancel → **status mode**: print the currently enabled `@multi-agent-plugins` plugins and exit without modifying anything. Status mode never runs the Implementation block below - that block is enable-mode only.
|
|
51
52
|
2. **Arg(s) present → enable mode.** Accept multiple space-separated stacks. Resolve each through the alias table (`web`→`frontend`, `mobile`→`ios android`, `fullstack`→`frontend backend`, `all`→every stack), union the plugin sets, then write the **current repo's** `.claude/settings.json`.
|
|
52
53
|
3. **Write rules:**
|
|
53
54
|
- every plugin in the union is set to `true`
|
|
@@ -75,17 +76,26 @@ ANDROID="ai-android-toolkit@${MP}"
|
|
|
75
76
|
FRONTEND="ai-frontend-toolkit@${MP}"
|
|
76
77
|
BACKEND="ai-backend-toolkit@${MP}"
|
|
77
78
|
|
|
79
|
+
# Enable-mode only: with zero args the write rules below would set every stack
|
|
80
|
+
# toolkit to false (nothing but $COMMON is in $ON), silently wiping the repo's
|
|
81
|
+
# selection. No args belongs to the picker / status path (Behaviour step 1).
|
|
82
|
+
if [ "$#" -eq 0 ]; then
|
|
83
|
+
echo "No stack given - nothing changed. Pass stacks (ios backend ...) or use the picker."
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
78
87
|
# resolve every arg through the alias table, union the ON set
|
|
79
88
|
ON="$COMMON"
|
|
80
89
|
for ARG in "$@"; do
|
|
81
90
|
case "$ARG" in
|
|
82
|
-
ios)
|
|
83
|
-
android)
|
|
84
|
-
frontend|web)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
ios) ON="$ON $IOS" ;;
|
|
92
|
+
android) ON="$ON $ANDROID" ;;
|
|
93
|
+
frontend|web) ON="$ON $FRONTEND" ;;
|
|
94
|
+
"frontend (web)") ON="$ON $FRONTEND" ;;
|
|
95
|
+
backend) ON="$ON $BACKEND" ;;
|
|
96
|
+
mobile) ON="$ON $IOS $ANDROID" ;;
|
|
97
|
+
fullstack) ON="$ON $FRONTEND $BACKEND" ;;
|
|
98
|
+
all) ON="$ON $IOS $ANDROID $FRONTEND $BACKEND" ;;
|
|
89
99
|
*) echo "Unknown stack '$ARG'. One of: ios android mobile backend frontend web fullstack all"; exit 1 ;;
|
|
90
100
|
esac
|
|
91
101
|
done
|