cohorte 2.9.0 → 2.10.1

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 (39) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +37 -45
  3. package/bin/cli.js +15 -43
  4. package/bin/report.js +2 -2
  5. package/core/adapter/render.js +33 -7
  6. package/core/commands/cohorte-doctor.md +28 -4
  7. package/core/commands/cohorte-fleet.md +2 -3
  8. package/core/commands/cohorte-ship.md +2 -2
  9. package/core/commands/cohorte-update-pipeline.md +34 -3
  10. package/core/hooks/gate.py +13 -5
  11. package/core/runtimes/codex.json +5 -3
  12. package/core/templates/steps/init-pipeline/04-write-render.md +32 -2
  13. package/core/workflows/review.js +1 -1
  14. package/{dashboard/server → lib}/doctor.js +48 -17
  15. package/{dashboard/server → lib}/runtime.js +6 -1
  16. package/{dashboard/server → lib}/versions.js +2 -2
  17. package/package.json +3 -8
  18. package/profile/PIPELINE.template.md +10 -2
  19. package/profile/SCHEMA.md +43 -6
  20. package/scripts/test-adapter.mjs +70 -1
  21. package/scripts/test-gate.mjs +15 -0
  22. package/scripts/{test-dashboard.mjs → test-lib.mjs} +58 -229
  23. package/scripts/validate-core.mjs +9 -19
  24. package/dashboard/README.md +0 -71
  25. package/dashboard/dist/apple-touch-icon-180.png +0 -0
  26. package/dashboard/dist/assets/index-BZ_LQlEj.css +0 -1
  27. package/dashboard/dist/assets/index-vtFc6Gyc.js +0 -43
  28. package/dashboard/dist/favicon-16.png +0 -0
  29. package/dashboard/dist/favicon-32.png +0 -0
  30. package/dashboard/dist/favicon-48.png +0 -0
  31. package/dashboard/dist/icon-192.png +0 -0
  32. package/dashboard/dist/icon-512.png +0 -0
  33. package/dashboard/dist/index.html +0 -16
  34. package/dashboard/server/fleet.js +0 -133
  35. package/dashboard/server/index.js +0 -408
  36. package/dashboard/server/kanban.js +0 -169
  37. package/dashboard/server/metrics.js +0 -120
  38. package/dashboard/server/usage.js +0 -61
  39. /package/{dashboard/server → lib}/yaml.js +0 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,39 @@ short, user-facing, most recent first. One `## <version> — <YYYY-MM-DD>` secti
7
7
  > They are history and are deliberately not rewritten — every command gained a `cohorte-` prefix
8
8
  > in 2.0.0.
9
9
 
10
+ ## 2.10.1 — 2026-09-14
11
+
12
+ - **Codex project isolation:** surface agents stay in each repository's `.codex/agents/*.toml`
13
+ even with a global core; no `CODEX_HOME` launcher or auth symlink is required.
14
+ - **Codex correctness:** init/update/doctor use native TOML agents and MCP settings, preflight
15
+ covers `spawn_agent` dispatches, explicit Codex model choices survive, and implementer
16
+ templates no longer accidentally force a read-only sandbox. Added regression coverage.
17
+
18
+ ## 2.10.0 — 2026-08-24
19
+
20
+ - **The dashboard is gone.** The local web cockpit — the `cohorte dashboard` verb, the
21
+ dependency-free HTTP server, the Vite/React app and the prebuilt `dashboard/dist` that shipped
22
+ in every tarball — is removed, along with the fleet registry, kanban and usage readers that
23
+ only it consumed. It was a second surface for state the CLI already reports, with its own
24
+ build step, its own security posture (loopback-bound because its action endpoints executed
25
+ code: install, update, reset, and `/cohorte-init-pipeline`·`/cohorte-update-pipeline`·`/cohorte-audit`
26
+ through headless Claude), and its own drift to police. `cohorte doctor`, `cohorte specs` and
27
+ `cohorte metrics` answer the same questions from the shell, and `doctor` exits 1 on any bad
28
+ check, which the browser never could.
29
+
30
+ **What did not go with it:** the four modules those verbs actually run on — the JS port of
31
+ `/cohorte-doctor`, the runtime-layout resolver, the version prober and the block-YAML parser —
32
+ moved from `dashboard/server/` to **`lib/`**. `cohorte doctor` / `specs` / `version` behave
33
+ exactly as before, and so do the Francois extension's `--panel` payloads. Their test suite
34
+ moved with them (`scripts/test-dashboard.mjs` ⇒ `scripts/test-lib.mjs`, 54 assertions), minus
35
+ the cases that covered deleted code.
36
+
37
+ Fallout worth naming: the package no longer has a build step at all (`prepack` and
38
+ `build:dashboard` are gone, npm and CI no longer build a React app to publish), the tarball
39
+ drops from 88 files to 73, and `~/.claude/cohorte-dashboard.json` — the tracked-project list —
40
+ is now dead; delete it. `--port` / `--host` / `--open` and `COHORTE_DASHBOARD_PORT` /
41
+ `COHORTE_DASHBOARD_HOST` no longer exist; `cohorte dashboard` prints the usage and exits 2.
42
+
10
43
  ## 2.9.0 — 2026-08-22
11
44
 
12
45
  - **`/cohorte-intake` — the door before the doors.** Work has always entered the pipeline
package/README.md CHANGED
@@ -24,6 +24,37 @@ Cursor, Gemini CLI or OpenCode. Install it once globally, then one command per p
24
24
  /cohorte-brainstorm → /cohorte-spec → (design) → /cohorte-build <id> → /cohorte-review → (/cohorte-fix) → /cohorte-ship
