@mmerterden/multi-agent-pipeline 12.11.0 → 13.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 (40) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +24 -7
  3. package/index.js +5 -2
  4. package/install/_codex-agents.mjs +211 -0
  5. package/install/_codex-instructions.mjs +33 -0
  6. package/install/_managed-block.mjs +99 -0
  7. package/install/codex.mjs +478 -0
  8. package/install/copilot.mjs +34 -80
  9. package/install/index.mjs +25 -9
  10. package/install/templates/codex-instructions.md +45 -0
  11. package/package.json +5 -3
  12. package/pipeline/claude-md-template.md +1 -0
  13. package/pipeline/commands/multi-agent/SKILL.md +1 -1
  14. package/pipeline/commands/multi-agent/dev/SKILL.md +31 -6
  15. package/pipeline/commands/multi-agent/finish/SKILL.md +1 -1
  16. package/pipeline/commands/multi-agent/language/SKILL.md +2 -2
  17. package/pipeline/commands/multi-agent/setup/SKILL.md +69 -2
  18. package/pipeline/commands/multi-agent/sync/SKILL.md +128 -5
  19. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +219 -0
  20. package/pipeline/commands/multi-agent/update/SKILL.md +7 -4
  21. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  22. package/pipeline/multi-agent-refs/cross-cli-contract.md +51 -17
  23. package/pipeline/multi-agent-refs/features/model-fallback.md +29 -0
  24. package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
  25. package/pipeline/multi-agent-refs/phases/log-format.md +1 -1
  26. package/pipeline/multi-agent-refs/phases/phase-0-init.md +14 -2
  27. package/pipeline/multi-agent-refs/phases/phase-4-review.md +36 -5
  28. package/pipeline/multi-agent-refs/progress-contract.md +1 -1
  29. package/pipeline/multi-agent-refs/tracker-contract.md +17 -1
  30. package/pipeline/schemas/prefs.schema.json +296 -62
  31. package/pipeline/schemas/reviewer-output.schema.json +1 -1
  32. package/pipeline/schemas/triage-output.schema.json +1 -1
  33. package/pipeline/scripts/cost-table.json +15 -1
  34. package/pipeline/scripts/smoke-cross-cli-behavior.sh +25 -10
  35. package/pipeline/scripts/uninstall.mjs +105 -9
  36. package/pipeline/scripts/update-check.sh +2 -1
  37. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
  38. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +48 -1
  39. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +87 -6
  40. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +120 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Codex CLI installer.
