@mmerterden/multi-agent-pipeline 12.10.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.
- package/CHANGELOG.md +179 -0
- package/README.md +24 -7
- package/index.js +5 -2
- package/install/_codex-agents.mjs +211 -0
- package/install/_codex-instructions.mjs +33 -0
- package/install/_managed-block.mjs +99 -0
- package/install/codex.mjs +478 -0
- package/install/copilot.mjs +34 -80
- package/install/index.mjs +25 -9
- package/install/templates/codex-instructions.md +45 -0
- package/package.json +5 -3
- package/pipeline/claude-md-template.md +1 -0
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev/SKILL.md +31 -6
- package/pipeline/commands/multi-agent/finish/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/language/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/setup/SKILL.md +69 -2
- package/pipeline/commands/multi-agent/sync/SKILL.md +128 -5
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +219 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +7 -4
- package/pipeline/multi-agent-refs/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +51 -17
- package/pipeline/multi-agent-refs/features/model-fallback.md +29 -0
- package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
- package/pipeline/multi-agent-refs/phases/log-format.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +14 -2
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +36 -5
- package/pipeline/multi-agent-refs/progress-contract.md +1 -1
- package/pipeline/multi-agent-refs/tracker-contract.md +17 -1
- package/pipeline/schemas/prefs.schema.json +296 -62
- package/pipeline/schemas/reviewer-output.schema.json +1 -1
- package/pipeline/schemas/triage-output.schema.json +1 -1
- package/pipeline/scripts/cost-table.json +15 -1
- package/pipeline/scripts/output-quality-check.sh +34 -0
- package/pipeline/scripts/phase-tracker.sh +79 -1
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +25 -10
- package/pipeline/scripts/uninstall.mjs +105 -9
- package/pipeline/scripts/update-check.sh +2 -1
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +48 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +87 -6
- 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 };
|
package/install/copilot.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @module install/copilot
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, mkdirSync, readdirSync,
|
|
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
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
|
-
*
|
|
80
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
"\\n---\\n+## Multi-Agent Task Orchestrator[\\s\\S]*?" + nextSectionEnd,
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 (
|
|
136
|
-
|
|
137
|
-
|
|
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 };
|