@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
package/CHANGELOG.md CHANGED
@@ -16,6 +16,126 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [13.0.0] - 2026-07-27
20
+
21
+ > **Why major and not minor.** The additions here are additive, but two defaults
22
+ > changed in ways a reasonable workflow would notice: `install --all` now writes a
23
+ > third tree (`~/.codex`) and registers an MCP server via `codex mcp add`, and the
24
+ > cross-CLI contract changed shape (the parity axis is per-host now, so an auditor
25
+ > comparing Codex on skill directories would read a correct install as drift).
26
+ > Per the versioning policy that is a changed default, not a new option.
27
+
28
+
29
+ Codex CLI becomes a third supported host, and a new pre-submission validation command.
30
+
31
+ ### Added
32
+
33
+ - **Codex CLI as a first-class target** (`install --codex`, included in `--all`).
34
+ The v9.7.0 adapter was deleted in v10.7.0 because Codex was then a degraded
35
+ consumer with no sub-agent fan-out. Codex 0.145 has skills, parallel sub-agents
36
+ with per-agent model and reasoning effort, hooks, MCP and a plugin marketplace,
37
+ so it earns the same treatment as the other two rather than an adapter.
38
+ `install/codex.mjs` writes `~/.codex/{skills/multi-agent,multi-agent-refs,
39
+ agents/*.toml,prompts/multi-agent.md,scripts,lib,schemas,rules}` plus a managed
40
+ span in `~/.codex/AGENTS.md`, and registers the dev-toolkit MCP server through
41
+ `codex mcp add` rather than hand-merging TOML that Codex owns.
42
+ - **`/multi-agent:testflight-validation`** (command 43). Three gates, each seeing
43
+ what the others structurally cannot: the static 18-rule archive audit, Apple's
44
+ own `altool --validate-app`, and a Review-Guidelines check against repo evidence.
45
+ ITMS codes are mapped to the rule each implies. It validates only - never
46
+ `--upload-app` - so a validation run cannot ship a build by accident.
47
+ - **App Store Connect credentials in `/multi-agent:setup`**, inside the Step 1
48
+ discovery / Step 2 mapping / Step 3b onboarding flow alongside Jira and
49
+ Bitbucket, not as a late add-on: a user who already has one in their keychain
50
+ gets it mapped automatically. Tier 1 is an API key, tier 2 an Apple ID plus an
51
+ app-specific password - which matters because creating an API key needs an
52
+ Admin or App Manager role many developers on a corporate team do not have.
53
+
54
+ ### Changed
55
+
56
+ - **Codex takes the Claude Code thin-dispatcher shape, and has to.** Measured on
57
+ Codex 0.145: installing one plugin declaring 142 skills took the assembled
58
+ skills block from 11 skills / 4,710 bytes to 83 / 22,111, surfacing only **75 of
59
+ 142** and **evicting an unrelated user-scope skill**. Shipping the 43
60
+ sub-commands as peer skills would silently lose pipeline commands next to any
61
+ stack toolkit. The pipeline contributes exactly one skill on Codex and keeps the
62
+ specs as refs; `smoke-install-layout.sh` fails if a second one appears.
63
+ - Phase 4 reviewer matrix gains a Codex column (gpt-5.6 @ xhigh / gpt-5.4 /
64
+ gpt-5.6 @ medium, triage at max) with two measured constraints written into the
65
+ contract: a `spawn_agent` that sets `model` without `fork_turns: "none"`
66
+ **silently inherits the parent model**, collapsing the panel onto one
67
+ perspective; and 4 concurrency slots *including the orchestrator* make three
68
+ reviewers the ceiling. Single-vendor caveat recorded - consensus among three
69
+ OpenAI models is weaker evidence than the same consensus on a two-vendor host.
70
+ - `tracker-contract.md` gains the `codex` visual channel: the native `update_plan`
71
+ tool, with its no-parallel-call and no-plan-mode caveats.
72
+ - Cross-CLI contract retitled for three hosts; parity axis for Codex is the **ref
73
+ set**, not the skill set, because comparing skill directories would demand the
74
+ layout that breaks it.
75
+
76
+ ### Fixed
77
+
78
+ - **The remote reachability gate blamed the network for every failure.** Any
79
+ non-zero `git ls-remote` exit was reported as `unreachable (VPN/DNS)`, so a
80
+ missing-credential error that returns in under a second sent the user to enable
81
+ a VPN that could not help, and offered a stale-base fallback for a cause that
82
+ had nothing to do with staleness. Failures are now classified from stderr
83
+ (credential / network / wrong remote / unknown), the observed line is printed
84
+ next to the classification, and only the network case offers the cached ref.
85
+ - **The Phase 0 branch-collision probe read a failed probe as "no collision".**
86
+ With `2>/dev/null` and an empty-output test, an auth or network failure was
87
+ indistinguishable from "the ref does not exist", so the run created a branch
88
+ that already existed on the remote - surfacing as a rejected push at Phase 6,
89
+ far from its cause. Exit codes are now distinguished (0 exists, 2 free, anything
90
+ else unknown-and-recorded).
91
+ - **`/multi-agent:update` deleted the Codex prompt on every run**, pruning
92
+ `~/.codex/prompts/multi-agent.md` as a retired v9.7.0 adapter leftover.
93
+ - **Uninstall could delete trailing user content in `~/.codex/AGENTS.md`.**
94
+ `stripManagedBlock` matched only the Copilot end marker, so on the Codex marker
95
+ it fell through to a heading-bounded fallback that returns nothing when the
96
+ trailing content has no top-level heading. Verified against the pre-fix path.
97
+ - `smoke-own-punctuation.sh` failed on HEAD: `test/tracker-title-entities.test.mjs`
98
+ (added in v12.11.0) holds the banned characters as its own assertion list and
99
+ was never allowlisted.
100
+ - **Half-English pickers on Turkish runs: six shipped files contradicted the
101
+ canonical language matrix.** `rules.md` is unambiguous - `AskUserQuestion`
102
+ `question` and `options[].description` render in `outputLanguage`, only `label`,
103
+ `header` and host chrome are pinned to English. But `/multi-agent:setup` Step 0
104
+ claimed `promptLanguage` governs "interactive pickers and prompts ... Picker UI is
105
+ always English", `/multi-agent:language` claimed "confirmation prompts ... are
106
+ authored in English. Only the assistant's free-form replies follow
107
+ `outputLanguage`", and four more files said variations of the same. Those are the
108
+ two commands a user goes to *configure* this, so the model followed whichever
109
+ canonical-looking doc it read first and gate questions came out English. All six
110
+ corrected to describe `promptLanguage`'s real scope: the button and chip chrome,
111
+ never the question a user reads.
112
+
113
+ ### Gates (continued)
114
+
115
+ - New `smoke-language-matrix.sh`: asserts `rules.md` still pins the per-field
116
+ matrix, then greps every shipped command / skill / ref for the seven phrasings
117
+ that actually shipped and contradicted it. Two authoritative docs giving opposite
118
+ answers is a spec with two answers, not a wording nit - the gate found a sixth
119
+ violation (`_input-parser.md`) that the manual sweep had missed.
120
+
121
+ ### Gates
122
+
123
+ - New `smoke-codex-install.sh` (37 assertions): install/uninstall round-trip into a
124
+ HOME seeded with user content, asserting every artifact lands, every rewritten
125
+ `$HOME/.codex` path resolves, generated agent TOML parses, and user content is
126
+ byte-identical afterwards.
127
+ - `smoke-install-layout.sh` extended to three targets, including a check that every
128
+ concrete `$HOME/.codex` reference **resolves on disk**. The earlier "no `.claude`
129
+ references remain" check was blind to a wrongly-rewritten path, which is how
130
+ `commands/multi-agent.md` became `multi-agent-refs/commands.md` - well-formed,
131
+ pointing at nothing. A miss is classified by whether the Claude counterpart
132
+ exists, so broken-by-rewrite fails and broken-upstream is only reported.
133
+ - New `test/codex-install.test.mjs` (24 assertions) locks the path-rewrite map,
134
+ the frontmatter transform, the persona tier map, and managed-block trailing
135
+ content, including regressions for both rewrite defects above.
136
+ - `MULTI_AGENT_SKIP_MCP_REGISTER=1` makes an install hermetic, so the layout
137
+ fingerprint no longer depends on whether `codex` is on PATH.
138
+
19
139
  ## [12.11.0] - 2026-07-26