3
+ *
4
+ * Lays down `~/.codex/{skills/multi-agent,multi-agent-refs,agents,prompts,
5
+ * scripts,lib,schemas}/` plus the managed block in `~/.codex/AGENTS.md`, and
6
+ * registers the dev-toolkit MCP server through the `codex` CLI.
7
+ *
8
+ * Shape note: this target follows the **Claude Code** thin-dispatcher layout,
9
+ * not the Copilot CLI fan-out. Codex assembles every discovered skill's
10
+ * name + description into a single prompt block and silently drops entries once
11
+ * that block overflows - measured at install-design time, one 142-skill plugin
12
+ * surfaced only 75 of its skills AND evicted an unrelated user skill. So the
13
+ * pipeline contributes exactly one skill (`multi-agent`) and keeps its 42
14
+ * sub-command specs as reference files that cost nothing until read. Converting
15
+ * them into peer skills would silently lose pipeline commands.
16
+ *
17
+ * @module install/codex
18
+ */
19
+
20
+ import { existsSync, readFileSync, readdirSync, rmSync } from "fs";
21
+ import { join } from "path";
22
+ import { execFileSync } from "child_process";
23
+
24
+ import {
25
+ copyDir,
26
+ copyFile,
27
+ countFiles,
28
+ ensureDir,
29
+ ensureRealDir,
30
+ isDryRun,
31
+ wipeDir,
32
+ writeFile,
33
+ } from "./_common.mjs";
34
+ import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
35
+ import { installCodexAgents } from "./_codex-agents.mjs";
36
+ import { generateCodexInstructions } from "./_codex-instructions.mjs";
37
+ import { mergeManagedBlock } from "./_managed-block.mjs";
38
+
39
+ /** Start of the pipeline-managed span in `~/.codex/AGENTS.md`. */
40
+ export const AGENTS_MD_START_MARKER = "# Multi-Agent Development Pipeline";
41
+
42
+ /** Explicit end marker written after the pipeline section. */
43
+ export const AGENTS_MD_END_MARKER = "<!-- multi-agent-pipeline:codex-instructions:end -->";
44
+
45
+ /** MCP server the pipeline's `/multi-agent:test` + design-check flows depend on. */
46
+ export const MCP_SERVER_NAME = "dev-toolkit";
47
+ const MCP_SERVER_PACKAGE = "@mmerterden/dev-toolkit-mcp";
48
+
49
+ /**
50
+ * `$HOME/.claude/...` path rewrites applied to every file installed into the
51
+ * Codex tree.
52
+ *
53
+ * Two categories, and the split is deliberate:
54
+ *
55
+ * - **CLI-owned trees** (refs, agents, scripts, lib, schemas, and the command
56
+ * specs) are installed under `~/.codex/`, so their references must point
57
+ * there. A Codex-only install has no `~/.claude` tree at all.
58
+ * - **Shared state** (`logs/`, `multi-agent-preferences.json`, `knowledge/`) is
59
+ * intentionally NOT rewritten. `~/.claude` is the cross-host state root - it
60
+ * is what makes `:resume` / `:log` / `:status` work on a task started from a
61
+ * different CLI. Copilot CLI already behaves this way.
62
+ *
63
+ * Order matters: the command-spec rule must run before the generic `commands`
64
+ * rule, and both before the bare-directory rules.
65
+ *
66
+ * @type {ReadonlyArray<{from: RegExp, to: string}>}
67
+ */
68
+ export const CODEX_PATH_REWRITES = Object.freeze([
69
+ // The dispatcher itself. MUST come first: the rules below match the bare
70
+ // `commands/multi-agent` prefix, and `\b` treats the `.` in `multi-agent.md`
71
+ // as a boundary - so a later-ordered rule rewrites this filename into
72
+ // `multi-agent-refs/commands.md`, a path that does not exist. On Codex the
73
+ // dispatcher is the router skill.
74
+ {
75
+ from: /\$HOME\/\.claude\/commands\/multi-agent\.md/g,
76
+ to: "$HOME/.codex/skills/multi-agent/SKILL.md",
77
+ },
78
+ { from: /~\/\.claude\/commands\/multi-agent\.md/g, to: "~/.codex/skills/multi-agent/SKILL.md" },
79
+ // Sub-command specs become reference files under the Codex refs tree.
80
+ {
81
+ from: /\$HOME\/\.claude\/commands\/multi-agent\b/g,
82
+ to: "$HOME/.codex/multi-agent-refs/commands",
83
+ },
84
+ { from: /~\/\.claude\/commands\/multi-agent\b/g, to: "~/.codex/multi-agent-refs/commands" },
85
+ // Standalone command specs (sim-test.md, deploy.md, ...) land in the same
86
+ // refs/commands dir, so these generic rules follow the multi-agent ones.
87
+ { from: /\$HOME\/\.claude\/commands\b/g, to: "$HOME/.codex/multi-agent-refs/commands" },
88
+ { from: /~\/\.claude\/commands\b/g, to: "~/.codex/multi-agent-refs/commands" },
89
+ { from: /\$HOME\/\.claude\/multi-agent-refs\b/g, to: "$HOME/.codex/multi-agent-refs" },
90
+ { from: /~\/\.claude\/multi-agent-refs\b/g, to: "~/.codex/multi-agent-refs" },
91
+ { from: /\$HOME\/\.claude\/agents\b/g, to: "$HOME/.codex/agents" },
92
+ { from: /~\/\.claude\/agents\b/g, to: "~/.codex/agents" },
93
+ { from: /\$HOME\/\.claude\/scripts\b/g, to: "$HOME/.codex/scripts" },
94
+ { from: /~\/\.claude\/scripts\b/g, to: "~/.codex/scripts" },
95
+ { from: /\$HOME\/\.claude\/lib\b/g, to: "$HOME/.codex/lib" },
96
+ { from: /~\/\.claude\/lib\b/g, to: "~/.codex/lib" },
97
+ { from: /\$HOME\/\.claude\/schemas\b/g, to: "$HOME/.codex/schemas" },
98
+ { from: /~\/\.claude\/schemas\b/g, to: "~/.codex/schemas" },
99
+ // The convention rules the analysis phase falls back to when a project ships
100
+ // no standards file. Dangling here means that fallback silently yields
101
+ // nothing, so the tree is installed on Codex too.
102
+ { from: /\$HOME\/\.claude\/rules\b/g, to: "$HOME/.codex/rules" },
103
+ { from: /~\/\.claude\/rules\b/g, to: "~/.codex/rules" },
104
+ // Personas are Markdown on Claude Code and generated TOML on Codex, so the
105
+ // directory rewrite above is not enough: `agents/code-reviewer.md` has to
106
+ // become `agents/code-reviewer.toml` or every persona reference in the refs
107
+ // resolves to nothing. Runs last, after the directory rules.
108
+ { from: /(\$HOME|~)\/\.codex\/agents\/([a-z0-9-]+)\.md\b/g, to: "$1/.codex/agents/$2.toml" },
109
+ ]);
110
+
111
+ /**
112
+ * Apply the Codex path map to a file's text.
113
+ *
114
+ * @param {string} text
115
+ * @returns {string}
116
+ */
117
+ export function rewriteCodexPaths(text) {
118
+ let out = text;
119
+ for (const { from, to } of CODEX_PATH_REWRITES) out = out.replace(from, to);
120
+ return out;
121
+ }
122
+
123
+ /**
124
+ * Rewrite the dispatcher's frontmatter for Codex.
125
+ *
126
+ * Codex requires `name` + `description`. `allowed-tools` is a Claude Code
127
+ * concept and `description-tr` is an installed-Claude-tree localization
128
+ * artifact, so both are dropped - the same rule the Copilot transform follows.
129
+ *
130
+ * @param {string} raw - dispatcher SKILL.md content
131
+ * @param {string} name - skill name to declare
132
+ * @returns {string}
133
+ */
134
+ export function toCodexSkill(raw, name) {
135
+ const lines = raw.split("\n");
136
+ if (lines[0]?.trim() !== "---") {
137
+ return `---\nname: ${name}\ndescription: "Multi-agent development pipeline orchestrator"\n---\n\n${raw}`;
138
+ }
139
+
140
+ let description = "";
141
+ let i = 1;
142
+ for (; i < lines.length; i++) {
143
+ if (lines[i].trim() === "---") {
144
+ i++;
145
+ break;
146
+ }
147
+ const m = /^description:\s*(.*)$/.exec(lines[i]);
148
+ if (m) description = m[1].trim();
149
+ }
150
+
151
+ const body = lines.slice(i).join("\n").replace(/^\n+/, "");
152
+ const front = [
153
+ "---",
154
+ `name: ${name}`,
155
+ `description: ${description || '"Multi-agent pipeline"'}`,
156
+ "---",
157
+ ];
158
+ return `${front.join("\n")}\n\n${body}`;
159
+ }
160
+
161
+ /**
162
+ * @param {{
163
+ * home: string,
164
+ * pipelineSrc: string,
165
+ * indexOnly: boolean,
166
+ * useSymlinks: boolean,
167
+ * platformFlag: "ios"|"android"|"all",
168
+ * }} ctx
169
+ */
170
+ export function installCodex(ctx) {
171
+ // `indexOnly` and `platformFlag` are intentionally not read. Both exist to
172
+ // shrink the 428-file external skills tree, which this target never installs:
173
+ // Codex gets one router skill, so there is nothing for either flag to reduce.
174
+ // Documented rather than silently dropped, so a future reader does not "fix"
175
+ // it by wiring flags that would have no effect.
176
+ const { home, pipelineSrc, useSymlinks } = ctx;
177
+
178
+ const CODEX_DIR = join(home, ".codex");
179
+ const CODEX_SKILLS = join(CODEX_DIR, "skills");
180
+ const CODEX_MA_REFS = join(CODEX_DIR, "multi-agent-refs");
181
+ const CODEX_AGENTS = join(CODEX_DIR, "agents");
182
+ const CODEX_PROMPTS = join(CODEX_DIR, "prompts");
183
+ const CODEX_SCRIPTS = join(CODEX_DIR, "scripts");
184
+ const CODEX_LIB = join(CODEX_DIR, "lib");
185
+ const CODEX_SCHEMAS = join(CODEX_DIR, "schemas");
186
+ const CODEX_RULES = join(CODEX_DIR, "rules");
187
+
188
+ console.log(" [Codex CLI] Installing pipeline orchestrator...");
189
+ ensureDir(CODEX_DIR);
190
+
191
+ installRouterSkill(pipelineSrc, CODEX_SKILLS);
192
+ installRefs(pipelineSrc, CODEX_MA_REFS);
193
+ installPersonas(pipelineSrc, CODEX_AGENTS);
194
+ installPrompt(CODEX_PROMPTS);
195
+ installScripts(pipelineSrc, CODEX_SCRIPTS, useSymlinks);
196
+ installTree("lib", pipelineSrc, CODEX_LIB, useSymlinks);
197
+ installTree("schemas", pipelineSrc, CODEX_SCHEMAS, useSymlinks);
198
+ installTree("rules", pipelineSrc, CODEX_RULES, useSymlinks);
199
+ writeAgentsMd(join(CODEX_DIR, "AGENTS.md"));
200
+ ensureSharedPreferences(home, pipelineSrc);
201
+ registerMcpServer();
202
+
203
+ console.log("");
204
+ }
205
+
206
+ /**
207
+ * Write the single router skill, and prune per-command skill dirs left by any
208
+ * install that predates the one-skill layout.
209
+ */
210
+ function installRouterSkill(pipelineSrc, skillsDir) {
211
+ const dispatcherSrc = join(pipelineSrc, "commands", "multi-agent", "SKILL.md");
212
+ if (!existsSync(dispatcherSrc)) return;
213
+
214
+ // ensureRealDir only swaps a --link-era symlink for a real dir; ensureDir
215
+ // creates a missing one. Every owned tree below needs both.
216
+ ensureRealDir(skillsDir);
217
+ ensureDir(skillsDir);
218
+ pruneLegacyCommandSkills(skillsDir);
219
+
220
+ const raw = readFileSync(dispatcherSrc, "utf-8");
221
+ const skill = rewriteCodexPaths(toCodexSkill(raw, "multi-agent"));
222
+ const dest = join(skillsDir, "multi-agent");
223
+ ensureDir(dest);
224
+ writeFile(join(dest, "SKILL.md"), skill);
225
+ console.log(` -> 1 router skill written to ${dest} (42 command specs stay as refs)`);
226
+ }
227
+
228
+ /**
229
+ * A pre-router install (or a hand-copied Copilot tree) leaves
230
+ * `multi-agent-<cmd>/` skill dirs behind. Each one costs a slot in the skills
231
+ * block, which is exactly the overflow this layout avoids, so remove them.
232
+ */
233
+ function pruneLegacyCommandSkills(skillsDir) {
234
+ if (!existsSync(skillsDir)) return;
235
+ let pruned = 0;
236
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
237
+ if (!entry.isDirectory()) continue;
238
+ if (!entry.name.startsWith("multi-agent-")) continue;
239
+ if (isDryRun()) {
240
+ console.log(` [dry-run] would prune per-command skill dir ${join(skillsDir, entry.name)}`);
241
+ pruned++;
242
+ continue;
243
+ }
244
+ try {
245
+ rmSync(join(skillsDir, entry.name), { recursive: true, force: true });
246
+ pruned++;
247
+ } catch {
248
+ /* non-fatal */
249
+ }
250
+ }
251
+ if (pruned > 0) {
252
+ console.log(
253
+ ` -> pruned ${pruned} per-command skill dir(s) (they overflow the skills block)`,
254
+ );
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Install the reference tree: the shared `multi-agent-refs/` docs plus the 42
260
+ * sub-command specs, all path-rewritten. These are read on demand and never
261
+ * enter the skills block.
262
+ */
263
+ function installRefs(pipelineSrc, dest) {
264
+ const refsSrc = join(pipelineSrc, "multi-agent-refs");
265
+ const commandsSrc = join(pipelineSrc, "commands", "multi-agent");
266
+
267
+ ensureRealDir(dest);
268
+ ensureDir(dest);
269
+ wipeDir(dest);
270
+
271
+ let count = 0;
272
+ if (existsSync(refsSrc)) count += copyTreeRewritten(refsSrc, dest);
273
+
274
+ // The 42 sub-command specs. Skip ONLY the top-level dispatcher SKILL.md -
275
+ // it already shipped as the router skill. The filter is on the path relative
276
+ // to the copy root, not the basename: every sub-command spec is itself named
277
+ // SKILL.md, so a basename filter would copy 42 empty directories and leave
278
+ // the orchestrator with nothing to read.
279
+ if (existsSync(commandsSrc)) {
280
+ count += copyTreeRewritten(commandsSrc, join(dest, "commands"), (rel) => rel !== "SKILL.md");
281
+ }
282
+
283
+ // Standalone Claude Code command specs (sim-test, deploy, archive-guard, ...).
284
+ // The router delegates to these by path, so a Codex install that omits them
285
+ // leaves those routes dangling.
286
+ const standaloneSrc = join(pipelineSrc, "commands");
287
+ if (existsSync(standaloneSrc)) {
288
+ count += copyTreeRewritten(standaloneSrc, join(dest, "commands"), (rel) =>
289
+ /^[^/]+\.md$/.test(rel),
290
+ );
291
+ }
292
+
293
+ console.log(` -> ${count} reference file(s) copied to ${dest}`);
294
+ }
295
+
296
+ /**
297
+ * Recursively copy a tree, applying the Codex path map to text files.
298
+ *
299
+ * Not `copyDir` from `_common.mjs`: that copies bytes, and every ref file here
300
+ * carries `$HOME/.claude/...` references that must be retargeted.
301
+ *
302
+ * @param {string} src
303
+ * @param {string} dest
304
+ * @param {(relPath: string) => boolean} [filter] - receives the path relative to
305
+ * the copy root (POSIX-separated), not the basename. Return false to skip.
306
+ * @param {string} [relBase] - internal: accumulated relative path
307
+ * @returns {number} files written
308
+ */
309
+ function copyTreeRewritten(src, dest, filter, relBase = "") {
310
+ const TEXT = /\.(md|json|sh|mjs|js|txt|ya?ml|toml)$/i;
311
+ let n = 0;
312
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
313
+ const from = join(src, entry.name);
314
+ const to = join(dest, entry.name);
315
+ const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
316
+ if (entry.isDirectory()) {
317
+ n += copyTreeRewritten(from, to, filter, rel);
318
+ continue;
319
+ }
320
+ if (!entry.isFile()) continue;
321
+ if (filter && !filter(rel)) continue;
322
+ ensureDir(dest);
323
+ const raw = readFileSync(from, TEXT.test(entry.name) ? "utf-8" : null);
324
+ writeFile(to, TEXT.test(entry.name) ? rewriteCodexPaths(raw) : raw);
325
+ n++;
326
+ }
327
+ return n;
328
+ }
329
+
330
+ function installPersonas(pipelineSrc, dest) {
331
+ const agentsSrc = join(pipelineSrc, "agents");
332
+ if (!existsSync(agentsSrc)) return;
333
+ const written = installCodexAgents(agentsSrc, dest);
334
+ console.log(` -> ${written} persona(s) generated as Codex agent TOML in ${dest}`);
335
+ }
336
+
337
+ /**
338
+ * `~/.codex/prompts/multi-agent.md` gives Codex a real `/multi-agent` slash
339
+ * command. It is a two-line router into the skill, not a second copy of the
340
+ * orchestrator - one spec, two entry points.
341
+ */
342
+ function installPrompt(promptsDir) {
343
+ ensureRealDir(promptsDir);
344
+ ensureDir(promptsDir);
345
+ const body = [
346
+ "Run the multi-agent development pipeline.",
347
+ "",
348
+ "Load the orchestrator spec at `$HOME/.codex/skills/multi-agent/SKILL.md` and follow it",
349
+ "for the input below. Sub-command specs are at",
350
+ "`$HOME/.codex/multi-agent-refs/commands/<cmd>/SKILL.md`; phase specs are at",
351
+ "`$HOME/.codex/multi-agent-refs/phases/`. Read them on demand as the spec directs.",
352
+ "",
353
+ "Input: $ARGUMENTS",
354
+ "",
355
+ ].join("\n");
356
+ writeFile(join(promptsDir, "multi-agent.md"), body);
357
+ console.log(` -> /multi-agent prompt written to ${promptsDir}`);
358
+ }
359
+
360
+ /**
361
+ * Copy `pipeline/scripts` minus the maintainer-only set.
362
+ *
363
+ * Same exclusion the Claude Code and Copilot CLI targets apply: smokes, evals
364
+ * and release tooling are not part of a user install. Without this the Codex
365
+ * tree carries ~146 extra files nobody runs, and the cross-target parity
366
+ * assertion in `smoke-install-layout.sh` fails.
367
+ */
368
+ function installScripts(pipelineSrc, dest, useSymlinks) {
369
+ const src = join(pipelineSrc, "scripts");
370
+ if (!existsSync(src)) return;
371
+ if (!useSymlinks) {
372
+ ensureRealDir(dest);
373
+ ensureDir(dest);
374
+ }
375
+ wipeDir(dest);
376
+ copyDir(src, dest, { exclude: DEV_ONLY_SCRIPTS, useSymlinks });
377
+ const excluded = countDevOnlyFiles(src);
378
+ console.log(
379
+ ` -> ${countFiles(src) - excluded} script file(s) copied to ${dest} (${excluded} dev-only excluded)`,
380
+ );
381
+ }
382
+
383
+ /**
384
+ * Copy a pipeline tree verbatim (lib / schemas). These are executed, not read
385
+ * as prose, and they resolve their own paths at runtime.
386
+ */
387
+ function installTree(name, pipelineSrc, dest, useSymlinks) {
388
+ const src = join(pipelineSrc, name);
389
+ if (!existsSync(src)) return;
390
+ if (!useSymlinks) {
391
+ ensureRealDir(dest);
392
+ ensureDir(dest);
393
+ }
394
+ wipeDir(dest);
395
+ copyDir(src, dest, { useSymlinks });
396
+ console.log(` -> ${countFiles(src)} ${name} file(s) copied to ${dest}`);
397
+ }
398
+
399
+ function writeAgentsMd(path) {
400
+ const result = mergeManagedBlock({
401
+ path,
402
+ body: generateCodexInstructions(),
403
+ startMarker: AGENTS_MD_START_MARKER,
404
+ endMarker: AGENTS_MD_END_MARKER,
405
+ });
406
+ const verb =
407
+ result === "created"
408
+ ? "Created"
409
+ : result === "appended"
410
+ ? "Appended pipeline section to"
411
+ : "Updated pipeline section in";
412
+ console.log(` -> ${verb} ${path}`);
413
+ }
414
+
415
+ /**
416
+ * Seed `~/.claude/multi-agent-preferences.json` when it is absent.
417
+ *
418
+ * Preferences are cross-host state, not a Claude Code artifact: every phase
419
+ * reads them and the orchestrator is specified to warn and stop when they are
420
+ * missing. A Codex-only install would otherwise produce a complete tree that
421
+ * halts on the first run, because only the Claude Code target creates the file.
422
+ *
423
+ * Existing files are never overwritten.
424
+ */
425
+ function ensureSharedPreferences(home, pipelineSrc) {
426
+ const prefsPath = join(home, ".claude", "multi-agent-preferences.json");
427
+ if (existsSync(prefsPath)) return;
428
+ const template = join(pipelineSrc, "preferences-template.json");
429
+ if (!existsSync(template)) return;
430
+ ensureDir(join(home, ".claude"));
431
+ copyFile(template, prefsPath);
432
+ console.log(` -> seeded shared preferences at ${prefsPath}`);
433
+ }
434
+
435
+ /**
436
+ * Register the dev-toolkit MCP server via the `codex` CLI.
437
+ *
438
+ * Deliberately NOT a hand-written `[mcp_servers.*]` block in `config.toml`:
439
+ * Codex owns that file (it writes `[marketplaces.*]` and `[plugins."x@y"]`
440
+ * itself), `codex mcp add` is idempotent, and Codex's own plugin-creator
441
+ * reference says to manipulate config through commands rather than by editing
442
+ * it. Hand-merging TOML here would be the riskiest write in this installer for
443
+ * no benefit.
444
+ */
445
+ function registerMcpServer() {
446
+ if (isDryRun()) {
447
+ console.log(
448
+ ` [dry-run] would run: codex mcp add ${MCP_SERVER_NAME} -- npx -y ${MCP_SERVER_PACKAGE}`,
449
+ );
450
+ return;
451
+ }
452
+ // Opt-out for hermetic installs. Registration shells out to `codex`, so its
453
+ // result depends on whether the binary is present - which makes the install
454
+ // layout non-deterministic across machines and CI. The layout smoke sets this
455
+ // so its fingerprint stays stable.
456
+ if (process.env.MULTI_AGENT_SKIP_MCP_REGISTER === "1") {
457
+ console.log(" -> skipped MCP registration (MULTI_AGENT_SKIP_MCP_REGISTER=1)");
458
+ return;
459
+ }
460
+ try {
461
+ // Bounded: an installer must never hang on a child process. `codex mcp add`
462
+ // is a local config write and returns in milliseconds.
463
+ execFileSync("codex", ["mcp", "add", MCP_SERVER_NAME, "--", "npx", "-y", MCP_SERVER_PACKAGE], {
464
+ stdio: "pipe",
465
+ timeout: 20_000,
466
+ });
467
+ console.log(` -> registered the ${MCP_SERVER_NAME} MCP server`);
468
+ } catch (e) {
469
+ const why =
470
+ e?.code === "ENOENT" ? "codex not on PATH" : (e?.message || "unknown error").split("\n")[0];
471
+ console.log(` -> skipped MCP registration (${why})`);
472
+ console.log(
473
+ ` run manually: codex mcp add ${MCP_SERVER_NAME} -- npx -y ${MCP_SERVER_PACKAGE}`,
474
+ );
475
+ }
476
+ }
477
+
478
+ export { generateCodexInstructions };
@@ -8,7 +8,7 @@
8
8
  * @module install/copilot
