@mmerterden/multi-agent-pipeline 12.11.0 → 13.1.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 (47) hide show
  1. package/CHANGELOG.md +179 -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 +52 -6
  15. package/pipeline/commands/multi-agent/dev-local/SKILL.md +21 -0
  16. package/pipeline/commands/multi-agent/finish/SKILL.md +1 -1
  17. package/pipeline/commands/multi-agent/language/SKILL.md +2 -2
  18. package/pipeline/commands/multi-agent/setup/SKILL.md +69 -2
  19. package/pipeline/commands/multi-agent/sync/SKILL.md +128 -5
  20. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +219 -0
  21. package/pipeline/commands/multi-agent/update/SKILL.md +7 -4
  22. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  23. package/pipeline/multi-agent-refs/component-dispatch.md +40 -7
  24. package/pipeline/multi-agent-refs/cross-cli-contract.md +51 -17
  25. package/pipeline/multi-agent-refs/features/model-fallback.md +29 -0
  26. package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
  27. package/pipeline/multi-agent-refs/phases/log-format.md +1 -1
  28. package/pipeline/multi-agent-refs/phases/phase-0-init.md +43 -2
  29. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +24 -1
  30. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +32 -0
  31. package/pipeline/multi-agent-refs/phases/phase-4-review.md +62 -5
  32. package/pipeline/multi-agent-refs/progress-contract.md +1 -1
  33. package/pipeline/multi-agent-refs/tracker-contract.md +17 -1
  34. package/pipeline/schemas/prefs.schema.json +296 -62
  35. package/pipeline/schemas/reviewer-output.schema.json +1 -1
  36. package/pipeline/schemas/triage-output.schema.json +1 -1
  37. package/pipeline/scripts/cost-table.json +15 -1
  38. package/pipeline/scripts/phase0-exit-gate.mjs +185 -0
  39. package/pipeline/scripts/smoke-cross-cli-behavior.sh +25 -10
  40. package/pipeline/scripts/uninstall.mjs +105 -9
  41. package/pipeline/scripts/update-check.sh +2 -1
  42. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +21 -0
  43. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +21 -0
  44. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
  45. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +48 -1
  46. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +87 -6
  47. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +120 -0
