@mmerterden/multi-agent-pipeline 13.5.0 → 14.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +3 -3
  3. package/docs/features.md +1 -1
  4. package/install/_common.mjs +73 -0
  5. package/install/_mcp-register.mjs +70 -31
  6. package/install/_plugin-skills.mjs +73 -14
  7. package/install/claude.mjs +28 -4
  8. package/install/codex.mjs +33 -2
  9. package/install/copilot.mjs +145 -9
  10. package/install/index.mjs +10 -6
  11. package/install/templates/copilot-instructions.md +1 -1
  12. package/package.json +1 -1
  13. package/pipeline/agents/code-reviewer.md +58 -1
  14. package/pipeline/commands/multi-agent/SKILL.md +7 -5
  15. package/pipeline/commands/multi-agent/analysis/SKILL.md +7 -7
  16. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -1
  17. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +7 -7
  18. package/pipeline/commands/multi-agent/channels/SKILL.md +5 -5
  19. package/pipeline/commands/multi-agent/dev/SKILL.md +23 -18
  20. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +19 -13
  21. package/pipeline/commands/multi-agent/dev-local/SKILL.md +14 -12
  22. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +17 -12
  23. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  24. package/pipeline/commands/multi-agent/help/SKILL.md +4 -4
  25. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
  26. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +4 -4
  27. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
  28. package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
  29. package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
  30. package/pipeline/commands/multi-agent/search/SKILL.md +1 -1
  31. package/pipeline/commands/multi-agent/setup/SKILL.md +6 -6
  32. package/pipeline/commands/multi-agent/{finish → ship}/SKILL.md +12 -12
  33. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
  34. package/pipeline/commands/multi-agent/update/SKILL.md +5 -2
  35. package/pipeline/commands/sim-test.md +2 -2
  36. package/pipeline/lib/credential-store-resolver.sh +16 -0
  37. package/pipeline/lib/credential-store.sh +47 -4
  38. package/pipeline/lib/fetch-figma-annotations.sh +26 -28
  39. package/pipeline/lib/figma-screenshot.sh +28 -39
  40. package/pipeline/lib/figma-token.sh +63 -0
  41. package/pipeline/multi-agent-refs/analysis-template.md +1 -1
  42. package/pipeline/multi-agent-refs/android-guide.md +1 -1
  43. package/pipeline/multi-agent-refs/channels/issue-comment.md +1 -1
  44. package/pipeline/multi-agent-refs/component-dispatch.md +2 -2
  45. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
  46. package/pipeline/multi-agent-refs/features/dev-critic.md +2 -2
  47. package/pipeline/multi-agent-refs/features/model-fallback.md +35 -2
  48. package/pipeline/multi-agent-refs/features/plan-todos.md +1 -1
  49. package/pipeline/multi-agent-refs/features/repo-map.md +1 -1
  50. package/pipeline/multi-agent-refs/features/review-multi-repo.md +3 -3
  51. package/pipeline/multi-agent-refs/features/shadow-git.md +1 -1
  52. package/pipeline/multi-agent-refs/features/skill-conformance.md +116 -0
  53. package/pipeline/multi-agent-refs/features/verify-by-test.md +1 -1
  54. package/pipeline/multi-agent-refs/generate-issue.md +1 -1
  55. package/pipeline/multi-agent-refs/multi-repo-integration-build.md +1 -1
  56. package/pipeline/multi-agent-refs/phases/log-format.md +4 -4
  57. package/pipeline/multi-agent-refs/phases/modes.md +7 -7
  58. package/pipeline/multi-agent-refs/phases/phase-0-init.md +13 -11
  59. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +17 -15
  60. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +7 -7
  61. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +28 -13
  62. package/pipeline/multi-agent-refs/phases/phase-4-review.md +90 -58
  63. package/pipeline/multi-agent-refs/phases/phase-5-test.md +7 -7
  64. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -8
  65. package/pipeline/multi-agent-refs/phases/phase-7-report.md +8 -8
  66. package/pipeline/multi-agent-refs/phases.md +13 -13
  67. package/pipeline/multi-agent-refs/progress-contract.md +2 -2
  68. package/pipeline/multi-agent-refs/rules.md +7 -5
  69. package/pipeline/multi-agent-refs/swiftui-guide.md +1 -1
  70. package/pipeline/multi-agent-refs/tracker-contract.md +16 -15
  71. package/pipeline/preferences-template.json +7 -1
  72. package/pipeline/rules/figma-pipeline.md +2 -2
  73. package/pipeline/schemas/agent-state.schema.json +333 -79
  74. package/pipeline/schemas/criteria-manifest.schema.json +228 -0
  75. package/pipeline/schemas/migrations/prefs-2.4.0-to-2.5.0.mjs +64 -0
  76. package/pipeline/schemas/prefs.schema.json +118 -262
  77. package/pipeline/schemas/reviewer-output.schema.json +48 -3
  78. package/pipeline/schemas/token-budget.json +34 -10
  79. package/pipeline/schemas/triage-output.schema.json +112 -27
  80. package/pipeline/scripts/cost-table.json +7 -4
  81. package/pipeline/scripts/gc-worktrees.sh +1 -1
  82. package/pipeline/scripts/gen-mode-dispatch.mjs +6 -6
  83. package/pipeline/scripts/match-skills.mjs +37 -4
  84. package/pipeline/scripts/migrate-prefs.mjs +88 -17
  85. package/pipeline/scripts/phase-tracker.sh +14 -3
  86. package/pipeline/scripts/pre-commit-check.sh +49 -2
  87. package/pipeline/scripts/skill-conformance.mjs +960 -0
  88. package/pipeline/scripts/smoke-schema-validation.sh +17 -4
  89. package/pipeline/scripts/uninstall.mjs +35 -9
  90. package/pipeline/scripts/validate-reviewer.mjs +108 -1
  91. package/pipeline/skills/.skill-manifest.json +1 -1
  92. package/pipeline/skills/.skills-index.json +36 -9
  93. package/pipeline/skills/shared/README.md +15 -12
  94. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +1 -0
  95. package/pipeline/skills/shared/core/apple-archive-compliance/references/rules.yml +167 -0
  96. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +1 -0
  97. package/pipeline/skills/shared/core/google-play-compliance/references/rules.yml +184 -0
  98. package/pipeline/skills/shared/core/multi-agent/SKILL.md +10 -10
  99. package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +4 -4
  100. package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +3 -3
  101. package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +2 -2
  102. package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
  103. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +6 -5
  104. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +7 -6
  105. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +4 -3
  106. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +2 -1
  107. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +2 -2
  108. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +1 -1
  109. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -4
  110. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
  111. package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +1 -1
  112. package/pipeline/skills/shared/core/multi-agent-search/SKILL.md +1 -1
  113. package/pipeline/skills/shared/core/{multi-agent-finish → multi-agent-ship}/SKILL.md +8 -8
  114. package/pipeline/skills/shared/external/ios-coding-standard/SKILL.md +44 -5
  115. package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +82 -0
  116. package/pipeline/skills/shared/external/ios-coding-standard/references/STANDARD.md +169 -10
  117. package/pipeline/skills/shared/external/ios-coding-standard/references/lint-local.sh +13 -1
  118. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +335 -16
  119. package/pipeline/skills/skills-index.md +11 -8