9
9
  */
10
10
 
11
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "fs";
11
+ import { existsSync, mkdirSync, readdirSync, rmSync } from "fs";
12
12
  import { join } from "path";
13
13
 
14
14
  import {
@@ -20,11 +20,11 @@ import {
20
20
  isDryRun,
21
21
  removePipelineAgentFiles,
22
22
  wipeDir,
23
- writeFile,
24
23
  } from "./_common.mjs";
25
24
  import { copyExternalSkillsFiltered } from "./_platform-filter.mjs";
26
25
  import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
27
26
  import { generateCopilotInstructions } from "./_copilot-instructions.mjs";
27
+ import { legacyTrailingContent, mergeManagedBlock } from "./_managed-block.mjs";
28
28
 
29
29
  /**
30
30
  * @param {{
@@ -69,91 +69,41 @@ export const INSTRUCTIONS_START_MARKER = "# Multi-Agent Development Pipeline";
69
69
  export const INSTRUCTIONS_END_MARKER = "<!-- multi-agent-pipeline:copilot-instructions:end -->";
70
70
 
71
71
  /**
72
- * Legacy files (written before the end marker existed) have no explicit
73
- * terminator. Bound the pipeline span at the next top-level "# " heading
74
- * after the start marker when one exists OUTSIDE fenced code blocks (the
75
- * pipeline body carries bash comments like "# Bootstrap once ..." inside
76
- * fences that must not be mistaken for headings); otherwise the span runs
77
- * to EOF, which matches the pre-marker behavior.
72
+ * Pre-v5.0 pipeline sections that predate the stable marker. These blocks
73
+ * ("## Multi-Agent Task Orchestrator", "## Instruction Sync") were written by
74
+ * older install scripts and now contradict the current generator output.
78
75
  *
79
- * @param {string} section - file content from the start marker onward
80
- * @returns {string} user content trailing the pipeline span ("" if none)
76
+ * Match strategy: end at EXPLICIT next-section markers - either the pipeline
77
+ * marker itself, or a known user-preserved heading. A generic `\n## ` sentinel
78
+ * is unsafe because bash code fences inside the body contain lines like
79
+ * `# Personal repos` which would be mistaken for markdown headings and
80
+ * truncate the match early.
81
81
  */
82
- export function legacyTrailingContent(section) {
83
- const lines = section.split("\n");
84
- let inFence = false;
85
- for (let i = 1; i < lines.length; i++) {
86
- if (/^\s*(```|~~~)/.test(lines[i])) {
87
- inFence = !inFence;
88
- continue;
89
- }
90
- if (!inFence && /^# /.test(lines[i])) return lines.slice(i).join("\n");
91
- }
92
- return "";
93
- }
94
-
95
- function writeInstructionsFile(path) {
96
- const pipelineSection = generateCopilotInstructions();
97
- const managedBlock = pipelineSection.trimEnd() + "\n\n" + INSTRUCTIONS_END_MARKER + "\n";
98
-
99
- if (!existsSync(path)) {
100
- writeFile(path, managedBlock);
101
- console.log(" -> Created copilot-instructions.md with pipeline");
102
- return;
103
- }
104
-
105
- let existing = readFileSync(path, "utf-8");
106
- const marker = INSTRUCTIONS_START_MARKER;
107
-
108
- // Drift cleanup (v5.6.2): detect pre-v5.0 pipeline sections that predate
109
- // the stable marker and strip them. These blocks ("## Multi-Agent Task
110
- // Orchestrator", "## Instruction Sync") were written by older install
111
- // scripts and now contradict the current generator output.
112
- //
113
- // Match strategy: end at EXPLICIT next-section markers - either the
114
- // pipeline marker itself, or a known user-preserved heading. Using a
115
- // generic `\n## ` sentinel is unsafe because bash code fences inside the
116
- // body contain lines like `# Personal repos` which would be mistaken for
117
- // markdown headings and truncate the match early.
82
+ const COPILOT_DRIFT_PATTERNS = (() => {
118
83
  const nextSectionEnd =
119
84
  "(?=\\n## Git Identity Routing|\\n## GitHub Account Routing|\\n## Hooks & Context Management|\\n# Multi-Agent Development Pipeline|$)";
120
- const orchestratorRe = new RegExp(
121
- "\\n---\\n+## Multi-Agent Task Orchestrator[\\s\\S]*?" + nextSectionEnd,
122
- );
123
- const instructionSyncRe = new RegExp("\\n---\\n+## Instruction Sync[\\s\\S]*?" + nextSectionEnd);
85
+ return [
86
+ new RegExp("\\n---\\n+## Multi-Agent Task Orchestrator[\\s\\S]*?" + nextSectionEnd),
87
+ new RegExp("\\n---\\n+## Instruction Sync[\\s\\S]*?" + nextSectionEnd),
88
+ ];
89
+ })();
124
90
 
125
- let cleaned = false;
126
- if (orchestratorRe.test(existing)) {
127
- existing = existing.replace(orchestratorRe, "");
128
- cleaned = true;
129
- }
130
- if (instructionSyncRe.test(existing)) {
131
- existing = existing.replace(instructionSyncRe, "");
132
- cleaned = true;
133
- }
91
+ function writeInstructionsFile(path) {
92
+ const result = mergeManagedBlock({
93
+ path,
94
+ body: generateCopilotInstructions(),
95
+ startMarker: INSTRUCTIONS_START_MARKER,
96
+ endMarker: INSTRUCTIONS_END_MARKER,
97
+ driftPatterns: COPILOT_DRIFT_PATTERNS,
98
+ });
134
99
 
135
- if (existing.includes(marker)) {
136
- const startIdx = existing.indexOf(marker);
137
- const before = existing.slice(0, startIdx).trimEnd();
138
- const fromStart = existing.slice(startIdx);
139
- // Replace only the start..end span. User content appended AFTER the
140
- // pipeline section (below the end marker, or below the next top-level
141
- // heading in legacy files) is preserved.
142
- const endIdx = fromStart.indexOf(INSTRUCTIONS_END_MARKER);
143
- const trailing =
144
- endIdx >= 0
145
- ? fromStart.slice(endIdx + INSTRUCTIONS_END_MARKER.length)
146
- : legacyTrailingContent(fromStart);
147
- let out = before.length > 0 ? before + "\n\n" : "";
148
- out += managedBlock;
149
- const trailingClean = trailing.replace(/^[\r\n]+/, "").trimEnd();
150
- if (trailingClean.length > 0) out += "\n" + trailingClean + "\n";
151
- writeFile(path, out);
152
- const suffix = cleaned ? " (also scrubbed pre-v5.0 drift section)" : "";
153
- console.log(` -> Updated existing pipeline section in copilot-instructions.md${suffix}`);
154
- } else {
155
- writeFile(path, existing.trimEnd() + "\n\n" + managedBlock);
100
+ if (result === "created") {
101
+ console.log(" -> Created copilot-instructions.md with pipeline");
102
+ } else if (result === "appended") {
156
103
  console.log(" -> Appended pipeline section to copilot-instructions.md");
104
+ } else {
105
+ const suffix = result === "updated+scrubbed" ? " (also scrubbed pre-v5.0 drift section)" : "";
106
+ console.log(` -> Updated existing pipeline section in copilot-instructions.md${suffix}`);
157
107
  }
158
108
  }
159
109
 
@@ -321,3 +271,7 @@ function installSkills(opts) {
321
271
  }
322
272
 
323
273
  export { generateCopilotInstructions };
274
+ // Re-exported for the install tests, which assert the legacy bounding rule
275
+ // through this module. The implementation moved to _managed-block.mjs in
276
+ // v13.0.0 so Codex CLI could reuse it.
277
+ export { legacyTrailingContent };