20
140
 
21
141
  ### Tracker tile titles rendered HTML entities
package/README.md CHANGED
@@ -6,15 +6,15 @@
6
6
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://github.com/mmerterden/multi-agent-pipeline/blob/main/package.json)
7
7
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/mmerterden/multi-agent-pipeline/badge)](https://scorecard.dev/viewer/?uri=github.com/mmerterden/multi-agent-pipeline)
8
8
 
9
- An 8-phase AI development pipeline for **Claude Code** and **Copilot CLI**. Drives a Jira issue or GitHub URL to a merged PR in one command - analysis → plan → TDD → review → test → commit → PR - with multi-repo orchestration, a plan-approval gate, CLI-aware parallel review, and store-compliance checks. Component and Figma-to-code work is dispatched to the per-stack marketplace plugins (iOS/SwiftUI, Android/Compose) rather than bundled, so component skills live in one place.
9
+ An 8-phase AI development pipeline for **Claude Code**, **Copilot CLI** and **Codex CLI**. Drives a Jira issue or GitHub URL to a merged PR in one command - analysis → plan → TDD → review → test → commit → PR - with multi-repo orchestration, a plan-approval gate, CLI-aware parallel review, and store-compliance checks. Component and Figma-to-code work is dispatched to the per-stack marketplace plugins (iOS/SwiftUI, Android/Compose) rather than bundled, so component skills live in one place.
10
10
 
11
- Runs natively on Claude Code and Copilot CLI. macOS / Linux / Windows. Zero runtime dependencies.
11
+ Runs natively on Claude Code, Copilot CLI and Codex CLI. macOS / Linux / Windows. Zero runtime dependencies.
12
12
 
13
13
  ## Quick Start
14
14
 
15
15
  ```bash
16
16
  # from the public registry (no auth)
17
- npx @mmerterden/multi-agent-pipeline install --all # Claude Code + Copilot CLI
17
+ npx @mmerterden/multi-agent-pipeline install --all # Claude Code + Copilot CLI + Codex CLI
18
18
 
19
19
  # then, once:
20
20
  /multi-agent:setup # keychain token scan + git identity + default stack
@@ -63,8 +63,10 @@ The discipline behind all of this - bounded loops, evidence gates, token-budgete
63
63
  | Dev | `/multi-agent:dev "task"` | Init → Dev → Commit → Report |
64
64
  | Local | `/multi-agent:local "task"` | Full pipeline, current branch (no worktree) |
65
65
  | Finish | `/multi-agent:finish` | Run the review→test→commit→report tail over local work |
66
+ | Audit | `/multi-agent:design-check` | Mock-mode vs Figma conformance, local-only |
67
+ | Audit | `/multi-agent:testflight-validation` | Pre-submission gates for a TestFlight build: static archive audit → Apple's `altool --validate-app` → Review-Guidelines check. Validates only, never uploads |
66
68
 
67
- Helpers: `setup`, `status`, `resume #N`, `review`, `test`, `channels`, `stack`, `update`, `sync`, `refactor`, `jira`, `issue`, `analysis`, `create-jira`, `save`, `routines`, `forget`. Full list: `/multi-agent:help`.
69
+ Helpers: `setup`, `status`, `resume #N`, `review`, `test`, `channels`, `stack`, `update`, `sync`, `refactor`, `jira`, `issue`, `analysis`, `create-jira`, `save`, `routines`, `forget`. 43 commands in all - full list: `/multi-agent:help`.
68
70
 
69
71
  ## Stacks
70
72
 
@@ -78,15 +80,29 @@ This enables the matching plugin (+ the shared `ai-common` plugin) in the repo's
78
80
 
79
81
  ## Tool support
80
82
 
81
- The pipeline runs natively on **Claude Code** and **Copilot CLI** - both install from the same `pipeline/skills/` source and get identical coverage.
83
+ The pipeline runs natively on **Claude Code**, **Copilot CLI** and **Codex CLI** - all three install from the same `pipeline/` source and get the same 43 commands.
82
84
 
83
- | Tool | Flag | Notes |
85
+ | Tool | Flag | What it installs |
84
86
  |---|---|---|
85
87
  | Claude Code | `--claude` (default) | slash commands + skills + agents + `PreToolUse` secret-scan hook |
86
- | Copilot CLI | `--copilot` | instructions + skills + scripts |
88
+ | Copilot CLI | `--copilot` | instructions + 43 sub-command skills + scripts |
89
+ | Codex CLI | `--codex` | one router skill + 43 specs as refs + 8 agent TOML + `AGENTS.md` block + `codex mcp add` |
87
90
 
88
91
  Filter skills by stack with `--platform=ios\|android\|all`.
89
92
 
93
+ **Why Codex gets one skill and not 43.** Codex assembles every discovered skill's name
94
+ and description into a single prompt block and drops entries when it overflows, with no
95
+ error. Measured on 0.145: installing one plugin that declares 142 skills surfaced only
96
+ 75 of them and evicted an unrelated user skill. So on Codex the pipeline ships a single
97
+ `multi-agent` router and keeps the sub-command specs as reference files that cost
98
+ nothing until read - same commands, same behaviour, a layout the host can actually hold.
99
+
100
+ Reviewer sets differ because the available models do: Claude Code runs 2 reviewers
101
+ (Fable + Sonnet), Copilot CLI 3 (Opus + GPT-5.4 + Sonnet), Codex CLI 3 (gpt-5.6 at
102
+ xhigh, gpt-5.4, gpt-5.6 at medium). Codex is single-vendor, so consensus among its three
103
+ is weaker evidence than the same consensus on a two-vendor host, and the triage note
104
+ says so.
105
+
90
106
  ## Tokens & integrations
91
107
 
92
108
  `setup` scans your OS keychain and maps each token by a **logical name** (e.g. `jira`) to its real keychain entry - the pipeline resolves tokens through that mapping (`credential-store.sh`), so literal keychain names never appear in synced files. Tokens stay in the keychain (macOS Keychain / Windows Credential Manager / Linux libsecret), are **never committed or logged**, and are all **optional** - the pipeline asks for any it needs at Phase 0.
@@ -102,6 +118,7 @@ Filter skills by stack with `--platform=ios\|android\|all`.
102
118
  | `firebase` | Firebase config (base64 JSON) for Firebase projects | as needed |
103
119
  | `jenkins` | CI trigger / status | build / deploy |
104
120
  | `npm` | package publish (mostly CI) | release |
121
+ | `appstore_connect_*` | TestFlight / App Store pre-submission validation (optional, iOS) | `testflight-validation` |
105
122
 
106
123
  The **secret scan** runs as a `PreToolUse` hook on Claude Code (hard-blocks a commit on a hit) and as a pre-push check elsewhere.
107
124
 
package/index.js CHANGED
@@ -50,7 +50,8 @@ if (command === "--version" || command === "-v" || command === "version") {
50
50
  Install:
51
51
  npx @mmerterden/multi-agent-pipeline install Install for Claude Code (default)
52
52
  npx @mmerterden/multi-agent-pipeline install --copilot Install for Copilot CLI
53
- npx @mmerterden/multi-agent-pipeline install --all Both Claude + Copilot
53
+ npx @mmerterden/multi-agent-pipeline install --codex Install for Codex CLI
54
+ npx @mmerterden/multi-agent-pipeline install --all Claude + Copilot + Codex
54
55
  npx @mmerterden/multi-agent-pipeline install --link Use symlinks (saves tokens, dev mode)
55
56
 
56
57
  Uninstall (token-preserving - Keychain/Credential Manager untouched):
@@ -58,7 +59,8 @@ if (command === "--version" || command === "-v" || command === "version") {
58
59
  npx @mmerterden/multi-agent-pipeline uninstall --yes Skip prompt
59
60
  npx @mmerterden/multi-agent-pipeline uninstall --dry-run Report what would be removed
60
61
  npx @mmerterden/multi-agent-pipeline uninstall --claude Only Claude Code
61
- npx @mmerterden/multi-agent-pipeline uninstall --cursor Legacy pre-v10.7 adapter-file cleanup (also --copilot-chat / --antigravity / --codex; --target=<path> overrides cwd)
62
+ npx @mmerterden/multi-agent-pipeline uninstall --codex Only Codex CLI
63
+ npx @mmerterden/multi-agent-pipeline uninstall --cursor Legacy pre-v10.7 adapter-file cleanup (also --copilot-chat / --antigravity; --target=<path> overrides cwd)
62
64
 
63
65
  Help:
64
66
  npx @mmerterden/multi-agent-pipeline help
@@ -72,6 +74,7 @@ if (command === "--version" || command === "-v" || command === "version") {
72
74
  After installation:
73
75
  Claude Code: /multi-agent "MOBILE-123"
74
76
  Copilot CLI: Describe your task naturally - pipeline instructions are loaded
77
+ Codex CLI: /multi-agent "MOBILE-123" (or $multi-agent)
75
78
 
76
79
  More info: https://github.com/mmerterden/multi-agent-pipeline
77
80
  `);
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Persona -> Codex subagent transform.
3
+ *
4
+ * Claude Code and Copilot CLI read the personas in `pipeline/agents/` directly
5
+ * as Markdown-with-frontmatter. Codex CLI wants one TOML file per agent under
6
+ * `$CODEX_HOME/agents/`, with the prompt body in `developer_instructions` and
7
+ * the model expressed as an OpenAI model id plus a reasoning effort.
8
+ *
9
+ * The `.md` files stay the single source of truth: this transform runs at
10
+ * install time only. Nothing edits a persona at runtime (see the non-goals in
11
+ * `multi-agent-refs/features/model-fallback.md`).
12
+ *
13
+ * @module install/_codex-agents
14
+ */
15
+
16
+ import { existsSync, readFileSync, readdirSync, rmSync } from "fs";
17
+ import { join } from "path";
18
+
19
+ import { ensureDir, ensureRealDir, isDryRun, writeFile } from "./_common.mjs";
20
+
21
+ /**
22
+ * Persona tier -> Codex model + reasoning effort.
23
+ *
24
+ * The pipeline's ladder is `fable -> opus -> sonnet -> haiku`. Codex offers no
25
+ * Anthropic models, so each tier maps onto an OpenAI model plus an effort
26
+ * setting: effort carries the depth distinction that the model id carries on
27
+ * Claude Code. Keep this table in sync with the Codex column of the Phase 4
28
+ * reviewer matrix and with `pipeline/scripts/cost-table.json`.
29
+ */
30
+ export const CODEX_TIER_MAP = Object.freeze({
31
+ fable: { model: "gpt-5.6", reasoning_effort: "xhigh" },
32
+ opus: { model: "gpt-5.6", reasoning_effort: "high" },
33
+ sonnet: { model: "gpt-5.4", reasoning_effort: "medium" },
34
+ haiku: { model: "gpt-5.6-terra", reasoning_effort: "low" },
35
+ });
36
+
37
+ /** Fallback when a persona declares no recognizable tier. */
38
+ const DEFAULT_TIER = "sonnet";
39
+
40
+ /**
41
+ * Split a persona file into its frontmatter map and body.
42
+ *
43
+ * Deliberately a minimal parser, not a YAML engine: persona frontmatter is a
44
+ * flat `key: value` map with optionally double-quoted values, and adding a YAML
45
+ * dependency to the installer for eight files is not worth it.
46
+ *
47
+ * @param {string} raw - full file content
48
+ * @returns {{front: Record<string,string>, body: string}}
49
+ */
50
+ export function parsePersona(raw) {
51
+ const front = {};
52
+ const lines = raw.split("\n");
53
+ if (lines[0]?.trim() !== "---") return { front, body: raw.trim() };
54
+
55
+ let i = 1;
56
+ for (; i < lines.length; i++) {
57
+ if (lines[i].trim() === "---") {
58
+ i++;
59
+ break;
60
+ }
61
+ const m = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(lines[i]);
62
+ if (!m) continue;
63
+ let value = m[2].trim();
64
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
65
+ value = value.slice(1, -1);
66
+ }
67
+ front[m[1]] = value;
68
+ }
69
+ return { front, body: lines.slice(i).join("\n").trim() };
70
+ }
71
+
72
+ /**
73
+ * Encode a string as a TOML value.
74
+ *
75
+ * Multi-line bodies use a literal string (`'''`), which needs no escaping at
76
+ * all. If a body ever contains `'''` the literal form is unusable, so fall back
77
+ * to a basic multi-line string with the three sequences TOML requires escaping.
78
+ * Without that guard a future persona could silently emit invalid TOML.
79
+ *
80
+ * @param {string} s
81
+ * @returns {string} TOML-quoted scalar
82
+ */
83
+ export function tomlString(s) {
84
+ if (!s.includes("\n")) {
85
+ return JSON.stringify(s);
86
+ }
87
+ if (!s.includes("'''")) {
88
+ return `'''\n${s}\n'''`;
89
+ }
90
+ const escaped = s.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"');
91
+ return `"""\n${escaped}\n"""`;
92
+ }
93
+
94
+ /**
95
+ * Render one persona as a Codex agent TOML document.
96
+ *
97
+ * @param {string} name - agent name (persona filename stem)
98
+ * @param {string} raw - persona file content
99
+ * @returns {string} TOML
100
+ */
101
+ export function renderCodexAgent(name, raw) {
102
+ const { front, body } = parsePersona(raw);
103
+ const tier = front.preferredModel || front.model || DEFAULT_TIER;
104
+ const mapped = CODEX_TIER_MAP[tier] || CODEX_TIER_MAP[DEFAULT_TIER];
105
+
106
+ const lines = [
107
+ GENERATED_MARKER,
108
+ `# Source of truth: pipeline/agents/${name}.md - edit that, then re-install.`,
109
+ "",
110
+ `name = ${tomlString(name)}`,
111
+ `description = ${tomlString(front.description || name)}`,
112
+ `model = ${tomlString(mapped.model)}`,
113
+ `model_reasoning_effort = ${tomlString(mapped.reasoning_effort)}`,
114
+ `developer_instructions = ${tomlString(body)}`,
115
+ "",
116
+ ];
117
+ return lines.join("\n");
118
+ }
119
+
120
+ /**
121
+ * Generate `$CODEX_HOME/agents/<persona>.toml` for every persona.
122
+ *
123
+ * Only pipeline-owned files are removed before writing - `~/.codex/agents/`
124
+ * also holds user-authored agents, so the directory is never wiped. The owned
125
+ * name set is derived from the source tree so it cannot go stale.
126
+ *
127
+ * @param {string} agentsSrc - `pipeline/agents`
128
+ * @param {string} dest - `$CODEX_HOME/agents`
129
+ * @returns {number} how many agent files were written
130
+ */
131
+ export function installCodexAgents(agentsSrc, dest) {
132
+ if (!existsSync(agentsSrc)) return 0;
133
+ const personas = readdirSync(agentsSrc).filter((f) => f.endsWith(".md"));
134
+ if (personas.length === 0) return 0;
135
+
136
+ // ensureRealDir only swaps a --link-era symlink for a real dir; it does not
137
+ // create a missing one. Both calls are needed.
138
+ ensureRealDir(dest);
139
+ ensureDir(dest);
140
+
141
+ // `~/.codex/agents/` also holds user-authored agents, so never wipe the dir.
142
+ // Remove only pipeline-owned files whose persona no longer exists upstream,
143
+ // otherwise a renamed persona lingers as a ghost agent Codex can still spawn.
144
+ removeStalePipelineAgents(dest, personas);
145
+
146
+ let written = 0;
147
+ for (const file of personas) {
148
+ const name = file.replace(/\.md$/, "");
149
+ const toml = renderCodexAgent(name, readFileSync(join(agentsSrc, file), "utf-8"));
150
+ const target = join(dest, `${name}.toml`);
151
+ if (isDryRun()) {
152
+ console.log(` [dry-run] would write ${target}`);
153
+ written++;
154
+ continue;
155
+ }
156
+ writeFile(target, toml);
157
+ written++;
158
+ }
159
+ return written;
160
+ }
161
+
162
+ /** Header line stamped on every generated agent file, used to identify ownership. */
163
+ const GENERATED_MARKER = "# Generated by multi-agent-pipeline install --codex.";
164
+
165
+ /**
166
+ * Remove generated agent files whose source persona is gone.
167
+ *
168
+ * Identified by the generated-marker header, so a user-authored `.toml` that
169
+ * happens to share a name is never deleted.
170
+ *
171
+ * @param {string} dest - `$CODEX_HOME/agents`
172
+ * @param {string[]} personas - current `pipeline/agents/*.md` filenames
173
+ * @returns {number} files removed
174
+ */
175
+ function removeStalePipelineAgents(dest, personas) {
176
+ if (!existsSync(dest)) return 0;
177
+ const current = new Set(personas.map((f) => f.replace(/\.md$/, ".toml")));
178
+ let removed = 0;
179
+ for (const name of readdirSync(dest)) {
180
+ if (!name.endsWith(".toml") || current.has(name)) continue;
181
+ const path = join(dest, name);
182
+ let owned;
183
+ try {
184
+ owned = readFileSync(path, "utf-8").startsWith(GENERATED_MARKER);
185
+ } catch {
186
+ continue;
187
+ }
188
+ if (!owned) continue;
189
+ if (isDryRun()) {
190
+ console.log(` [dry-run] would remove stale generated agent ${path}`);
191
+ removed++;
192
+ continue;
193
+ }
194
+ try {
195
+ rmSync(path, { force: true });
196
+ removed++;
197
+ } catch {
198
+ /* non-fatal */
199
+ }
200
+ }
201
+ if (removed > 0) console.log(` -> removed ${removed} stale generated agent file(s)`);
202
+ return removed;
203
+ }
204
+
205
+ /**
206
+ * Header the installer stamps on generated agent files. Uninstall matches on
207
+ * this rather than on a name list derived from the source tree: ownership has
208
+ * to be decidable from the installed file alone, since uninstall can run from a
209
+ * different pipeline version than the one that installed.
210
+ */
211
+ export { GENERATED_MARKER as CODEX_AGENT_GENERATED_MARKER };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Codex CLI instructions block - read from a Markdown source file.
3
+ *
4
+ * Mirrors `_copilot-instructions.mjs`: the canonical text lives in
5
+ * `install/templates/codex-instructions.md` so it is editable as Markdown
6
+ * (renders on GitHub, lints with prettier, diffs cleanly) rather than as an
7
+ * escaped JavaScript template literal.
8
+ *
9
+ * This is a separate template from the Copilot one on purpose. The two hosts
10
+ * differ in the places that matter: tree paths (`~/.codex` vs `~/.copilot`),
11
+ * persona file format (TOML vs Markdown), progress channel (`update_plan` vs
12
+ * `phase-tracker.sh render`), and Codex's two silent-failure constraints
13
+ * (sub-agent delegation gating, skills-block overflow). Substituting paths into
14
+ * the Copilot text would produce a document that is wrong in all four places.
15
+ *
16
+ * @module install/_codex-instructions
17
+ */
18
+
19
+ import { readFileSync } from "fs";
20
+ import { dirname, join } from "path";
21
+ import { fileURLToPath } from "url";
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const TEMPLATE_PATH = join(__dirname, "templates", "codex-instructions.md");
25
+
26
+ /**
27
+ * Build the pipeline instructions block for `~/.codex/AGENTS.md`.
28
+ *
29
+ * @returns {string} Markdown content
30
+ */
31
+ export function generateCodexInstructions() {
32
+ return readFileSync(TEMPLATE_PATH, "utf-8");
33
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Managed-block merge for host instruction files.
3
+ *
4
+ * A host instruction file (`~/.copilot/copilot-instructions.md`,
5
+ * `~/.codex/AGENTS.md`) is co-owned: the pipeline owns one span, the user owns
6
+ * everything around it. Every install replaces only that span, so user content
7
+ * before and after survives.
8
+ *
9
+ * Extracted from `install/copilot.mjs` in v13.0.0 when Codex CLI became a third
10
+ * install target. The bounding logic carries two rounds of drift-cleanup fixes
11
+ * (v5.6.2 pre-marker sections, v11.4.1 explicit end marker) and must not be
12
+ * re-derived per target.
13
+ *
14
+ * @module install/_managed-block
15
+ */
16
+
17
+ import { existsSync, readFileSync } from "fs";
18
+
19
+ import { writeFile } from "./_common.mjs";
20
+
21
+ /**
22
+ * Legacy files (written before the end marker existed) have no explicit
23
+ * terminator. Bound the pipeline span at the next top-level "# " heading after
24
+ * the start marker when one exists OUTSIDE fenced code blocks (the pipeline
25
+ * body carries bash comments like "# Bootstrap once ..." inside fences that
26
+ * must not be mistaken for headings); otherwise the span runs to EOF, which
27
+ * matches the pre-marker behavior.
28
+ *
29
+ * @param {string} section - file content from the start marker onward
30
+ * @returns {string} user content trailing the pipeline span ("" if none)
31
+ */
32
+ export function legacyTrailingContent(section) {
33
+ const lines = section.split("\n");
34
+ let inFence = false;
35
+ for (let i = 1; i < lines.length; i++) {
36
+ if (/^\s*(```|~~~)/.test(lines[i])) {
37
+ inFence = !inFence;
38
+ continue;
39
+ }
40
+ if (!inFence && /^# /.test(lines[i])) return lines.slice(i).join("\n");
41
+ }
42
+ return "";
43
+ }
44
+
45
+ /**
46
+ * Write `body` into `path` as the pipeline-managed span, preserving user
47
+ * content on both sides.
48
+ *
49
+ * @param {object} opts
50
+ * @param {string} opts.path - instruction file to merge into
51
+ * @param {string} opts.body - pipeline section content (without the end marker)
52
+ * @param {string} opts.startMarker - first line of the managed span
53
+ * @param {string} opts.endMarker - HTML comment terminating the managed span
54
+ * @param {RegExp[]} [opts.driftPatterns] - pre-marker sections to scrub
55
+ * @returns {"created"|"updated"|"appended"} what the merge did
56
+ */
57
+ export function mergeManagedBlock(opts) {
58
+ const { path, body, startMarker, endMarker, driftPatterns = [] } = opts;
59
+ const managedBlock = body.trimEnd() + "\n\n" + endMarker + "\n";
60
+
61
+ if (!existsSync(path)) {
62
+ writeFile(path, managedBlock);
63
+ return "created";
64
+ }
65
+
66
+ let existing = readFileSync(path, "utf-8");
67
+
68
+ // Drift cleanup: strip pipeline sections written by installers that predate
69
+ // the stable marker. Without this they linger and contradict current output.
70
+ let cleaned = false;
71
+ for (const re of driftPatterns) {
72
+ if (re.test(existing)) {
73
+ existing = existing.replace(re, "");
74
+ cleaned = true;
75
+ }
76
+ }
77
+
78
+ if (!existing.includes(startMarker)) {
79
+ writeFile(path, existing.trimEnd() + "\n\n" + managedBlock);
80
+ return "appended";
81
+ }
82
+
83
+ const startIdx = existing.indexOf(startMarker);
84
+ const before = existing.slice(0, startIdx).trimEnd();
85
+ const fromStart = existing.slice(startIdx);
86
+ // Replace only the start..end span. User content appended AFTER the pipeline
87
+ // section (below the end marker, or below the next top-level heading in
88
+ // legacy files) is preserved.
89
+ const endIdx = fromStart.indexOf(endMarker);
90
+ const trailing =
91
+ endIdx >= 0 ? fromStart.slice(endIdx + endMarker.length) : legacyTrailingContent(fromStart);
92
+
93
+ let out = before.length > 0 ? before + "\n\n" : "";
94
+ out += managedBlock;
95
+ const trailingClean = trailing.replace(/^[\r\n]+/, "").trimEnd();
96
+ if (trailingClean.length > 0) out += "\n" + trailingClean + "\n";
97
+ writeFile(path, out);
98
+ return cleaned ? "updated+scrubbed" : "updated";
99
+ }