25
25
  ```
26
26
 
27
+ <div align="center">
28
+ <img src="https://raw.githubusercontent.com/TheBidouilleAgency/cohorte/main/assets/demo-cli.gif" alt="cohorte doctor reporting a green pipeline, the spec board, and the gate denying a chained destructive command" width="760">
29
+ <br>
30
+ <sub><code>cohorte doctor</code> · the spec board · the gate refusing a hard-denied command chained behind a benign one.<br>
31
+ Recorded from the real CLI by <code>scripts/demo/record-cli.sh</code> — no output is hand-edited.</sub>
32
+ </div>
33
+
34
+ ## Why a pipeline at all
35
+
36
+ Your coding agent is already good at the first prompt of a feature. It gets worse at every one
37
+ after — because what it knows lives in a conversation, and a conversation gets summarised,
38
+ truncated, and re-sent at input price on every turn. Cohorte is the set of constraints that stop
39
+ that degradation. Each one is a failure you have already had:
40
+
41
+ | The failure | The constraint |
42
+ | --- | --- |
43
+ | *"It forgot what we decided."* | The spec is **frozen to disk** and re-read by every stateless agent. Nothing is remembered, so nothing is forgotten — and `/clear` between stages is always safe. |
44
+ | *"The API and the UI don't fit."* | The **contract is authored before any implementer is dispatched**. Surfaces import it read-only and never talk to each other — which is what makes building them in parallel safe. |
45
+ | *"It edited a file I didn't want it to."* | **One owner per tree**, enforced in the rendered agent's frontmatter — not requested politely in a prompt. |
46
+ | *"It ran something it shouldn't have."* | `gate.py` is a **real blocking hook**, branch-aware, hard-denying destructive commands from every agent — subagents included. |
47
+ | *"The review said it was fine."* | Review is a **separate read-only agent** that sees the spec and the diff, never the conversation that produced them. A dead reviewer's zero findings can't read as ship. |
48
+ | *"Four agents just told me it doesn't compile."* | A **deterministic preflight** runs typecheck/lint/tests first. Red ⇒ **zero agents spawned**. |
49
+ | *"We fixed this same thing last week."* | `/cohorte-retro` turns repeated findings into ratified conventions **baked into the implementers**, so the next build never produces them. |
50
+
51
+ None of this is intelligence added to your agent — it's the opposite. It moves the decisions an
52
+ agent makes badly under context pressure into files, hooks and ownership boundaries, where they're
53
+ cheap and don't degrade with conversation length. Which is why it's markdown, shell and one Python
54
+ hook: **your app code never imports anything from Cohorte.**
55
+
56
+ [**The long version — including the four cases where you shouldn't use it →**](https://thebidouilleagency.github.io/cohorte/guide/why-cohorte)
57
+
27
58
  ## How it works — three layers
28
59
 
29
60
  | Layer | What it holds | Lives in | Scope |
@@ -206,45 +237,6 @@ decisions), surface agents are re-rendered, settings are patched additively, new
206
237
  wired. **`/cohorte-init-pipeline` is one-time per project** — after init, `/cohorte-update-pipeline` is the only
207
238
  maintenance command you ever run (`/cohorte-build` auto-grows surfaces as specs need them).
208
239
 
209
- ## Dashboard — a local web cockpit
210
-
211
- A browser view of pipeline state, for when a checklist beats scanning files:
212
-
213
- ```sh
214
- cohorte dashboard # serves http://localhost:4317 (Ctrl-C to stop)
215
- cohorte dashboard <path> # start focused on another project
216
- cohorte dashboard --port=4400 --open # custom port, open the browser
217
- ```
218
-
219
- **Bound to `127.0.0.1` by default** — the dashboard's actions execute code (install/update/reset,
220
- and `/cohorte-init-pipeline`·`/cohorte-update-pipeline`·`/cohorte-audit` via headless Claude), so it must stay on loopback. Each user
221
- runs their own agent and drives only their own machine. `--host=0.0.0.0` exposes it to the network
222
- (it prints a security warning) — only on a trusted network, since anyone who reaches the port can run
223
- those actions.
224
-
225
- - **Fleet overview** — the global core version vs npm latest, plus every tracked project's freshness
226
- and health at a glance. Add a project by absolute path or with the **folder picker** (Browse…); the
227
- set is remembered in `~/.claude/cohorte-dashboard.json`.
228
- - **Per-project drill-down** — Freshness (installed core vs npm), `/cohorte-doctor` rendered as a live
229
- ✅/⚠️/❌ checklist (each failure with its fix), the **Surfaces ↔ agents** map from `PIPELINE.md`,
230
- and one board: a **Kanban** if the project has a linked Obsidian board (columns + cards from the
231
- vault, with clickable PR links + live open/merged/closed status and a ship-date-sorted Shipped
232
- column, via `gh`), otherwise a **Specs board** from `specs/*.md` (by `draft · frozen · in-progress ·
233
- in-review · blocked · shipped`). The Kanban supersedes the Specs board when both would apply.
234
- - **Actions** (stream their output live) — **Update / Install core** (the shared global core, or a
235
- repo's bundled core); **Init-pipeline / Update-pipeline / Audit**, which run those Claude Code
236
- commands **headless** (`claude -p`, autonomous — Init skips the interactive interview, so review
237
- the result; Audit writes `specs/refactor-backlog.md`; headless runs start without any prompt and
238
- have **no resume** if the session dies); and **Reset pipeline**, which backs up then wipes a
239
- project's pipeline footprint and reinstalls a fresh core. Buttons render only when they apply
240
- (e.g. Init only when there's no profile). The drill-down's health checklist also shows the
241
- **workflows** state (scripts + profile-reader installed, which path a session will take).
242
-
243
- Runtime is **dependency-free** — node's built-in `http` server serves a prebuilt React app (the app
244
- source lives in `dashboard/app/`, built to `dashboard/dist/` at publish time). The `/cohorte-doctor` checks
245
- are reimplemented in JS, so the dashboard needs no Claude session to compute state. See
246
- [`dashboard/README.md`](dashboard/README.md) for the architecture.
247
-
248
240
  ## Reading a project without an agent
249
241
 
250
242
  The two read-only halves of the pipeline, in the shell:
@@ -260,8 +252,8 @@ extension below renders.
260
252
 
261
253
  `doctor`'s exit code makes it a CI step as-is. Add `--porcelain` for one record per line with
262
254
  `U+001F` between fields (a spec title with a space in it never misaligns a column), or `--json`
263
- for the native document. Both reuse the dashboard's own readers, so the board and the CLI can
264
- never give two answers about one repo.
255
+ for the native document. Both read through `lib/`, so every consumer the CLI, a Francois
256
+ panel answers from one implementation and the repo never gets two verdicts.
265
257
 
266
258
  `--panel` — on `specs`, `doctor` and `metrics` — emits the payload shape a
267
259
  [Francois](https://github.com/antoine-gmnz/francois) extension panel expects. It is the one
@@ -418,7 +410,10 @@ See `profile/SCHEMA.md` for every field in `PIPELINE.md` and how the pipeline us
418
410
 
419
411
  ```
420
412
  package.json # npm package (cohorte) — semver source of truth
421
- bin/cli.js # the npm CLI: install / update / dashboard / specs / doctor / metrics / version (cross-platform, no deps)
413
+ bin/cli.js # the npm CLI: install / update / specs / doctor / metrics / version (cross-platform, no deps)
414
+ bin/report.js # the four renderings of `specs` / `doctor` (human, porcelain, json, panel)
415
+ lib/ # shared readers behind those verbs — doctor.js (the JS port of
416
+ # /cohorte-doctor) + runtime.js / versions.js / yaml.js. No deps.
422
417
  install.sh # script installer (fresh + --update) for npm-less setups — still needs Node
423
418
  install.ps1 # same installer for Windows PowerShell (fresh + -Update)
424
419
  core/ # copied verbatim into ~/.claude (global) or <project>/.claude (bundled)
@@ -432,7 +427,4 @@ profile/
432
427
  SCHEMA.md # field reference
433
428
  cohorte.config.template.yaml # seeds ~/.claude/cohorte.config.yaml (kanban)
434
429
  scripts/ # worktree-isolation templates + the shipped preflight/kanban scripts
435
- dashboard/ # local web cockpit (cohorte dashboard) — see dashboard/README.md
436
- server/ # dependency-free node runtime (serves the built app + JSON/stream API)
437
- app/ # Vite + React source (built to dashboard/dist/ at publish time)
438
430
  ```
package/bin/cli.js CHANGED
@@ -40,7 +40,6 @@ function usage(code) {
40
40
  Usage:
41
41
  cohorte install [target] [--global] [--runtime=a,b | --all-runtimes]
42
42
  cohorte update [target] [--global] [--runtime=a,b | --all-runtimes]
43
- cohorte dashboard [target] [--port=N] [--host=ADDR] [--open]
44
43
  cohorte metrics [target] [--days=N] [--since=ISO] [--runs] [--json]
45
44
  cohorte specs [target] [--porcelain | --json | --panel]
46
45
  cohorte doctor [target] [--porcelain | --json | --panel]
@@ -53,11 +52,6 @@ Commands:
53
52
  update Refresh the stack-agnostic core only. PIPELINE.md, rendered surface
54
53
  agents, gate-config.json, settings.json and your filled
55
54
  ~/.claude/cohorte.config.yaml are never touched.
56
- dashboard Serve a local web cockpit for the pipeline (freshness, /cohorte-doctor
57
- health, specs board, install/update actions). Binds 127.0.0.1:4317
58
- by default (loopback only — its actions execute code). --host=ADDR
59
- to expose (e.g. --host=0.0.0.0, prints a security warning). --open
60
- to launch the browser.
61
55
  metrics Cost and runtime per command, reconstructed from Claude Code's own
62
56
  transcripts (tokens, USD, wall/active time, subagent count). Reads
63
57
  ~/.claude/projects — nothing to enable, and it covers runs that
@@ -90,11 +84,6 @@ const args = process.argv.slice(2);
90
84
  let mode = null;
91
85
  let scope = 'project';
92
86
  let target = process.cwd();
93
- let port = parseInt(process.env.COHORTE_DASHBOARD_PORT, 10) || 4317;
94
- // Bind to loopback by default — the dashboard's action endpoints execute code (install/update/
95
- // reset/claude), so it must NOT be reachable from the network unless the user explicitly opts in.
96
- let host = process.env.COHORTE_DASHBOARD_HOST || '127.0.0.1';
97
- let openBrowser = false;
98
87
 
99
88
  // Flags that belong to `metrics` and are forwarded verbatim to the collector. They are
100
89
  // listed here rather than parsed, so the collector stays the single source of truth for
@@ -112,7 +101,7 @@ let format = 'human';
112
101
  let wantRuntimes = [];
113
102
 
114
103
  for (const a of args) {
115
- if (a === 'install' || a === 'update' || a === 'dashboard' || a === 'metrics'
104
+ if (a === 'install' || a === 'update' || a === 'metrics'
116
105
  || a === 'specs' || a === 'doctor') mode = a;
117
106
  else if (a === '--porcelain' || a === '--panel') format = a.slice(2);
118
107
  else if (isMetricsFlag(a)) { if (a === '--json') format = 'json'; metricsFlags.push(a); }
@@ -131,40 +120,18 @@ for (const a of args) {
131
120
  }
132
121
  else if (a === 'version' || a === '--version' || a === '-v') { console.log(VERSION); process.exit(0); }
133
122
  else if (a === '--global' || a === '-g') scope = 'global';
134
- else if (a.startsWith('--port=')) {
135
- // A bad value used to land as NaN, which http.listen() silently treats as
136
- // "any free port" — the banner then printed `localhost:NaN` and nothing worked.
137
- port = parseInt(a.slice(7), 10);
138
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
139
- console.error(`error: --port must be an integer 1-65535 (got "${a.slice(7)}")`);
140
- process.exit(2);
141
- }
142
- } else if (a.startsWith('--host=')) {
143
- host = a.slice(7).trim();
144
- if (!host) { console.error('error: --host= needs an address (e.g. --host=0.0.0.0)'); process.exit(2); }
145
- }
146
- else if (a === '--open') { openBrowser = true; }
147
123
  else if (a === 'help' || a === '--help' || a === '-h') usage(0);
148
124
  else if (a.startsWith('-')) { console.error(`error: unknown flag: ${a}`); usage(2); }
149
125
  else target = path.resolve(a);
150
126
  }
151
127
  if (!mode) usage(args.length ? 2 : 0);
152
128
 
153
- // --- dashboard: local web cockpit -------------------------------------------
154
- // Short-circuits before the install/update machinery (CommonJS wraps the module,
155
- // so a top-level return is valid here). Runtime is dependency-free node `http`.
156
- if (mode === 'dashboard') {
157
- const globalDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
158
- require('../dashboard/server')({ projectRoot: target, globalDir, port, host, openBrowser, pkgRoot, version: VERSION });
159
- return;
160
- }
161
-
162
129
  // --- specs / doctor: read-only reports on <target> ---------------------------
163
- // Both reuse the dashboard's own readers, so the board and the CLI can never drift
164
- // into two answers about the same repo. Nothing here writes or spawns anything.
130
+ // Both read through lib/, so every consumer of the pipeline's state the CLI, a
131
+ // Francois panel answers from one implementation. Nothing here writes or spawns.
165
132
  if (mode === 'specs' || mode === 'doctor') {
166
133
  const report = require('./report.js');
167
- const { state, scanSpecs } = require('../dashboard/server/doctor.js');
134
+ const { state, scanSpecs } = require('../lib/doctor.js');
168
135
  const globalDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
169
136
 
170
137
  if (mode === 'specs') {
@@ -253,8 +220,7 @@ function resolveTemplateConditionals(dir) {
253
220
  if (e.isDirectory()) { resolveTemplateConditionals(p); continue; }
254
221
  if (!p.endsWith('.md')) continue;
255
222
  const src = fs.readFileSync(p, 'utf8');
256
- if (!src.includes('cohorte:if')) continue;
257
- fs.writeFileSync(p, adapter.applyConditionals(src, runtime));
223
+ fs.writeFileSync(p, adapter.adaptInstructions(src, runtime));
258
224
  }
259
225
  }
260
226
 
@@ -342,8 +308,10 @@ function copyCore() {
342
308
  paths: {
343
309
  core: paths.core, commands: paths.commands,
344
310
  agents: paths.agents || path.join(paths.core, 'agents'),
345
- // Where the gate registration lives. The dashboard needs it to tell a registered hook
346
- // from a missing one without re-deriving each runtime's config layout itself.
311
+ surface_agents: runtime.id === 'codex' ? '.codex/agents' : undefined,
312
+ agent_ext: runtime.agent.ext,
313
+ // Where the gate registration lives. `cohorte doctor` needs it to tell a registered
314
+ // hook from a missing one without re-deriving each runtime's config layout itself.
347
315
  hooks_config: paths.hooks_config,
348
316
  state: adapter.stateDir(runtime), config: adapter.configPath(runtime),
349
317
  },
@@ -565,7 +533,7 @@ async function seedConfig() {
565
533
  //
566
534
  // CLAUDE_CONFIG_DIR moves the whole `~/.claude` tree (globalDir already follows it), but
567
535
  // adapter.configPath() speaks in literal `~/.claude/…` — expanding through HOME alone
568
- // seeded a file at a path no reader probes (kanban-move.sh and the dashboard follow
536
+ // seeded a file at a path no reader probes (kanban-move.sh follows
569
537
  // CLAUDE_CONFIG_DIR), missed a filled config there (re-seeding disabled defaults beside
570
538
  // it — the exact two-file fork this function's comments forbid), and printed a banner
571
539
  // path that did not exist. Re-root the claude-shaped path onto globalDir so the seed,
@@ -705,7 +673,7 @@ async function staleVersionNotice() {
705
673
  if (process.env.COHORTE_NO_VERSION_CHECK || process.env.CI) return;
706
674
  let latest = null;
707
675
  try {
708
- const v = require('../dashboard/server/versions.js');
676
+ const v = require('../lib/versions.js');
709
677
  latest = await v.latestNpm({ timeoutMs: 2500, fallback: false });
710
678
  if (!latest || v.cmpSemver(VERSION, latest) >= 0) return;
711
679
  } catch { return; }
@@ -767,6 +735,10 @@ if (runtime.id !== 'claude') {
767
735
  invoke: ${runtime.command.invoke.replace('<name>', 'cohorte-init-pipeline')}
768
736
  agents: ${agentsWhere}
769
737
  gate: ${hookState}`);
738
+ if (runtime.id === 'codex') {
739
+ console.log(' surface agents: .codex/agents/*.toml in each project (generated by init/reconcile)');
740
+ console.log(' keep your normal CODEX_HOME; no project launcher or auth symlink is needed');
741
+ }
770
742
  if ((runtime.exclude_commands || []).length) {
771
743
  const named = runtime.exclude_commands.map((c) => runtime.command.invoke.replace('<name>', c));
772
744
  console.log(` not installed here: ${named.join(', ')}${runtime.exclude_reason ? ` — ${runtime.exclude_reason}` : ''}`);
package/bin/report.js CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
  // Machine-readable reports for `cohorte specs` and `cohorte doctor`.
3
3
  //
4
- // Both commands read what the dashboard already computes (dashboard/server/doctor.js:
5
- // `scanSpecs` and `state`) and render it four ways:
4
+ // Both commands read what lib/doctor.js computes (`scanSpecs` and `state`) and
5
+ // render it four ways:
6
6
  //
7
7
  // default a human table on a terminal
8
8
  // --porcelain one record per line, fields separated by U+001F (the ASCII unit
@@ -205,7 +205,8 @@ function configPath(runtime) {
205
205
  function preamble(runtime, paths, projectRoot, { kind = 'command' } = {}) {
206
206
  const caps = runtime.capabilities || {};
207
207
  const core = displayPath(paths.core, projectRoot);
208
- const agentsDir = paths.agents ? displayPath(paths.agents, projectRoot) : `${core}/agents`;
208
+ const fixedAgentsDir = paths.agents ? displayPath(paths.agents, projectRoot) : `${core}/agents`;
209
+ const agentsDir = runtime.id === 'codex' ? '.codex/agents' : fixedAgentsDir;
209
210
  const L = [];
210
211
  L.push(`> **Runtime: ${runtime.label}.** Generated by the cohorte adapter — do not edit this file;`);
211
212
  L.push(`> edit \`core/${kind === 'agent' ? 'agents' : 'commands'}/\` in the cohorte source and re-install.`);
@@ -216,6 +217,11 @@ function preamble(runtime, paths, projectRoot, { kind = 'command' } = {}) {
216
217
  L.push(`> - \`<config>\` = \`${configPath(runtime)}\` — your user-level config (kanban boards, shared vault). One per human, never committed.`);
217
218
 
218
219
  L.push(`> - \`<agents>\` = \`${agentsDir}\` — real subagents. Dispatch: ${runtime.agent.dispatch}.`);
220
+ L.push(`> - \`<fixed-agents>\` = \`${fixedAgentsDir}\` — shipped review, release and profile-reader agents. Agent extension: \`${runtime.agent.ext}\`.`);
221
+ if (runtime.id === 'codex') {
222
+ L.push('> - Surface agents are always project-local `.codex/agents/*.toml`, even with a global core. Keep the normal user `CODEX_HOME`; no project launcher or authentication symlink is needed. Never overwrite another project\'s global agents.');
223
+ L.push('> - Agent files use TOML `name`, `description`, `developer_instructions`; optional `model` and `model_reasoning_effort` must be Codex-compatible. Omitted model settings inherit; never write Anthropic aliases. MCP registration lives in `.codex/config.toml`, not a standalone `.mcp.json`.');
224
+ }
219
225
 
220
226
  if (caps.hooks) {
221
227
  const cfg = displayPath(paths.hooks_config, projectRoot);
@@ -240,6 +246,14 @@ function preamble(runtime, paths, projectRoot, { kind = 'command' } = {}) {
240
246
  return L.join('\n') + '\n';
241
247
  }
242
248
 
249
+ // Apply to commands AND the instruction templates they consume. Filenames here refer to
250
+ // rendered agents, not to the Markdown source templates shipped inside the core.
251
+ function adaptInstructions(text, runtime) {
252
+ const resolved = applyConditionals(text, runtime);
253
+ if (runtime.id !== 'codex') return resolved;
254
+ return resolved.replace(/(<(?:agents|fixed-agents)>\/[^\s`]+)\.md\b/g, '$1.toml');
255
+ }
256
+
243
257
  // --- 3. surface encoding ------------------------------------------------------
244
258
 
245
259
  function parseFrontmatter(text) {
@@ -272,10 +286,11 @@ function substituteArgs(text, runtime) {
272
286
  return text.split('$ARGUMENTS').join(token);
273
287
  }
274
288
 
275
- // TOML basic-string escaping for the multi-line ''' form Gemini expects. A literal
276
- // ''' inside a prompt would close the string early, so it is the one sequence broken up.
289
+ // Prefer readable multi-line literal TOML; fall back to basic strings for delimiter collisions.
277
290
  function tomlMultiline(s) {
278
- return "'''\n" + s.replace(/'''/g, "''\\'") + "\n'''";
291
+ // Literal TOML strings cannot escape their delimiter. Use a basic string when the
292
+ // prompt itself contains triple apostrophes, preserving the content exactly.
293
+ return s.includes("'''") ? JSON.stringify(s) : "'''\n" + s + "\n'''";
279
294
  }
280
295
  function tomlBasic(s) {
281
296
  return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ') + '"';
@@ -287,7 +302,7 @@ function tomlBasic(s) {
287
302
  */