package/CHANGELOG.md CHANGED
@@ -16,6 +16,185 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [13.1.0] - 2026-07-27
20
+
21
+ Six gates, from one branch that spent half its commits on rework.
22
+
23
+ A Figma-driven screen task was built through the generic development path. The cause
24
+ chain, measured from the run's own artefacts: Phase 0 reported `completed` having
25
+ written only `tracker-state.json`, so `agent-state.json` and its `taskType` never
26
+ existed, so Phase 3's component dispatch could not fire. The stack plugin already
27
+ ships `create-screen`, `figma-validate` (7 criteria including design-token compliance
28
+ and Code Connect strategy) and `figma-review` (14-item checklist) - none of them ran.
29
+ Padding came out 16 where the frame said `Spacing/12`. Three of six commits were fixes,
30
+ the last a full sheet rebuild.
31
+
32
+ Nothing here is a new capability. Every gate enforces a rule that already existed or
33
+ invokes a skill that was already written but never called.
34
+
35
+ ### Added
36
+
37
+ - **`phase0-exit-gate.mjs`, blocking.** Phase 0 may not be marked completed until
38
+ `agent-state.json` exists with a `taskType`, and a Figma reference forces
39
+ `taskType: "component"` plus a recorded `figmaAccess.tier`. A phase that reports
40
+ success without its output is worse than one that fails: every later phase then
41
+ reasons from a field that is not there. The evaluator is pure and exported, so the
42
+ gate is testable without reproducing a run.
43
+ - **Phase 4 Step 2.8, visual conformance gate.** Runs `figma-review` and
44
+ `/multi-agent:design-check` for component/screen work, with the coverage gate, and
45
+ asserts Code Connect was **published** rather than merely written - a
46
+ `*.figma.swift` on disk with "Not published" in Figma is a binding that exists for
47
+ nobody. `design-check` had been a command with no phase invoking it, so the only
48
+ thing standing between a build and visual drift was the user opening the app.
49
+ - **Phase 1 Step 1.45, reuse discovery, blocking.** Search for an existing wrapper,
50
+ entity, mapper or screen before proposing a new one. One run wrote a repository over
51
+ an endpoint a sibling domain already wrapped **with its country parameter**, called
52
+ the generated method without it, and re-invented an entity the module already had.
53
+ "Copy X and rename it" is the reuse answer, not a hint.
54
+ - **`smoke-component-dispatch-gates.sh`** (12 assertions) pins all six.
55
+
56
+ ### Changed
57
+
58
+ - **Component dispatch halts instead of degrading.** The contract used to send an
59
+ incomplete-state component task down the generic TDD path while the next sentence
60
+ said "never silently skip the Figma work" - taking the generic path *is* skipping
61
+ it. That wording authorised the exact failure above.
62
+ - **Dispatch routes on scope, not just platform.** A screen and a component are
63
+ different jobs and the plugin ships a skill for each; routing a screen to the
64
+ component skill is why one run produced entities and a mapper but left the screen
65
+ half-wired. `figma-validate` now runs before the create skill.
66
+ - **Phase 1 captures spacing by token name, per atom.** Phase 3 is forbidden from
67
+ calling Figma, so a pixel number - or a missing entry - is unrecoverable later. A
68
+ UI frame with no spacing entries is a capture failure, not an empty frame.
69
+ - **Phase 3 records that generated trees are not editable.** A mock fixture went into
70
+ the generated tree; the fix moved it to the custom tree and registered the scenario
71
+ in the generated index. Same content, wrong side of the generator, and the Debug menu
72
+ never showed it.
73
+ - **Fast modes warn when handed an analysis document.** `--dev` skips Analysis and
74
+ Planning by design, so there is no phase that turns a document into a plan. The doc
75
+ becomes context for one pass and work lands in whatever order it was read.
76
+
77
+
78
+ ## [13.0.0] - 2026-07-27
79
+
80
+ > **Why major and not minor.** The additions here are additive, but two defaults
81
+ > changed in ways a reasonable workflow would notice: `install --all` now writes a
82
+ > third tree (`~/.codex`) and registers an MCP server via `codex mcp add`, and the
83
+ > cross-CLI contract changed shape (the parity axis is per-host now, so an auditor
84
+ > comparing Codex on skill directories would read a correct install as drift).
85
+ > Per the versioning policy that is a changed default, not a new option.
86
+
87
+
88
+ Codex CLI becomes a third supported host, and a new pre-submission validation command.
89
+
90
+ ### Added
91
+
92
+ - **Codex CLI as a first-class target** (`install --codex`, included in `--all`).
93
+ The v9.7.0 adapter was deleted in v10.7.0 because Codex was then a degraded
94
+ consumer with no sub-agent fan-out. Codex 0.145 has skills, parallel sub-agents
95
+ with per-agent model and reasoning effort, hooks, MCP and a plugin marketplace,
96
+ so it earns the same treatment as the other two rather than an adapter.
97
+ `install/codex.mjs` writes `~/.codex/{skills/multi-agent,multi-agent-refs,
98
+ agents/*.toml,prompts/multi-agent.md,scripts,lib,schemas,rules}` plus a managed
99
+ span in `~/.codex/AGENTS.md`, and registers the dev-toolkit MCP server through
100
+ `codex mcp add` rather than hand-merging TOML that Codex owns.
101
+ - **`/multi-agent:testflight-validation`** (command 43). Three gates, each seeing
102
+ what the others structurally cannot: the static 18-rule archive audit, Apple's
103
+ own `altool --validate-app`, and a Review-Guidelines check against repo evidence.
104
+ ITMS codes are mapped to the rule each implies. It validates only - never
105
+ `--upload-app` - so a validation run cannot ship a build by accident.
106
+ - **App Store Connect credentials in `/multi-agent:setup`**, inside the Step 1
107
+ discovery / Step 2 mapping / Step 3b onboarding flow alongside Jira and
108
+ Bitbucket, not as a late add-on: a user who already has one in their keychain
109
+ gets it mapped automatically. Tier 1 is an API key, tier 2 an Apple ID plus an
110
+ app-specific password - which matters because creating an API key needs an
111
+ Admin or App Manager role many developers on a corporate team do not have.
112
+
113
+ ### Changed
114
+
115
+ - **Codex takes the Claude Code thin-dispatcher shape, and has to.** Measured on
116
+ Codex 0.145: installing one plugin declaring 142 skills took the assembled
117
+ skills block from 11 skills / 4,710 bytes to 83 / 22,111, surfacing only **75 of
118
+ 142** and **evicting an unrelated user-scope skill**. Shipping the 43
119
+ sub-commands as peer skills would silently lose pipeline commands next to any
120
+ stack toolkit. The pipeline contributes exactly one skill on Codex and keeps the
121
+ specs as refs; `smoke-install-layout.sh` fails if a second one appears.
122
+ - Phase 4 reviewer matrix gains a Codex column (gpt-5.6 @ xhigh / gpt-5.4 /
123
+ gpt-5.6 @ medium, triage at max) with two measured constraints written into the
124
+ contract: a `spawn_agent` that sets `model` without `fork_turns: "none"`
125
+ **silently inherits the parent model**, collapsing the panel onto one
126
+ perspective; and 4 concurrency slots *including the orchestrator* make three
127
+ reviewers the ceiling. Single-vendor caveat recorded - consensus among three
128
+ OpenAI models is weaker evidence than the same consensus on a two-vendor host.
129
+ - `tracker-contract.md` gains the `codex` visual channel: the native `update_plan`
130
+ tool, with its no-parallel-call and no-plan-mode caveats.
131
+ - Cross-CLI contract retitled for three hosts; parity axis for Codex is the **ref
132
+ set**, not the skill set, because comparing skill directories would demand the
133
+ layout that breaks it.
134
+
135
+ ### Fixed
136
+
137
+ - **The remote reachability gate blamed the network for every failure.** Any
138
+ non-zero `git ls-remote` exit was reported as `unreachable (VPN/DNS)`, so a
139
+ missing-credential error that returns in under a second sent the user to enable
140
+ a VPN that could not help, and offered a stale-base fallback for a cause that
141
+ had nothing to do with staleness. Failures are now classified from stderr
142
+ (credential / network / wrong remote / unknown), the observed line is printed
143
+ next to the classification, and only the network case offers the cached ref.
144
+ - **The Phase 0 branch-collision probe read a failed probe as "no collision".**
145
+ With `2>/dev/null` and an empty-output test, an auth or network failure was
146
+ indistinguishable from "the ref does not exist", so the run created a branch
147
+ that already existed on the remote - surfacing as a rejected push at Phase 6,
148
+ far from its cause. Exit codes are now distinguished (0 exists, 2 free, anything
149
+ else unknown-and-recorded).
150
+ - **`/multi-agent:update` deleted the Codex prompt on every run**, pruning
151
+ `~/.codex/prompts/multi-agent.md` as a retired v9.7.0 adapter leftover.
152
+ - **Uninstall could delete trailing user content in `~/.codex/AGENTS.md`.**
153
+ `stripManagedBlock` matched only the Copilot end marker, so on the Codex marker
154
+ it fell through to a heading-bounded fallback that returns nothing when the
155
+ trailing content has no top-level heading. Verified against the pre-fix path.
156
+ - `smoke-own-punctuation.sh` failed on HEAD: `test/tracker-title-entities.test.mjs`
157
+ (added in v12.11.0) holds the banned characters as its own assertion list and
158
+ was never allowlisted.
159
+ - **Half-English pickers on Turkish runs: six shipped files contradicted the
160
+ canonical language matrix.** `rules.md` is unambiguous - `AskUserQuestion`
161
+ `question` and `options[].description` render in `outputLanguage`, only `label`,
162
+ `header` and host chrome are pinned to English. But `/multi-agent:setup` Step 0
163
+ claimed `promptLanguage` governs "interactive pickers and prompts ... Picker UI is
164
+ always English", `/multi-agent:language` claimed "confirmation prompts ... are
165
+ authored in English. Only the assistant's free-form replies follow
166
+ `outputLanguage`", and four more files said variations of the same. Those are the
167
+ two commands a user goes to *configure* this, so the model followed whichever
168
+ canonical-looking doc it read first and gate questions came out English. All six
169
+ corrected to describe `promptLanguage`'s real scope: the button and chip chrome,
170
+ never the question a user reads.
171
+
172
+ ### Gates (continued)
173
+
174
+ - New `smoke-language-matrix.sh`: asserts `rules.md` still pins the per-field
175
+ matrix, then greps every shipped command / skill / ref for the seven phrasings
176
+ that actually shipped and contradicted it. Two authoritative docs giving opposite
177
+ answers is a spec with two answers, not a wording nit - the gate found a sixth
178
+ violation (`_input-parser.md`) that the manual sweep had missed.
179
+
180
+ ### Gates
181
+
182
+ - New `smoke-codex-install.sh` (37 assertions): install/uninstall round-trip into a
183
+ HOME seeded with user content, asserting every artifact lands, every rewritten
184
+ `$HOME/.codex` path resolves, generated agent TOML parses, and user content is
185
+ byte-identical afterwards.
186
+ - `smoke-install-layout.sh` extended to three targets, including a check that every
187
+ concrete `$HOME/.codex` reference **resolves on disk**. The earlier "no `.claude`
188
+ references remain" check was blind to a wrongly-rewritten path, which is how
189
+ `commands/multi-agent.md` became `multi-agent-refs/commands.md` - well-formed,
190
+ pointing at nothing. A miss is classified by whether the Claude counterpart
191
+ exists, so broken-by-rewrite fails and broken-upstream is only reported.
192
+ - New `test/codex-install.test.mjs` (24 assertions) locks the path-rewrite map,
193
+ the frontmatter transform, the persona tier map, and managed-block trailing
194
+ content, including regressions for both rewrite defects above.
195
+ - `MULTI_AGENT_SKIP_MCP_REGISTER=1` makes an install hermetic, so the layout
196
+ fingerprint no longer depends on whether `codex` is on PATH.
197
+
19
198
  ## [12.11.0] - 2026-07-26
20
199
 
21
200
  ### 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
+ }