@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.
- package/CHANGELOG.md +120 -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/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
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
|
[](https://github.com/mmerterden/multi-agent-pipeline/blob/main/package.json)
|
|
7
7
|
[](https://scorecard.dev/viewer/?uri=github.com/mmerterden/multi-agent-pipeline)
|
|
8
8
|
|
|
9
|
-
An 8-phase AI development pipeline for **Claude Code** and **
|
|
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
|
|
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`.
|
|
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 **
|
|
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 |
|
|
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 --
|
|
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 --
|
|
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
|
+
}
|