288
303
  function renderCommand({ source, name, runtime, paths, projectRoot }) {
289
304
  const { keys, body } = parseFrontmatter(source);
290
- let out = applyConditionals(body, runtime);
305
+ let out = adaptInstructions(body, runtime);
291
306
  out = preamble(runtime, paths, projectRoot) + '\n' + out.replace(/^\n+/, '');
292
307
  out = substituteArgs(out, runtime);
293
308
 
@@ -322,6 +337,9 @@ function renderCommand({ source, name, runtime, paths, projectRoot }) {
322
337
  function isReadOnly(keys) {
323
338
  const tools = (keys.find(([k]) => k === 'tools') || [])[1];
324
339
  if (!tools) return false;
340
+ // The implementer template is rendered before its surface tool list is known.
341
+ // A placeholder is not evidence that the eventual worker must be read-only.
342
+ if (tools.includes('<SURFACE_TOOLS>')) return false;
325
343
  return !/\b(Write|Edit|MultiEdit|Bash|NotebookEdit)\b/.test(tools);
326
344
  }
327
345
 
@@ -336,7 +354,7 @@ function isReadOnly(keys) {
336
354
  function renderAgent({ source, name, runtime, paths, projectRoot }) {
337
355
  const { keys, body } = parseFrontmatter(source);
338
356
  const out = preamble(runtime, paths, projectRoot, { kind: 'agent' }) + '\n'
339
- + applyConditionals(body, runtime).replace(/^\n+/, '');
357
+ + adaptInstructions(body, runtime).replace(/^\n+/, '');
340
358
  const spec = runtime.agent;
341
359
  const readonly = isReadOnly(keys);
342
360
 
@@ -345,7 +363,14 @@ function renderAgent({ source, name, runtime, paths, projectRoot }) {
345
363
  const lines = [`# cohorte — generated for ${runtime.label}. Do not edit; edit core/agents/${name}.md.`];
346
364
  lines.push(`name = ${tomlBasic(get('name') || name)}`);
347
365
  if (get('description')) lines.push(`description = ${tomlBasic(get('description'))}`);
348
- if (spec.frontmatter.includes('model') && get('model')) lines.push(`model = ${tomlBasic(get('model'))}`);
366
+ const model = get('model');
367
+ if (spec.frontmatter.includes('model') && model
368
+ && !['inherit', 'sonnet', 'haiku', 'opus'].includes(model) && !model.startsWith('<')) {
369
+ lines.push(`model = ${tomlBasic(model)}`);
370
+ }
371
+ if (spec.frontmatter.includes('model_reasoning_effort') && get('model_reasoning_effort')) {
372
+ lines.push(`model_reasoning_effort = ${tomlBasic(get('model_reasoning_effort'))}`);
373
+ }
349
374
  if (readonly && spec.readonly_key) {
350
375
  lines.push(`${spec.readonly_key} = ${tomlBasic(spec.readonly_value)}`);
351
376
  }
@@ -380,6 +405,7 @@ module.exports = {
380
405
  expandHome,
381
406
  displayPath,
382
407
  applyConditionals,
408
+ adaptInstructions,
383
409
  testCondition,
384
410
  parseFrontmatter,
385
411
  emitFrontmatter,
@@ -30,13 +30,24 @@ fix only with the human's go-ahead (or hand them the command).
30
30
  commands' step files are present — `templates/steps/init-pipeline/` non-empty (a router whose
31
31
  `templates/steps/<cmd>/` dir is missing is a partial/stale install ⇒
32
32
  re-run install/update). **Shipped scripts present and executable** in `<core>/pipeline/scripts/`:
33
- `kanban-move.sh`, `preflight.sh`,
34
- `new-feature.sh.template`, `remove-feature.sh.template` — ❌ any missing one.
33
+ `kanban-move.sh`, `preflight.sh`; the `new-feature.sh.template` and
34
+ `remove-feature.sh.template` sources must be readable, not executable — ❌ any missing one.
35
35
  Every caller chains these with `|| true`, so an absent script is a **silent**
36
36
  no-op (no kanban card moves, no error anywhere) — this check is the only thing
37
37
  that sees it. Also flag ❌ a `VERSION` **newer than** the other `pipeline/` files (compare mtimes):
38
38
  a version bumped without a full re-copy is a half-done update ⇒ re-run install/update.
39
39
  2. **Profile.** `PIPELINE.md` exists and its `yaml pipeline-profile` block parses. Every
40
+ <!-- cohorte:if runtime:codex -->
41
+ `surfaces[].agent` has a valid `.codex/agents/<agent>.toml` in this project, regardless of
42
+ core scope. Check `name`, `description`, `developer_instructions` and no unfilled placeholders.
43
+ Reconcile only this project's agents; never treat unrelated global agents as orphans.
44
+ Generic `review.toml`, `release.toml`, `profile-reader.toml` live under `<fixed-agents>/`.
45
+ The read-only generic agents must carry `sandbox_mode = "read-only"`.
46
+ Missing `model` means inheritance, not an error. Reject Anthropic aliases; compare explicit
47
+ Codex model pins with the profile when supplied. Claude `tools:` is not a Codex TOML field.
48
+ Flag project launchers that redefine `CODEX_HOME` just to discover local agents; native
49
+ project discovery needs no auth symlink. Do not delete global agents without checking ownership.
50
+ <!-- cohorte:else -->
40
51
  `surfaces[].agent` has its `<agents>/<agent>.md` and every agent file has its `surfaces[]`
41
52
  entry — **no orphans either way** (SCHEMA.md rule). Each rendered agent's frontmatter `tools`
42
53
  matches its surface's `tools` (incl. `DesignSync` iff `uses_design`, retrieval MCP tools iff
@@ -46,6 +57,7 @@ fix only with the human's go-ahead (or hand them the command).
46
57
  dispatch); ⚠️ any `inherit` with the note that it bills at the lead's tier. The generic agents
47
58
  (`review.md`, `release.md`, `profile-reader.md` in `<agents>/`) must
48
59
  each carry their `model:` line too (sonnet/haiku/haiku).
60
+ <!-- cohorte:endif -->
49
61
  <!-- cohorte:if runtime:claude -->
50
62
  **Command pins:** every mechanical command file
51
63
  (`build`, `review`, `fix`, `ship`, `audit`, `refactor`, `doctor`, `align-ds`,
@@ -62,8 +74,14 @@ fix only with the human's go-ahead (or hand them the command).
62
74
  double registration, it double-prompts — and its `command` points at a `gate.py` that exists.
63
75
  Check the **matcher** actually covers what it must: on Claude Code that means both `Bash` and
64
76
  `Task`, since the preflight phase gate keys off `Task` dispatches and a `Bash`-only matcher
65
- leaves it silently dead (the 1.3.0–1.3.1 regression). Prove the wiring end to end rather than
66
- trusting the file: `python3 <core>/hooks/gate.py --check "<a pattern from the ask list>"` must
77
+ leaves it silently dead (the 1.3.0–1.3.1 regression).
78
+ <!-- cohorte:if runtime:codex -->
79
+ Codex's matcher must cover `Bash` plus `spawn_agent`/`Agent`, and
80
+ `gate.py` must read `tool_input.agent_type`. With preflight enabled and no fresh stamp,
81
+ a synthetic `spawn_agent` review payload must be denied. Check client hook enablement/trust
82
+ separately; a direct script check is not proof the client invoked it.
83
+ <!-- cohorte:endif -->
84
+ Test the evaluator too: `python3 <core>/hooks/gate.py --check "<a pattern from the ask list>"` must
67
85
  return a non-`allow` verdict. If the Runtime preamble said this runtime has **no confirmation
68
86
  tier**, state it here too: every `ask` pattern behaves as a `deny`, which is safe but stricter
69
87
  than the profile reads, and a human who expects a prompt will read the refusal as a bug.
@@ -82,8 +100,14 @@ fix only with the human's go-ahead (or hand them the command).
82
100
  the committed copy lands in every clone and new worktree; the gate then blocks clean trees and
83
101
  greens unchecked ones. fix: `git rm --cached <state>/preflight.ok` + add it to `.gitignore`.
84
102
  4. **Retrieval** (if `retrieval.provider` ≠ `none`). Run the SCHEMA.md §Code retrieval health
103
+ <!-- cohorte:if runtime:codex -->
104
+ check: CLI resolvable from PATH, `[mcp_servers.<provider>]` in `.codex/config.toml`,
105
+ `.serena/` gitignored, and tools actually connected in this session. A standalone
106
+ `.mcp.json` is not Codex project registration.
107
+ <!-- cohorte:else -->
85
108
  check: CLI resolvable from PATH, `.mcp.json` entry present in PATH-proof launcher form,
86
109
  `.serena/` gitignored, server actually connects.
110
+ <!-- cohorte:endif -->
87
111
  5. **Design** (if `design.enabled`). `snapshot_dir` exists and is committed; `ui_kit_path` +
88
112
  `tokens_path` exist; if `provider: claude-design`, `DesignSync` responds (`list_files` on the `design_system_project`) and
89
113
  `design_system_project` is reachable. Recall: spec `design_files` are full
@@ -14,9 +14,8 @@ nothing more.
14
14
  fire a loop per worktree from here — is the 2.2.0 driver's grave: child sessions nobody
15
15
  supervises, stalling on prompts nobody sees. Each feature's build/review/loop runs in **its own
16
16
  worktree's session**, supervised like any other; the fleet plans, watches and rebases. State
17
- lives in `specs/reports/fleet.json` (the main checkout's gitignored buffer — not to be confused
18
- with the dashboard's multi-*project* fleet registry; this file is this repo's multi-*feature*
19
- flight plan).
17
+ lives in `specs/reports/fleet.json` (the main checkout's gitignored buffer — this repo's
18
+ multi-*feature* flight plan).
20
19
 
21
20
  > Read `PIPELINE.md` §`pipeline-profile` first: `surfaces` (paths — the overlap analysis keys
22
21
  > on them), `contract`, `vcs.default_branch`, and the `isolation` block. _Skip the re-read if
@@ -98,8 +98,8 @@ watch, rather than letting the job go red.
98
98
  **Move the card to Shipped — required, and verify it actually moved.** Run
99
99
  `<core>/pipeline/scripts/kanban-move.sh auto $ARGUMENTS shipped --pr <num>`, which **appends the PR
100
100
  number** so the line reads `- [ ] <title> #$ARGUMENTS — PR #<num>`. Take `<num>` from the PR URL
101
- (`…/pull/13` ⇒ `13`); **always pass it when a PR was created** (the `gh` path) — it is what the
102
- dashboard turns into a PR link. If only a compare URL was emitted (no PR yet), drop `--pr`.
101
+ (`…/pull/13` ⇒ `13`); **always pass it when a PR was created** (the `gh` path) — it is what a
102
+ board reader turns into a PR link. If only a compare URL was emitted (no PR yet), drop `--pr`.
103
103
 
104
104
  Then **read the script's own output**, which is the verification: `moved #$ARGUMENTS -> Shipped
105
105
  (PR #<num>)` means done, and a `kanban: <reason>` line means the mirror is off and says why. Both are
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  model: sonnet
3
- description: Refresh the pipeline core (global ~/.claude, or a repo's bundled .claude) to the latest published cohorte version, then reconcile this repo's generated files to it — /cohorte-init-pipeline stays one-time.
3
+ description: Refresh this runtime's global or project-local pipeline core, then reconcile the project's generated files — /cohorte-init-pipeline stays one-time.
4
4
  argument-hint: [path-to-local-checkout]
5
5
  ---
6
6
 
@@ -24,6 +24,17 @@ the human's choices — so `/cohorte-init-pipeline` never needs re-running for a
24
24
 
25
25
  ## 2. Run the update
26
26
 
27
+ <!-- cohorte:if runtime:codex -->
28
+ Preserve the existing install scope and explicitly select Codex:
29
+
30
+ - Local source checkout supplied: `node <path>/bin/cli.js update --runtime=codex [--global]`.
31
+ - Published release: `npm i -g cohorte@latest`, then `cohorte update --runtime=codex [--global]`.
32
+
33
+ Expand `[--global]` to `--global` only for a global core; otherwise omit it. Run from the target
34
+ project, or pass its path. Keep the user's normal `CODEX_HOME`. The global core and generic
35
+ agents may be shared, but reconciliation always writes surface agents in this project's
36
+ `.codex/agents/*.toml`.
37
+ <!-- cohorte:else -->
27
38
  - If `$ARGUMENTS` is a path to a local checkout of the pipeline repo (contains `core/` + `install.sh`),
28
39
  run from there — useful when iterating on the pipeline itself:
29
40
 
@@ -51,6 +62,7 @@ the human's choices — so `/cohorte-init-pipeline` never needs re-running for a
51
62
  ```
52
63
 
53
64
  (The piped installer clones the repo itself; `-s --` forwards the flags.)
65
+ <!-- cohorte:endif -->
54
66
 
55
67
  ## 3. Report old → new
56
68
 
@@ -77,7 +89,20 @@ or the **quiet command variants**: `test_quiet_cmd`/`lint_quiet_cmd` + `commands
77
89
  `lint_quiet`, proposing the detected bridled forms per §Output discipline; `gate.preflight` tops up
78
90
  silently at its defaults), re-render the surface agents from the current `implementer.template.md`
79
91
  (this refreshes each agent's **baked §Conventions slice** — required after any hand-edit of the
80
- profile's prose), additively patch `settings.json`/`gate-config.json` (including the `preflight`
92
+ profile's prose).
93
+ <!-- cohorte:if runtime:codex -->
94
+ Write surface agents as `.codex/agents/*.toml`, validate TOML, and preserve explicit Codex model
95
+ choices (legacy Anthropic aliases mean inheritance). Patch `<state>/gate-config.json` and
96
+ verify the selected scope's hook covers shell and `spawn_agent`/`Agent`; do not duplicate it.
97
+ Verify `<fixed-agents>/profile-reader.toml` and the other shipped generic agents. Workflows are
98
+ unavailable on Codex and their absence is expected. Reconcile MCP in `.codex/config.toml` using
99
+ SCHEMA.md §Code retrieval, preserving unrelated configuration and checking actual connectivity.
100
+ If a previous install wrote this project's agents globally, compare ownership/content before
101
+ moving them locally; never remove unrelated global agents or overwrite modified local copies.
102
+ Remove project-only `CODEX_HOME` workarounds only after verifying native discovery. Do not copy
103
+ authentication into the repository. Report what changed and anything still unverified.
104
+ <!-- cohorte:else -->
105
+ Additively patch `settings.json`/`gate-config.json` (including the `preflight`
81
106
  block and the workflow-agent `allow` entries from init step 5), and run any newly-added capability's
82
107
  wiring (e.g. Serena's project-scope `claude mcp add`). Verify the refreshed core actually carries
83
108
  `<core>/workflows/` + `agents/profile-reader.md` — missing means the update half-ran: re-run the
@@ -87,6 +112,7 @@ upgrading a bare `serena` entry to the PATH-proof launcher form, `.serena/` giti
87
112
  actually connected) and repair whatever fails — wiring that worked at
88
113
  init can rot (PATH changes, uninstalls, hand-edits). Report what was reconciled; if nothing was
89
114
  missing, say so. This is why `/cohorte-init-pipeline` never needs re-running for a core upgrade.
115
+ <!-- cohorte:endif -->
90
116
 
91
117
  Four of the §Reconcile steps matter specifically here:
92
118
 
@@ -124,11 +150,16 @@ Four of the §Reconcile steps matter specifically here:
124
150
 
125
151
  ## 4. Tell the human the follow-ups
126
152
 
127
- - **Restart / reload the Claude Code session** so it picks up updated commands, agents, and any
153
+ - **Restart / reload the coding-agent session** so it picks up updated commands, agents, and any
128
154
  newly-registered MCP server.
129
155
  - **Other repos using the global core:** their core is already fresh, but reconcile is per-repo — run
130
156
  `/cohorte-update-pipeline` inside each (it will skip the already-done core update and just reconcile).
157
+ <!-- cohorte:if runtime:codex -->
158
+ - **Commit** the reconciled `PIPELINE.md`, `.codex/agents/*.toml`, `.codex/config.toml` if added,
159
+ and versioned `<state>` files. Never commit auth or session state.
160
+ <!-- cohorte:else -->
131
161
  - **Commit** the reconciled files (`PIPELINE.md`, `.claude/`, `.mcp.json` if added) so teammates get them.
162
+ <!-- cohorte:endif -->
132
163
  - The kanban config is global and user-scoped
133
164
  (`<config>`) — never committed. The core update never touches it; only the
134
165
  reconcile above seeds the file and writes kanban board links (into that global file, not the repo).