@@ -27,7 +27,7 @@
27
27
  import { existsSync, readFileSync, readdirSync, statSync } from "fs";
28
28
  import { join } from "path";
29
29
 
30
- import { copyDir, countFiles, ensureDir, ensureRealDir, isDryRun } from "./_common.mjs";
30
+ import { copyDir, countFiles, ensureDir, ensureRealDir, isDryRun, wipeDir } from "./_common.mjs";
31
31
 
32
32
  /** Subtrees a plugin authors itself. `knowledge/` is generated, so it is excluded. */
33
33
  export const AUTHORED_GROUPS = Object.freeze(["index", "reference", "workflow", "tools"]);
@@ -148,16 +148,17 @@ export function pluginsToDeliver(home, platformFlag) {
148
148
  * @param {string} opts.dest - flat skills directory
149
149
  * @param {"ios"|"android"|"all"} opts.platformFlag
150
150
  * @param {string} opts.label - host label for log lines
151
- * @param {Set<string>} [opts.skipNames] - skill names the host already has
152
- * @returns {{copied: number, skipped: number, plugins: string[], missing: string[]}}
151
+ * @param {Set<string>} [opts.skipNames] - PIPELINE-owned skill names; a plugin skill matching one is delivered plugin-prefixed rather than dropped
152
+ * @returns {{copied: number, renamed: string[], collided: number, plugins: string[], missing: string[]}}
153
153
  */
154
154
  export function installAuthoredPluginSkills(opts) {
155
155
  const { home, dest, platformFlag, label, skipNames = new Set() } = opts;
156
156
  const { names: wanted, source: selectionSource } = pluginsToDeliver(home, platformFlag);
157
157
 
158
158
  let copied = 0;
159
- let skipped = 0;
160
159
  let collided = 0;
160
+ /** `<original> -> <plugin-prefixed>` for each name a pipeline skill already owns. */
161
+ const renamed = [];
161
162
  const plugins = [];
162
163
  const missing = [];
163
164
  // Names delivered in THIS pass, so a later plugin cannot overwrite an earlier
@@ -186,17 +187,43 @@ export function installAuthoredPluginSkills(opts) {
186
187
  .map((e) => ({ name: e.name, from: join(groupDir, e.name) }));
187
188
 
188
189
  for (const { name, from } of entries) {
190
+ // A plugin skill whose name a PIPELINE skill already owns used to be dropped.
191
+ // That is not a duplicate: the pipeline's `architecture` is a generic ADR
192
+ // framework while the iOS plugin's is that stack's structural rules, and the
193
+ // same holds for `backlog`. Claude Code reaches both because its loader
194
+ // namespaces plugin skills (`ai-ios-engineering-toolkit:architecture`); the
195
+ // copy hosts had no namespace, so the stack-specific version was silently
196
+ // unreachable on exactly the repos that need it most.
197
+ //
198
+ // Deliver it under the plugin-prefixed name instead - the flat analogue of
199
+ // what the loader does. The pipeline keeps the bare name, so nothing that
200
+ // dispatches on it changes.
201
+ let deliveredName = name;
189
202
  if (skipNames.has(name)) {
190
- skipped++;
191
- continue;
203
+ deliveredName = `${pluginName}-${name}`;
204
+ renamed.push(`${name} -> ${deliveredName}`);
192
205
  }
193
- if (delivered.has(name)) {
194
- collided++;
195
- continue;
206
+ // Two plugins can author the same skill name - `index` in every toolkit,
207
+ // `fix-bug` and `branch-and-pr` in four of them. Dropping the later one lost
208
+ // it entirely on the copy hosts: with the iOS and common toolkits both
209
+ // enabled, the common toolkit's `index` was simply gone. Prefix instead, so
210
+ // the first (stack) plugin keeps the bare name that dispatch expects and the
211
+ // later one is still reachable.
212
+ if (delivered.has(deliveredName)) {
213
+ const prefixed = `${pluginName}-${name}`;
214
+ if (delivered.has(prefixed)) {
215
+ collided++;
216
+ continue;
217
+ }
218
+ renamed.push(`${deliveredName} -> ${prefixed}`);
219
+ deliveredName = prefixed;
196
220
  }
197
- delivered.add(name);
198
- const to = join(dest, name);
221
+ delivered.add(deliveredName);
222
+ const to = join(dest, deliveredName);
199
223
  ensureDir(dest);
224
+ // Wipe before copy: `copyDir` only adds, so a file the plugin dropped upstream
225
+ // would linger in the installed skill and keep being read as current.
226
+ wipeDir(to);
200
227
  copyDir(from, to, {});
201
228
  copied += countFiles(from);
202
229
  perPlugin++;
@@ -215,8 +242,11 @@ export function installAuthoredPluginSkills(opts) {
215
242
  ` -> ${label}: ${collided} name collision(s) resolved in favour of the earlier plugin`,
216
243
  );
217
244
  }
218
- if (skipped > 0) {
219
- console.log(` -> ${label}: skipped ${skipped} skill(s) the host already carries`);
245
+ if (renamed.length > 0) {
246
+ console.log(
247
+ ` -> ${label}: ${renamed.length} plugin skill(s) plugin-prefixed so a name clash loses ` +
248
+ `nothing (${renamed.join(", ")})`,
249
+ );
220
250
  }
221
251
  if (missing.length > 0) {
222
252
  console.log(
@@ -224,7 +254,7 @@ export function installAuthoredPluginSkills(opts) {
224
254
  `clone {owner}/multi-agent-plugins or enable them in Claude Code to deliver their skills`,
225
255
  );
226
256
  }
227
- return { copied, skipped, collided, plugins, missing, selectionSource };
257
+ return { copied, collided, renamed, plugins, missing, selectionSource };
228
258
  }
229
259
 
230
260
  /**
@@ -243,4 +273,33 @@ export function existingSkillNames(dir) {
243
273
  );
244
274
  }
245
275
 
276
+ /**
277
+ * Skill names the PIPELINE owns, derived from its source tree.
278
+ *
279
+ * This is what `skipNames` should be. Callers used to pass
280
+ * `existingSkillNames(dest)` - a snapshot of whatever was already installed - which
281
+ * did protect pipeline skills from being shadowed by a plugin, but also meant a
282
+ * plugin skill was copied exactly once and then frozen: on the second install its
283
+ * own name was "already there", so it was skipped, forever. A stale plugin skill
284
+ * then outlives every upstream fix in it, which is how a reference to a path the
285
+ * plugin no longer names survived in an installed tree.
286
+ *
287
+ * Deriving from the source keeps the shadowing protection (a plugin still cannot
288
+ * overwrite a pipeline skill) while letting plugin skills refresh on every install.
289
+ *
290
+ * @param {string} pipelineSrc - the repo's `pipeline/` directory
291
+ * @returns {Set<string>}
292
+ */
293
+ export function pipelineOwnedSkillNames(pipelineSrc) {
294
+ const owned = new Set();
295
+ for (const group of ["core", "external"]) {
296
+ const dir = join(pipelineSrc, "skills", "shared", group);
297
+ if (!existsSync(dir)) continue;
298
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
299
+ if (e.isDirectory()) owned.add(e.name);
300
+ }
301
+ }
302
+ return owned;
303
+ }
304
+
246
305
  export { ensureRealDir };
@@ -23,18 +23,21 @@ import { localizeCommands } from "../pipeline/scripts/localize-commands.mjs";
23
23
  import {
24
24
  copyDir,
25
25
  copyFile,
26
+ copySkillsIndex,
26
27
  countFiles,
27
28
  ensureDir,
28
29
  ensureRealDir,
29
30
  isDryRun,
30
31
  pruneAbandonedTrees,
31
32
  pruneLegacyMultiAgentSkills,
33
+ pruneOrphanSkillFiles,
32
34
  removePipelineAgentFiles,
33
35
  wipeDir,
34
36
  writeFile,
35
37
  } from "./_common.mjs";
36
38
  import { copyExternalSkillsFiltered } from "./_platform-filter.mjs";
37
39
  import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
40
+ import { registerMcpServer } from "./_mcp-register.mjs";
38
41
 
39
42
  /**
40
43
  * Dead matcher written by pre-v11.4.1 installs. Hook matchers only ever see
@@ -86,6 +89,14 @@ export function installClaude(ctx) {
86
89
  ensurePreferences(PREFS_PATH, pipelineSrc);
87
90
  configureSettings(home);
88
91
 
92
+ // Claude Code was the last target still shipping skills without the tools those
93
+ // skills call. The dev-toolkit server backs design-check, every ios_* / android_*
94
+ // simulator call and the archive audits, and its absence is invisible until a run
95
+ // reaches for one of them. `--scope user` is not optional here: `claude mcp add`
96
+ // defaults to project scope, which would register the server only for whichever
97
+ // directory the installer ran in.
98
+ registerMcpServer("claude", "Claude Code");
99
+
89
100
  console.log("");
90
101
  }
91
102
 
@@ -325,10 +336,7 @@ function installSkills(opts) {
325
336
 
326
337
  if (indexOnly) {
327
338
  ensureDir(dest);
328
- const indexJson = join(pipelineSrc, "skills", ".skills-index.json");
329
- const indexMd = join(pipelineSrc, "skills", "skills-index.md");
330
- if (existsSync(indexJson)) copyFile(indexJson, join(dest, ".skills-index.json"));
331
- if (existsSync(indexMd)) copyFile(indexMd, join(dest, "skills-index.md"));
339
+ copySkillsIndex(pipelineSrc, dest);
332
340
  console.log(` -> --index-only: wrote skills-index.{json,md} to ${dest}`);
333
341
  console.log(` -> set prefs.global.dynamicSkillLoading=true to use (see docs)`);
334
342
  return;
@@ -386,6 +394,22 @@ function installSkills(opts) {
386
394
  }
387
395
  }
388
396
 
397
+ // The index ships on FULL installs too, not just --index-only. It is what
398
+ // `match-skills.mjs` reads when prefs.global.dynamicSkillLoading is on, and the
399
+ // schema has always claimed a full install provides it. It did not: only
400
+ // --index-only copied it, so flipping the pref on a normal install produced an
401
+ // exit-1 "cannot read index" on the first dispatch. Two small files, and the
402
+ // feature is either wired or it is not.
403
+ copySkillsIndex(pipelineSrc, dest);
404
+
405
+ // Same pre-directory-layout leftovers Copilot accumulated. Claude Code's tree is
406
+ // clean today, but the legacy shape predates both targets, so the prune runs here
407
+ // too rather than waiting for someone to find the first stale copy.
408
+ const orphans = pruneOrphanSkillFiles(dest);
409
+ if (orphans > 0) {
410
+ console.log(` -> pruned ${orphans} stale flat skill file(s) from an older layout`);
411
+ }
412
+
389
413
  console.log(` -> ${claudeSkillCount} skill files installed to ${dest}`);
390
414
 
391
415
  // Keep the shared skills index README in sync.
package/install/codex.mjs CHANGED
@@ -23,6 +23,7 @@ import { join } from "path";
23
23
  import {
24
24
  copyDir,
25
25
  copyFile,
26
+ copySkillsIndex,
26
27
  countFiles,
27
28
  ensureDir,
28
29
  ensureRealDir,
@@ -33,7 +34,7 @@ import {
33
34
  import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
34
35
  import { registerMcpServer } from "./_mcp-register.mjs";
35
36
  import { installCodexAgents } from "./_codex-agents.mjs";
36
- import { existingSkillNames, installAuthoredPluginSkills } from "./_plugin-skills.mjs";
37
+ import { installAuthoredPluginSkills, pipelineOwnedSkillNames } from "./_plugin-skills.mjs";
37
38
  import { generateCodexInstructions } from "./_codex-instructions.mjs";
38
39
  import { mergeManagedBlock } from "./_managed-block.mjs";
39
40
 
@@ -86,6 +87,12 @@ export const CODEX_PATH_REWRITES = Object.freeze([
86
87
  // refs/commands dir, so these generic rules follow the multi-agent ones.
87
88
  { from: /\$HOME\/\.claude\/commands\b/g, to: "$HOME/.codex/multi-agent-refs/commands" },
88
89
  { from: /~\/\.claude\/commands\b/g, to: "~/.codex/multi-agent-refs/commands" },
90
+ // Skills live under the refs tree on this host, so a skill referenced by path
91
+ // (the compliance pair, humanizer, the xcode-build family) has to point there.
92
+ // MUST precede the bare `multi-agent-refs` rule below, which would otherwise
93
+ // leave the `/skills` segment pointing at the Claude tree.
94
+ { from: /\$HOME\/\.claude\/skills\b/g, to: "$HOME/.codex/multi-agent-refs/skills" },
95
+ { from: /~\/\.claude\/skills\b/g, to: "~/.codex/multi-agent-refs/skills" },
89
96
  { from: /\$HOME\/\.claude\/multi-agent-refs\b/g, to: "$HOME/.codex/multi-agent-refs" },
90
97
  { from: /~\/\.claude\/multi-agent-refs\b/g, to: "~/.codex/multi-agent-refs" },
91
98
  { from: /\$HOME\/\.claude\/agents\b/g, to: "$HOME/.codex/agents" },
@@ -375,6 +382,7 @@ function installPrompt(promptsDir) {
375
382
  */
376
383
  function installSkillRefs(pipelineSrc, dest, home, platformFlag) {
377
384
  const externalSrc = join(pipelineSrc, "skills", "shared", "external");
385
+ const coreSrc = join(pipelineSrc, "skills", "shared", "core");
378
386
 
379
387
  ensureRealDir(dest);
380
388
  ensureDir(dest);
@@ -383,14 +391,37 @@ function installSkillRefs(pipelineSrc, dest, home, platformFlag) {
383
391
  let count = 0;
384
392
  if (existsSync(externalSrc)) count += copyTreeRewritten(externalSrc, dest);
385
393
 
394
+ // `shared/core` also holds two skills that are NOT pipeline sub-commands:
395
+ // apple-archive-compliance and google-play-compliance. Copying only `external`
396
+ // left both unreachable on Codex while `smoke-compliance-skills.sh` asserted they
397
+ // were wired to four consumers - the gate passed, the host had nothing to load.
398
+ // The `multi-agent*` dirs stay out: those are sub-command specs, and they ship as
399
+ // refs under `commands/` precisely so they never enter the skills block.
400
+ if (existsSync(coreSrc)) {
401
+ count += copyTreeRewritten(
402
+ coreSrc,
403
+ dest,
404
+ (rel) => !rel.startsWith("multi-agent/") && !rel.startsWith("multi-agent-"),
405
+ );
406
+ }
407
+
408
+ // Pipeline-owned names come from the SOURCE tree. A destination snapshot also
409
+ // matched plugin skills from a previous install, so each was copied once and then
410
+ // skipped forever - frozen against every upstream fix in it.
386
411
  const res = installAuthoredPluginSkills({
387
412
  home,
388
413
  dest,
389
414
  platformFlag,
390
415
  label: "Codex CLI",
391
- skipNames: existingSkillNames(dest),
416
+ skipNames: pipelineOwnedSkillNames(pipelineSrc),
392
417
  });
393
418
 
419
+ // The index lands beside the refs it indexes. Codex has no skills block to feed,
420
+ // but `match-skills.mjs` is installed here too and dynamic skill loading reads the
421
+ // same file on every host - and the router's `ls`/`grep` discovery gets a cheap
422
+ // catalogue out of it rather than heading four lines of each SKILL.md.
423
+ copySkillsIndex(pipelineSrc, dest);
424
+
394
425
  console.log(
395
426
  ` -> ${count + res.copied} skill file(s) copied to ${dest} as refs (0 skills-block cost)`,
396
427
  );
@@ -8,24 +8,27 @@
8
8
  * @module install/copilot
9
9
  */
10
10
 
11
- import { existsSync, mkdirSync, readdirSync, rmSync } from "fs";
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "fs";
12
12
  import { join } from "path";
13
13
 
14
14
  import {
15
15
  copyDir,
16
16
  copyFile,
17
+ copySkillsIndex,
17
18
  countFiles,
18
19
  ensureDir,
19
20
  ensureRealDir,
20
21
  isDryRun,
22
+ pruneOrphanSkillFiles,
21
23
  removePipelineAgentFiles,
22
24
  wipeDir,
25
+ writeFile,
23
26
  } from "./_common.mjs";
24
27
  import { copyExternalSkillsFiltered } from "./_platform-filter.mjs";
25
28
  import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
26
29
  import { generateCopilotInstructions } from "./_copilot-instructions.mjs";
27
30
  import { legacyTrailingContent, mergeManagedBlock } from "./_managed-block.mjs";
28
- import { existingSkillNames, installAuthoredPluginSkills } from "./_plugin-skills.mjs";
31
+ import { installAuthoredPluginSkills, pipelineOwnedSkillNames } from "./_plugin-skills.mjs";
29
32
  import { registerMcpServer } from "./_mcp-register.mjs";
30
33
 
31
34
  /**
@@ -47,6 +50,7 @@ export function installCopilot(ctx) {
47
50
  const COPILOT_SKILLS = join(COPILOT_DIR, "skills");
48
51
  const COPILOT_SCHEMAS = join(COPILOT_DIR, "schemas");
49
52
  const COPILOT_LIB = join(COPILOT_DIR, "lib");
53
+ const COPILOT_RULES = join(COPILOT_DIR, "rules");
50
54
 
51
55
  console.log(" [Copilot CLI] Installing pipeline instructions...");
52
56
  ensureDir(COPILOT_DIR);
@@ -56,6 +60,7 @@ export function installCopilot(ctx) {
56
60
  installAgents(pipelineSrc, COPILOT_AGENTS, useSymlinks);
57
61
  installSchemas(pipelineSrc, COPILOT_SCHEMAS, useSymlinks);
58
62
  installLib(pipelineSrc, COPILOT_LIB, useSymlinks);
63
+ installRules(pipelineSrc, COPILOT_RULES, useSymlinks);
59
64
  installSkills({ home, pipelineSrc, dest: COPILOT_SKILLS, indexOnly, useSymlinks, platformFlag });
60
65
 
61
66
  // Copilot never registered the companion MCP server, while Codex did from the day its
@@ -66,6 +71,91 @@ export function installCopilot(ctx) {
66
71
  registerMcpServer("copilot", "Copilot CLI");
67
72
  }
68
73
 
74
+ /**
75
+ * `$HOME/.claude/...` rewrites applied to skill files copied into the Copilot tree.
76
+ *
77
+ * Copilot's skills used to be copied byte-for-byte, so a Copilot-only install
78
+ * shipped 15 references to `~/.claude/scripts` and `~/.claude/lib` - trees that
79
+ * install never creates. Every one of those was a dangling path: the skill told the
80
+ * agent to run a script at a location that does not exist on that machine. Codex has
81
+ * had this rewrite since its installer was written; Copilot simply never got it.
82
+ *
83
+ * Only the trees Copilot actually OWNS are rewritten:
84
+ *
85
+ * - `scripts`, `lib`, `schemas`, `agents`, `rules` and `skills` are installed under
86
+ * `~/.copilot/`, so references must point there.
87
+ * - `multi-agent-refs` and `commands` are deliberately NOT rewritten: Copilot does
88
+ * not install them, and the instructions block already routes those to GitHub or
89
+ * to `~/.claude` when Claude Code is present on the same machine.
90
+ * - Shared state (`logs/`, `multi-agent-preferences.json`, `knowledge/`) is NOT
91
+ * rewritten either. `~/.claude` is the cross-host state root - that is what lets
92
+ * `:resume` / `:log` / `:status` see a task started from another CLI.
93
+ *
94
+ * @type {ReadonlyArray<{from: RegExp, to: string}>}
95
+ */
96
+ export const COPILOT_PATH_REWRITES = Object.freeze([
97
+ // Skills first: a skill that names a sibling skill by path (the compliance pair,
98
+ // humanizer, the xcode-build family) has to land inside THIS host's skills tree.
99
+ { from: /\$HOME\/\.claude\/skills\b/g, to: "$HOME/.copilot/skills" },
100
+ { from: /~\/\.claude\/skills\b/g, to: "~/.copilot/skills" },
101
+ { from: /\$HOME\/\.claude\/scripts\b/g, to: "$HOME/.copilot/scripts" },
102
+ { from: /~\/\.claude\/scripts\b/g, to: "~/.copilot/scripts" },
103
+ { from: /\$HOME\/\.claude\/lib\b/g, to: "$HOME/.copilot/lib" },
104
+ { from: /~\/\.claude\/lib\b/g, to: "~/.copilot/lib" },
105
+ { from: /\$HOME\/\.claude\/schemas\b/g, to: "$HOME/.copilot/schemas" },
106
+ { from: /~\/\.claude\/schemas\b/g, to: "~/.copilot/schemas" },
107
+ { from: /\$HOME\/\.claude\/agents\b/g, to: "$HOME/.copilot/agents" },
108
+ { from: /~\/\.claude\/agents\b/g, to: "~/.copilot/agents" },
109
+ { from: /\$HOME\/\.claude\/rules\b/g, to: "$HOME/.copilot/rules" },
110
+ { from: /~\/\.claude\/rules\b/g, to: "~/.copilot/rules" },
111
+ ]);
112
+
113
+ /**
114
+ * Apply the Copilot path map to a file's text.
115
+ * @param {string} text
116
+ * @returns {string}
117
+ */
118
+ export function rewriteCopilotPaths(text) {
119
+ let out = text;
120
+ for (const { from, to } of COPILOT_PATH_REWRITES) out = out.replace(from, to);
121
+ return out;
122
+ }
123
+
124
+ /**
125
+ * Recursively copy a tree, applying the Copilot path map to text files.
126
+ *
127
+ * Not `copyDir`: that copies bytes, and every skill file here carries
128
+ * `$HOME/.claude/...` references that have to be retargeted.
129
+ *
130
+ * @param {string} src
131
+ * @param {string} dest
132
+ * @param {(relPath: string) => boolean} [filter] - receives the path relative to the
133
+ * copy root (POSIX-separated). Return false to skip.
134
+ * @param {string} [relBase] - internal: accumulated relative path
135
+ * @returns {number} files written
136
+ */
137
+ function copyTreeRewritten(src, dest, filter, relBase = "") {
138
+ const TEXT = /\.(md|json|sh|mjs|js|txt|ya?ml|toml)$/i;
139
+ let n = 0;
140
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
141
+ const from = join(src, entry.name);
142
+ const to = join(dest, entry.name);
143
+ const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
144
+ if (entry.isDirectory()) {
145
+ n += copyTreeRewritten(from, to, filter, rel);
146
+ continue;
147
+ }
148
+ if (!entry.isFile()) continue;
149
+ if (filter && !filter(rel)) continue;
150
+ ensureDir(dest);
151
+ const isText = TEXT.test(entry.name);
152
+ const raw = readFileSync(from, isText ? "utf-8" : null);
153
+ writeFile(to, isText ? rewriteCopilotPaths(raw) : raw);
154
+ n++;
155
+ }
156
+ return n;
157
+ }
158
+
69
159
  /** Start of the pipeline-managed span in copilot-instructions.md. */
70
160
  export const INSTRUCTIONS_START_MARKER = "# Multi-Agent Development Pipeline";
71
161
 
@@ -168,6 +258,26 @@ function installLib(pipelineSrc, dest, useSymlinks) {
168
258
  console.log(` -> ${countFiles(libSrc)} files copied to ${dest}`);
169
259
  }
170
260
 
261
+ /**
262
+ * The convention rules the analysis phase falls back to when a project ships no
263
+ * standards file of its own.
264
+ *
265
+ * Codex has installed these since its installer was written, with the reason stated
266
+ * there: a dangling `rules/` means that fallback silently yields nothing. Copilot never
267
+ * got the tree, so every `rules/figma-pipeline.md` and `rules/tdd.md` reference in its
268
+ * skills pointed at a directory that install never created - the same silent-nothing
269
+ * outcome, on the host that has no `~/.claude` to borrow from.
270
+ */
271
+ function installRules(pipelineSrc, dest, useSymlinks) {
272
+ console.log(" [Copilot CLI] Installing convention rules...");
273
+ const rulesSrc = join(pipelineSrc, "rules");
274
+ if (!existsSync(rulesSrc)) return;
275
+ if (!useSymlinks) ensureRealDir(dest);
276
+ wipeDir(dest);
277
+ copyDir(rulesSrc, dest, { useSymlinks });
278
+ console.log(` -> ${countFiles(rulesSrc)} files copied to ${dest}`);
279
+ }
280
+
171
281
  function installSkills(opts) {
172
282
  const { home, pipelineSrc, dest, indexOnly, useSymlinks, platformFlag } = opts;
173
283
  console.log(" [Copilot CLI] Installing skills...");
@@ -182,10 +292,7 @@ function installSkills(opts) {
182
292
  } catch {
183
293
  /* ok */
184
294
  }
185
- const indexJson = join(pipelineSrc, "skills", ".skills-index.json");
186
- const indexMd = join(pipelineSrc, "skills", "skills-index.md");
187
- if (existsSync(indexJson)) copyFile(indexJson, join(dest, ".skills-index.json"));
188
- if (existsSync(indexMd)) copyFile(indexMd, join(dest, "skills-index.md"));
295
+ copySkillsIndex(pipelineSrc, dest);
189
296
  console.log(` -> --index-only: wrote skills-index.{json,md} to ${dest}`);
190
297
  console.log("");
191
298
  return;
@@ -229,8 +336,24 @@ function installSkills(opts) {
229
336
  }
230
337
 
231
338
  if (existsSync(sharedCoreSrc)) {
232
- copyDir(sharedCoreSrc, dest, { useSymlinks });
233
- copilotSkillCount += countFiles(sharedCoreSrc);
339
+ if (useSymlinks) {
340
+ // --link (dev) mode points at the repo, so there is nothing to rewrite -
341
+ // and rewriting would mean editing the checkout.
342
+ copyDir(sharedCoreSrc, dest, { useSymlinks });
343
+ copilotSkillCount += countFiles(sharedCoreSrc);
344
+ } else {
345
+ // Wipe each pipeline-owned skill dir before refilling it. Copying only ADDS,
346
+ // so a subtree from an older layout survives inside a directory the installer
347
+ // still manages - a real install carried `skills/multi-agent/refs/` for
348
+ // releases after Copilot stopped installing refs, and every file in it still
349
+ // pointed at `~/.claude/lib`.
350
+ for (const e of readdirSync(sharedCoreSrc, { withFileTypes: true })) {
351
+ if (e.isDirectory()) wipeDir(join(dest, e.name));
352
+ }
353
+ // The core skills are the ones that invoke scripts and lib helpers, so they
354
+ // are the ones whose `~/.claude/...` paths have to be retargeted.
355
+ copilotSkillCount += copyTreeRewritten(sharedCoreSrc, dest);
356
+ }
234
357
  }
235
358
 
236
359
  if (existsSync(sharedExternalSrc)) {
@@ -273,17 +396,30 @@ function installSkills(opts) {
273
396
  // `knowledge/` is skipped: it is generated from this pipeline's own
274
397
  // skills/shared/external, which the step above already installed. Copying it again
275
398
  // would duplicate ~5 MB the host already has.
399
+ // skipNames is the PIPELINE-owned set, derived from the source tree - not a
400
+ // snapshot of the destination. A dest snapshot also matched plugin skills from a
401
+ // previous install, so each was copied once and then skipped forever, freezing it
402
+ // against every upstream fix.
276
403
  const pluginResult = installAuthoredPluginSkills({
277
404
  home,
278
405
  dest,
279
406
  platformFlag,
280
407
  label: "Copilot CLI",
281
- skipNames: existingSkillNames(dest),
408
+ skipNames: pipelineOwnedSkillNames(pipelineSrc),
282
409
  });
283
410
  if (pluginResult.copied > 0) {
284
411
  console.log(` -> ${pluginResult.copied} plugin skill file(s) delivered to ${dest}`);
285
412
  }
286
413
 
414
+ // Full installs ship the index too - see copySkillsIndex for why.
415
+ copySkillsIndex(pipelineSrc, dest);
416
+
417
+ // Drop `<name>.md` duplicates the pre-directory layout left behind.
418
+ const orphans = pruneOrphanSkillFiles(dest);
419
+ if (orphans > 0) {
420
+ console.log(` -> pruned ${orphans} stale flat skill file(s) from an older layout`);
421
+ }
422
+
287
423
  console.log(` -> ${copilotSkillCount} skill files installed to ${dest}`);
288
424
 
289
425
  const sharedReadmeSrc = join(pipelineSrc, "skills", "shared", "README.md");
package/install/index.mjs CHANGED
@@ -182,16 +182,20 @@ function printSummary(opts) {
182
182
  console.log("");
183
183
  }
184
184
 
185
- console.log(" For UI testing, the mobile MCP server:");
186
- if (forCodex) {
187
- console.log(" Codex CLI: registered automatically (codex mcp add dev-toolkit)");
185
+ // All three register automatically now. Claude Code was the last to gain it, and
186
+ // this summary kept telling users to do it by hand long enough that the
187
+ // hand-registered entry became the only reason it worked on any given machine.
188
+ console.log(" For UI testing, the dev-toolkit MCP server is registered automatically:");
189
+ if (forClaude) {
190
+ console.log(" Claude Code: claude mcp add --scope user dev-toolkit (user scope, all projects)");
188
191
  }
189
192
  if (forCopilot) {
190
- console.log(" Copilot CLI: registered automatically (copilot mcp add dev-toolkit)");
193
+ console.log(" Copilot CLI: copilot mcp add dev-toolkit");
191
194
  }
192
- if (forClaude) {
193
- console.log(" Claude Code: add it yourself - claude mcp add dev-toolkit -- npx -y @mmerterden/dev-toolkit-mcp");
195
+ if (forCodex) {
196
+ console.log(" Codex CLI: codex mcp add dev-toolkit");
194
197
  }
198
+ console.log(" Skipped only when the host CLI is not on PATH - the command to run is printed above.");
195
199
  console.log("");
196
200
  console.log(" Uninstall everything: npx @mmerterden/multi-agent-pipeline uninstall");
197
201
  console.log(" (Personal access tokens in keychain are preserved.)");
@@ -26,7 +26,7 @@
26
26
  ## Modes
27
27
 
28
28
  - **Normal**: Full 8 phases with code review + Plan Approval Gate
29
- - **Fast (multi-agent-dev)**: Init -> Dev(Opus) -> Commit -> Report
29
+ - **Fast (multi-agent-dev)**: Init -> Dev(Opus) -> Review -> Test -> Commit -> Report
30
30
  - **Autopilot (multi-agent-autopilot)**: Skip confirmations, auto commit/PR
31
31
  - **Fastest (multi-agent-dev-autopilot)**: Combine dev + autopilot
32
32
  - **Local (multi-agent-local)**: Full 8 phases on current branch - no worktree
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "13.5.0",
3
+ "version": "14.0.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",
@@ -21,9 +21,17 @@ You are a code reviewer. Review the provided diff for:
21
21
  Return ONLY a JSON object:
22
22
 
23
23
  ```json
24
- {"findings": [{"severity": "blocking|important|suggestion", "file": "...", "line": N, "issue": "...", "fix": "..."}], "approved": true|false}
24
+ {
25
+ "findings": [{"severity": "blocking|important|suggestion", "file": "...", "line": N, "issue": "...", "fix": "...", "ruleId": "SEC-01", "criteriaSource": "ios-coding-standard"}],
26
+ "conformance": [{"ruleId": "SEC-01", "verdict": "conformant|violated|not-applicable", "file": "Sources/X.swift", "line": 42, "reason": "..."}],
27
+ "approved": true|false
28
+ }
25
29
  ```
26
30
 
31
+ `ruleId` + `criteriaSource` are required on a finding that comes from a cited
32
+ rule and omitted otherwise. `conformance` is required whenever a `${CRITERIA}`
33
+ block was supplied - see below.
34
+
27
35
  ## Severity Classification
28
36
 
29
37
  - **blocking**: Must fix - bugs, security holes, data loss risk, architecture violations
@@ -32,6 +40,55 @@ Return ONLY a JSON object:
32
40
 
33
41
  Do NOT approve if any blocking findings exist.
34
42
 
43
+ ## Criteria (required when supplied)
44
+
45
+ The orchestrator may pass a `${CRITERIA}` block: the rule IDs the changed code
46
+ was supposed to honour, resolved deterministically before you were dispatched,
47
+ plus the paths of the registry files that define them. Read the registry files
48
+ you are pointed at. They are YAML, and a rule carries `title`, `severity` and
49
+ `enforcement`; **many but not all** carry a `check` spelling out what counts as a
50
+ violation (in the shipped iOS registry, 53 of 99 do). Where `check` is absent,
51
+ judge against the rule's `title` and `rationale` rather than assuming the rule is
52
+ unusable - and say so in the conformance row's evidence, so a thin rule reads as
53
+ a thin rule and not as a pass.
54
+
55
+ ```
56
+ ${CRITERIA}
57
+ registries:
58
+ <name> (<version>) -> <absolute path to rules.yml>
59
+ rules in scope:
60
+ <RULE-ID> [<severity>/<enforcement>] -> <files it applies to>
61
+ ...
62
+ module guides:
63
+ <repo-relative path>
64
+ coverage gaps:
65
+ <language>: <why no rule applies - review these files on general criteria>
66
+ ```
67
+
68
+ Return one `conformance` row **per rule ID in that list**, and no rows for IDs
69
+ outside it. Rules:
70
+
71
+ - `conformant` requires evidence: the `file` and `line` you checked. A verdict
72
+ with no evidence is an assertion, and the report would then certify
73
+ completeness you never established.
74
+ - `violated` must have a matching entry in `findings` carrying the same
75
+ `ruleId`.
76
+ - `not-applicable` requires a `reason` naming why the rule cannot bind here.
77
+ - An ID that is neither checked nor explicitly waived fails the stage. This is
78
+ the whole point of the list: "I found nothing" and "I checked nothing"
79
+ produce identical `findings` arrays, so the checklist is what tells them
80
+ apart.
81
+ - Never invent a rule ID. Cite only IDs from the block, and only for rules the
82
+ registry actually contains.
83
+
84
+ Enforcement kinds tell you who owns a rule: `format` belongs to the formatter
85
+ and `lint` to the linter, so do not hand-review those beyond noticing an
86
+ obvious breach; `judgement` rules are yours, and each needs the measurement its
87
+ `check` names (a count, a call-site list) rather than an impression.
88
+
89
+ When no `${CRITERIA}` block is supplied, omit `conformance` entirely and review
90
+ on the focus areas above.
91
+
35
92
  ## Priority Files (advisory)
36
93
 
37
94
  When the orchestrator passes a `${PRIORITY_FILES}` block, treat it as a heuristic