cohorte 2.10.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.
- package/CHANGELOG.md +8 -0
- package/bin/cli.js +7 -2
- package/core/adapter/render.js +33 -7
- package/core/commands/cohorte-doctor.md +28 -4
- package/core/commands/cohorte-update-pipeline.md +34 -3
- package/core/hooks/gate.py +12 -4
- package/core/runtimes/codex.json +5 -3
- package/core/templates/steps/init-pipeline/04-write-render.md +32 -2
- package/lib/doctor.js +45 -15
- package/lib/runtime.js +5 -0
- package/package.json +1 -1
- package/profile/PIPELINE.template.md +10 -2
- package/profile/SCHEMA.md +38 -1
- package/scripts/test-adapter.mjs +70 -1
- package/scripts/test-gate.mjs +15 -0
- package/scripts/test-lib.mjs +47 -1
- package/scripts/validate-core.mjs +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ 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
|
+
|
|
10
18
|
## 2.10.0 — 2026-08-24
|
|
11
19
|
|
|
12
20
|
- **The dashboard is gone.** The local web cockpit — the `cohorte dashboard` verb, the
|
package/bin/cli.js
CHANGED
|
@@ -220,8 +220,7 @@ function resolveTemplateConditionals(dir) {
|
|
|
220
220
|
if (e.isDirectory()) { resolveTemplateConditionals(p); continue; }
|
|
221
221
|
if (!p.endsWith('.md')) continue;
|
|
222
222
|
const src = fs.readFileSync(p, 'utf8');
|
|
223
|
-
|
|
224
|
-
fs.writeFileSync(p, adapter.applyConditionals(src, runtime));
|
|
223
|
+
fs.writeFileSync(p, adapter.adaptInstructions(src, runtime));
|
|
225
224
|
}
|
|
226
225
|
}
|
|
227
226
|
|
|
@@ -309,6 +308,8 @@ function copyCore() {
|
|
|
309
308
|
paths: {
|
|
310
309
|
core: paths.core, commands: paths.commands,
|
|
311
310
|
agents: paths.agents || path.join(paths.core, 'agents'),
|
|
311
|
+
surface_agents: runtime.id === 'codex' ? '.codex/agents' : undefined,
|
|
312
|
+
agent_ext: runtime.agent.ext,
|
|
312
313
|
// Where the gate registration lives. `cohorte doctor` needs it to tell a registered
|
|
313
314
|
// hook from a missing one without re-deriving each runtime's config layout itself.
|
|
314
315
|
hooks_config: paths.hooks_config,
|
|
@@ -734,6 +735,10 @@ if (runtime.id !== 'claude') {
|
|
|
734
735
|
invoke: ${runtime.command.invoke.replace('<name>', 'cohorte-init-pipeline')}
|
|
735
736
|
agents: ${agentsWhere}
|
|
736
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
|
+
}
|
|
737
742
|
if ((runtime.exclude_commands || []).length) {
|
|
738
743
|
const named = runtime.exclude_commands.map((c) => runtime.command.invoke.replace('<name>', c));
|
|
739
744
|
console.log(` not installed here: ${named.join(', ')}${runtime.exclude_reason ? ` — ${runtime.exclude_reason}` : ''}`);
|
package/core/adapter/render.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
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 =
|
|
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
|
-
+
|
|
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
|
-
|
|
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
|
-
`
|
|
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).
|
|
66
|
-
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
model: sonnet
|
|
3
|
-
description: Refresh
|
|
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)
|
|
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
|
|
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).
|
package/core/hooks/gate.py
CHANGED
|
@@ -86,10 +86,12 @@ WS = re.compile(r"\s+")
|
|
|
86
86
|
# stat data. Must stay ≥ the filesystem's mtime granularity (1 s on ext4/HFS+); 5 s covers a
|
|
87
87
|
# clock that ticks backwards a little without costing anything on a quiet tree.
|
|
88
88
|
RACY_WINDOW_S = 5
|
|
89
|
+
HOOK_CWD = None
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
def project_root() -> str:
|
|
92
|
-
return os.environ.get("COHORTE_PROJECT_DIR") or os.environ.get("CLAUDE_PROJECT_DIR"
|
|
93
|
+
return (os.environ.get("COHORTE_PROJECT_DIR") or os.environ.get("CLAUDE_PROJECT_DIR")
|
|
94
|
+
or HOOK_CWD or ".")
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
# The generated per-project files (gate config + preflight stamp) live under `.claude/`
|
|
@@ -317,7 +319,8 @@ def check_preflight(payload: dict, cfg: dict) -> int:
|
|
|
317
319
|
# carrying `subagent_type`; Gemini exposes each subagent as a tool of the SAME NAME, so the
|
|
318
320
|
# dispatch arrives as `tool_name: review`. Accept both rather than gating only the shape
|
|
319
321
|
# one vendor happens to use — a phase gate that silently never fires is the 1.3.0 bug.
|
|
320
|
-
|
|
322
|
+
tool_input = payload.get("tool_input") or {}
|
|
323
|
+
subagent = tool_input.get("subagent_type") or tool_input.get("agent_type") or ""
|
|
321
324
|
if not subagent and payload.get("tool_name") in agents:
|
|
322
325
|
subagent = payload.get("tool_name")
|
|
323
326
|
if subagent not in agents:
|
|
@@ -493,7 +496,7 @@ def main() -> int:
|
|
|
493
496
|
if argv and argv[0] in ("--check", "--check-dispatch"):
|
|
494
497
|
return check_cli(argv)
|
|
495
498
|
|
|
496
|
-
global RUNTIME
|
|
499
|
+
global RUNTIME, HOOK_CWD
|
|
497
500
|
for i, a in enumerate(argv):
|
|
498
501
|
if a == "--runtime" and i + 1 < len(argv):
|
|
499
502
|
RUNTIME = argv[i + 1]
|
|
@@ -505,6 +508,11 @@ def main() -> int:
|
|
|
505
508
|
except Exception:
|
|
506
509
|
return 0 # malformed input → don't block
|
|
507
510
|
|
|
511
|
+
# Codex provides the project cwd in the envelope, not CLAUDE_PROJECT_DIR.
|
|
512
|
+
# A globally installed hook must inspect the calling project, not the process cwd.
|
|
513
|
+
if isinstance(payload.get("cwd"), str) and payload["cwd"]:
|
|
514
|
+
HOOK_CWD = payload["cwd"]
|
|
515
|
+
|
|
508
516
|
tool = payload.get("tool_name")
|
|
509
517
|
# Cursor's shell hook carries the command at the top level rather than in tool_input, and
|
|
510
518
|
# names no tool. Normalise once here so every check below stays runtime-agnostic.
|
|
@@ -515,7 +523,7 @@ def main() -> int:
|
|
|
515
523
|
cfg = load_config()
|
|
516
524
|
|
|
517
525
|
# A dispatch, in whichever shape this runtime sends it (see check_preflight).
|
|
518
|
-
if tool
|
|
526
|
+
if tool in ("Task", "spawn_agent", "Agent") or tool in ((cfg.get("preflight") or {}).get("agents") or ["review"]):
|
|
519
527
|
return check_preflight(payload, cfg)
|
|
520
528
|
# Bash is Claude's/Codex's name for the shell tool; Gemini calls it run_shell_command, and
|
|
521
529
|
# Cursor's beforeShellExecution was normalised to "Bash" above. Anything else is not a
|
package/core/runtimes/codex.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"Subagents are TOML, one file per agent, and the body lives in `developer_instructions`.",
|
|
15
15
|
"Hooks use the SAME PreToolUse envelope as Claude Code, with one difference that matters:",
|
|
16
16
|
"`permissionDecision: ask` is parsed but not honoured, so gate.py escalates ask to deny here.",
|
|
17
|
-
"
|
|
17
|
+
"Anthropic model aliases are dropped and default to inheritance. Explicit Codex model and model_reasoning_effort values are preserved. Surface agents always live in the current project's .codex/agents, even with a global core; CODEX_HOME does not change."
|
|
18
18
|
],
|
|
19
19
|
"scopes": {
|
|
20
20
|
"global": {
|
|
@@ -56,7 +56,9 @@
|
|
|
56
56
|
"body_key": "developer_instructions",
|
|
57
57
|
"frontmatter": [
|
|
58
58
|
"name",
|
|
59
|
-
"description"
|
|
59
|
+
"description",
|
|
60
|
+
"model",
|
|
61
|
+
"model_reasoning_effort"
|
|
60
62
|
],
|
|
61
63
|
"readonly_key": "sandbox_mode",
|
|
62
64
|
"readonly_value": "read-only",
|
|
@@ -66,7 +68,7 @@
|
|
|
66
68
|
"hook": {
|
|
67
69
|
"format": "claude",
|
|
68
70
|
"event": "PreToolUse",
|
|
69
|
-
"matcher": "Bash|shell",
|
|
71
|
+
"matcher": "Bash|shell|spawn_agent|Agent",
|
|
70
72
|
"supports_ask": false,
|
|
71
73
|
"config_shape": "json"
|
|
72
74
|
},
|
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
1. **Write `PIPELINE.md`** at the repo root (source: the installer's `pipeline/PIPELINE.template.md`).
|
|
6
6
|
2. **Wire it into `<memory>`:** if `<memory>` exists, ensure it references the profile (add a line
|
|
7
|
+
<!-- cohorte:if runtime:codex -->
|
|
8
|
+
near the top: `Read PIPELINE.md for the project profile and pipeline rules before pipeline work.`).
|
|
9
|
+
If absent, create `AGENTS.md` with that instruction and a one-paragraph project intro.
|
|
10
|
+
<!-- cohorte:else -->
|
|
7
11
|
near the top: `> Project profile & pipeline facts: **@PIPELINE.md**`). If not, create a minimal
|
|
8
12
|
`<memory>` with that reference + a one-paragraph project intro.
|
|
13
|
+
<!-- cohorte:endif -->
|
|
9
14
|
3. **Render one agent per surface** — for each surface, follow SCHEMA.md §"Rendering / reconciling a
|
|
10
15
|
surface agent" (steps 2–3): render `<agents>/<agent>.md` from the installer's
|
|
11
16
|
`pipeline/implementer.template.md`, substituting `<SURFACE_AGENT>`, `<SURFACE_LABEL>`, `<SURFACE_PATH>`,
|
|
@@ -14,12 +19,28 @@
|
|
|
14
19
|
PIPELINE.md you just wrote), and the surface-specific blocks
|
|
15
20
|
(`<SURFACE_EXTRA_NEVER>`, `<SURFACE_DESIGN_INPUT>`, `<SURFACE_TDD_STEP1>` — fill design-related ones
|
|
16
21
|
only when `uses_design`).
|
|
17
|
-
Leave the fixed agents as-is (generic, shipped by the installer
|
|
18
|
-
|
|
22
|
+
Leave the fixed agents as-is (generic, shipped by the installer under `<fixed-agents>/`).
|
|
23
|
+
<!-- cohorte:if runtime:codex -->
|
|
24
|
+
Write `.codex/agents/<agent>.toml` in this project even when the core is global. Preserve
|
|
25
|
+
TOML syntax when substituting the template; parse every result before dispatch. Each file
|
|
26
|
+
needs `name`, `description`, and `developer_instructions`. Omit `model` for `inherit` or
|
|
27
|
+
legacy `sonnet`/`haiku` profiles; preserve an explicitly selected Codex model and reasoning
|
|
28
|
+
effort. Do not add Claude `tools:` frontmatter. Codex discovers these project files natively:
|
|
29
|
+
leave the user's `CODEX_HOME` unchanged and do not create a dedicated launcher or auth symlink.
|
|
30
|
+
<!-- cohorte:endif -->
|
|
19
31
|
4. **Generate `<state>/gate-config.json`** from the `gate` block — copy all five keys verbatim:
|
|
20
32
|
`{"deny": [...], "ask": [...], "ask_on_default_branch": [...], "default_branch": "<vcs.default_branch>",
|
|
21
33
|
"preflight": {"enabled": <gate.preflight.enabled>, "agents": [...], "max_age_minutes": <n>}}`
|
|
22
34
|
(profile has no `preflight` block ⇒ omit the key — the hook then skips the phase gate).
|
|
35
|
+
<!-- cohorte:if runtime:codex -->
|
|
36
|
+
5. **Codex configuration.** Preserve `.codex/config.toml` and the user's configuration.
|
|
37
|
+
The installer registers `PreToolUse` in the selected scope's `hooks.json`; verify exactly
|
|
38
|
+
one Cohorte hook covering `Bash|shell|spawn_agent|Agent` with `--runtime codex`.
|
|
39
|
+
In global mode do not duplicate it locally. In project mode use `.codex/hooks.json`.
|
|
40
|
+
Check the hook is enabled/trusted in this client; a file alone does not prove enforcement.
|
|
41
|
+
Never write `.claude/settings.json` or Claude `Bash(...)` permission rules for Codex.
|
|
42
|
+
`ask` rules become `deny` in this hook; explain this stricter behavior.
|
|
43
|
+
<!-- cohorte:else -->
|
|
23
44
|
<!-- cohorte:if hooks -->
|
|
24
45
|
5. **Write `.claude/settings.json`** permissions (`ask`/`deny` lists mirroring the gate, **plus an
|
|
25
46
|
`allow` list of the project's read-only / verification commands** so agents don't stall on
|
|
@@ -53,7 +74,15 @@
|
|
|
53
74
|
deny/ask patterns live, so fill it from the profile exactly and do not skip it. If this repo is
|
|
54
75
|
also driven from Claude Code, that install's hook reads the same file; nothing to duplicate.
|
|
55
76
|
<!-- cohorte:endif -->
|
|
77
|
+
<!-- cohorte:endif -->
|
|
56
78
|
6. **Wire the retrieval provider** (skip if `retrieval.provider: none`):
|
|
79
|
+
<!-- cohorte:if runtime:codex -->
|
|
80
|
+
Follow SCHEMA.md §Code retrieval's Codex procedure: merge `[mcp_servers.serena]` into the
|
|
81
|
+
project's `.codex/config.toml`, preserve other settings, gitignore `.serena/`, then check
|
|
82
|
+
CLI availability, registration and actual session connectivity. Do not write a standalone
|
|
83
|
+
`.mcp.json` or run `claude mcp add`. Missing connectivity requires a restart/diagnosis,
|
|
84
|
+
not a claim that registration succeeded end to end.
|
|
85
|
+
<!-- cohorte:else -->
|
|
57
86
|
- **serena:** if the `serena` CLI is missing, have the human install it (`uv tool install -p 3.13
|
|
58
87
|
serena-agent`) — or set the provider to `none` if they decline, and say `/cohorte-update-pipeline` can wire
|
|
59
88
|
it later. If the binary exists (e.g. `~/.local/bin/serena`) but `command -v serena` fails,
|
|
@@ -74,6 +103,7 @@
|
|
|
74
103
|
big changes.
|
|
75
104
|
- Either way the rendered agents already carry the provider's MCP tools in their `tools:` list
|
|
76
105
|
(step 3 / SCHEMA §Rendering); remind the human the new MCP server appears after a session restart.
|
|
106
|
+
<!-- cohorte:endif -->
|
|
77
107
|
7. **Render the isolation scripts** (if `isolation.enabled`) from the installer's
|
|
78
108
|
`pipeline/scripts/*.template` to this repo's `scripts/new-feature.sh` and `scripts/remove-feature.sh`,
|
|
79
109
|
substituting the `__TOKENS__` (project
|
package/lib/doctor.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const { layouts,
|
|
10
|
+
const { layouts, stateDirs } = require('./runtime.js');
|
|
11
11
|
const { parseProfileBlock } = require('./yaml');
|
|
12
12
|
const { versions } = require('./versions');
|
|
13
13
|
|
|
@@ -54,6 +54,15 @@ function mk(id, label, status, detail, fix) {
|
|
|
54
54
|
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function acrossRuntimes(all, check) {
|
|
58
|
+
const distinct = all.filter((l, i) => all.findIndex(other => other.id === l.id) === i);
|
|
59
|
+
const results = (distinct.length ? distinct : [null]).map(check);
|
|
60
|
+
if (results.length === 1) return results[0];
|
|
61
|
+
const rank = { skip: 0, ok: 1, warn: 2, bad: 3 };
|
|
62
|
+
const worst = results.reduce((a, b) => rank[b.status] > rank[a.status] ? b : a);
|
|
63
|
+
return { ...worst, detail: results.map((r, i) => `${distinct[i].label}: ${r.detail}`).join(' · ') };
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
// --- individual checks -------------------------------------------------------
|
|
58
67
|
|
|
59
68
|
function checkCore(v) {
|
|
@@ -99,19 +108,20 @@ function checkAgents(profile, projectRoot, layout) {
|
|
|
99
108
|
if (!layout) {
|
|
100
109
|
return mk('agents', 'Surfaces ↔ agents', 'skip', 'no core installed — nothing renders agents yet');
|
|
101
110
|
}
|
|
102
|
-
const agentsDir = layout.agents;
|
|
111
|
+
const agentsDir = layout.surfaceAgents || layout.agents;
|
|
112
|
+
const ext = layout.agentExt || (layout.id === 'codex' ? '.toml' : '.md');
|
|
103
113
|
const surfaceAgents = profile.surfaces.map(s => s.agent).filter(Boolean);
|
|
104
114
|
|
|
105
|
-
const missing = surfaceAgents.filter(a => !exists(path.join(agentsDir, `${a}
|
|
115
|
+
const missing = surfaceAgents.filter(a => !exists(path.join(agentsDir, `${a}${ext}`)));
|
|
106
116
|
|
|
107
117
|
let files = [];
|
|
108
|
-
try { files = fs.readdirSync(agentsDir).filter(f => f.endsWith(
|
|
118
|
+
try { files = fs.readdirSync(agentsDir).filter(f => f.endsWith(ext)).map(f => f.slice(0, -ext.length)); }
|
|
109
119
|
catch { /* dir absent → handled by `missing` */ }
|
|
110
120
|
// Orphan detection only makes sense against a PROJECT agents dir. A global install's
|
|
111
121
|
// agents dir (~/.claude/agents) is the user's shared Claude Code space — their personal
|
|
112
122
|
// agents and other cohorte projects' surface agents live there legitimately, and
|
|
113
123
|
// flagging them sent humans deleting files that were not this project's to judge.
|
|
114
|
-
const orphans = layout.scope === 'global'
|
|
124
|
+
const orphans = layout.scope === 'global' && layout.id !== 'codex'
|
|
115
125
|
? []
|
|
116
126
|
: files.filter(f => !FIXED_AGENTS.has(f) && !surfaceAgents.includes(f));
|
|
117
127
|
|
|
@@ -276,7 +286,7 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
276
286
|
|
|
277
287
|
const problems = [];
|
|
278
288
|
const okLines = [];
|
|
279
|
-
for (const l of hosts) {
|
|
289
|
+
for (const l of hosts.filter((h, i) => hosts.findIndex(other => other.id === h.id) === i)) {
|
|
280
290
|
const event = (l.id === 'cursor') ? 'beforeShellExecution'
|
|
281
291
|
: (l.id === 'gemini') ? 'BeforeTool' : 'PreToolUse';
|
|
282
292
|
// A Claude registration serves the project from EITHER scope: bundled repos get it from
|
|
@@ -287,7 +297,10 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
287
297
|
? (installMode === 'global'
|
|
288
298
|
? [path.join(globalDir, 'settings.json'), path.join(projectRoot, '.claude', 'settings.json')]
|
|
289
299
|
: [path.join(projectRoot, '.claude', 'settings.json'), path.join(globalDir, 'settings.json')])
|
|
290
|
-
:
|
|
300
|
+
: l.id === 'codex'
|
|
301
|
+
? [...new Set([...hosts.filter(h => h.id === 'codex').map(h => h.hooksConfig),
|
|
302
|
+
path.join(projectRoot, '.codex', 'hooks.json')])]
|
|
303
|
+
: [l.hooksConfig];
|
|
291
304
|
const found = paths.map(p2 => ({ path: p2, regs: gateRegs(p2, event) })).filter(f => f.regs.length);
|
|
292
305
|
if (!found.length) {
|
|
293
306
|
problems.push(`${l.label}: not registered (${event})`);
|
|
@@ -298,11 +311,16 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
298
311
|
problems.push(`${l.label}: registered ${total}× — it will double-prompt`);
|
|
299
312
|
continue;
|
|
300
313
|
}
|
|
301
|
-
//
|
|
302
|
-
// a `Task` tool — a Bash-only matcher there leaves the preflight phase gate dead.
|
|
314
|
+
// Validate the regex against the host's real tool names, including dispatch aliases.
|
|
303
315
|
const matcher = String(found[0].regs[0].matcher || '');
|
|
304
|
-
|
|
305
|
-
|
|
316
|
+
const matches = name => {
|
|
317
|
+
if (!matcher || matcher === '*') return true;
|
|
318
|
+
try { return new RegExp(matcher).test(name); } catch { return false; }
|
|
319
|
+
};
|
|
320
|
+
const dispatch = l.id === 'codex' ? ['spawn_agent', 'Agent'] : ['Task'];
|
|
321
|
+
if (['claude', 'codex'].includes(l.id)
|
|
322
|
+
&& (!matches('Bash') || !dispatch.some(matches))) {
|
|
323
|
+
problems.push(`${l.label}: matcher "${matcher}" must cover Bash and ${dispatch.join('/')}`);
|
|
306
324
|
continue;
|
|
307
325
|
}
|
|
308
326
|
okLines.push(`${l.label} (${event}${matcher ? `, ${matcher}` : ''})`);
|
|
@@ -319,11 +337,24 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
319
337
|
return mk('hooks', 'Gate hook', 'ok', `registered once for ${okLines.join(', ')}${tail}`);
|
|
320
338
|
}
|
|
321
339
|
|
|
322
|
-
function checkRetrieval(profile, projectRoot) {
|
|
340
|
+
function checkRetrieval(profile, projectRoot, layout) {
|
|
323
341
|
const provider = profile && profile.retrieval && profile.retrieval.provider;
|
|
324
342
|
if (!provider || provider === 'none' || String(provider).startsWith('<')) {
|
|
325
343
|
return mk('retrieval', 'Code retrieval', 'skip', 'provider: none');
|
|
326
344
|
}
|
|
345
|
+
if (layout?.id === 'codex') {
|
|
346
|
+
const file = '.codex/config.toml';
|
|
347
|
+
const config = readText(path.join(projectRoot, file)) || '';
|
|
348
|
+
// Static registration check only, as for the JSON path below. Connectivity requires
|
|
349
|
+
// a live session. Anchor to table declarations so prose/comments cannot count as wired.
|
|
350
|
+
const tables = [...config.matchAll(/^\s*\[mcp_servers\.([^\]\n]+)\]\s*(?:#.*)?$/gm)]
|
|
351
|
+
.map(m => m[1].trim().replace(/^["']|["']$/g, ''));
|
|
352
|
+
const wired = tables.includes(String(provider));
|
|
353
|
+
return mk('retrieval', 'Code retrieval', wired ? 'ok' : 'warn', wired
|
|
354
|
+
? `provider: ${provider} — table present in ${file} (validity/connectivity need in-session verification)`
|
|
355
|
+
: `profile says provider: ${provider} but ${file} has no matching server table`,
|
|
356
|
+
wired ? undefined : '$cohorte-update-pipeline (merge the project MCP table)');
|
|
357
|
+
}
|
|
327
358
|
// The profile alone isn't proof the provider was ever wired: /cohorte-init-pipeline
|
|
328
359
|
// registers it at project scope in .mcp.json. Verify the entry exists on disk;
|
|
329
360
|
// live connectivity still needs a session — note it, don't fake green.
|
|
@@ -461,18 +492,17 @@ async function state({ projectRoot, globalDir, cliVersion }) {
|
|
|
461
492
|
// this rather than assuming `.claude/` — on a Cursor-only repo that assumption reported the
|
|
462
493
|
// core, the agents, the artifacts and the hook as all broken, and every one of them was fine.
|
|
463
494
|
const all = layouts({ projectRoot, globalDir });
|
|
464
|
-
const main = primary(all);
|
|
465
495
|
const stateAbs = stateDirs(all, projectRoot);
|
|
466
496
|
const stateRels = stateAbs.map(rel(projectRoot));
|
|
467
497
|
|
|
468
498
|
const checks = [
|
|
469
499
|
checkCore(v),
|
|
470
500
|
checkProfile(profile, pipelineMd != null),
|
|
471
|
-
checkAgents(profile, projectRoot,
|
|
501
|
+
acrossRuntimes(all, layout => checkAgents(profile, projectRoot, layout)),
|
|
472
502
|
checkGate(profile, projectRoot, stateAbs),
|
|
473
503
|
checkLocalArtifacts(projectRoot, stateRels),
|
|
474
504
|
checkHooks(projectRoot, globalDir, v.installMode, all),
|
|
475
|
-
checkRetrieval(profile, projectRoot),
|
|
505
|
+
acrossRuntimes(all, layout => checkRetrieval(profile, projectRoot, layout)),
|
|
476
506
|
checkDesign(profile, projectRoot),
|
|
477
507
|
checkIsolation(profile, projectRoot),
|
|
478
508
|
checkWorkflows(projectRoot, globalDir, v.installMode, all),
|
package/lib/runtime.js
CHANGED
|
@@ -94,6 +94,11 @@ function layouts({ projectRoot, globalDir }) {
|
|
|
94
94
|
scope: rec.scope || scope,
|
|
95
95
|
core: dir,
|
|
96
96
|
agents: abs(p.agents, path.join(dir, 'agents')),
|
|
97
|
+
// A global Codex core must never bind surface agents to its installing project.
|
|
98
|
+
// Also repair discovery for registries written before this distinction existed.
|
|
99
|
+
surfaceAgents: id === 'codex' ? path.join(projectRoot, '.codex', 'agents')
|
|
100
|
+
: abs(p.surface_agents || p.agents, path.join(dir, 'agents')),
|
|
101
|
+
agentExt: p.agent_ext || (id === 'codex' ? '.toml' : '.md'),
|
|
97
102
|
commands: abs(p.commands, path.join(dir, 'commands')),
|
|
98
103
|
state: abs(null, p.state || stateDirFor(id)),
|
|
99
104
|
// Pre-2.2.0 cores carry no registry, so nothing records where the hook is registered.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.1",
|
|
4
4
|
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code, Codex CLI, Cursor, Gemini CLI and OpenCode — install the core, run /cohorte-init-pipeline, and it adapts to your project's stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cohorte": "bin/cli.js"
|
|
@@ -37,7 +37,7 @@ repo:
|
|
|
37
37
|
# wired. serena = live LSP symbol navigation (default, no index to maintain);
|
|
38
38
|
# graphify = persistent tree-sitter knowledge graph over code + docs (needs an
|
|
39
39
|
# index step + rescans); none = agents fall back to Grep/Glob/Read.
|
|
40
|
-
# Registered by /cohorte-init-pipeline
|
|
40
|
+
# Registered by /cohorte-init-pipeline in the runtime's project MCP configuration.
|
|
41
41
|
retrieval:
|
|
42
42
|
provider: serena # serena | graphify | none
|
|
43
43
|
|
|
@@ -53,12 +53,16 @@ surfaces:
|
|
|
53
53
|
- key: backend # short id, used as agent name + scope
|
|
54
54
|
path: apps/api # the ONLY tree this surface's agent may touch
|
|
55
55
|
label: backend (AdonisJS)
|
|
56
|
-
agent: backend # rendered
|
|
56
|
+
agent: backend # rendered in the runtime's project agents dir
|
|
57
57
|
tools: [Read, Write, Edit, Bash, Grep, Glob, mcp__serena] # mcp__<provider> mirrors retrieval.provider
|
|
58
|
+
<!-- cohorte:if runtime:codex -->
|
|
59
|
+
model: inherit # omit the TOML model pin; explicit Codex models also allowed
|
|
60
|
+
<!-- cohorte:else -->
|
|
58
61
|
model: sonnet # frontmatter model tier: sonnet | haiku | inherit
|
|
59
62
|
# sonnet = default (applies the frozen contract — cheap
|
|
60
63
|
# vs the Opus lead); haiku = purely mechanical scaffolding;
|
|
61
64
|
# inherit = only for surfaces with real design decisions
|
|
65
|
+
<!-- cohorte:endif -->
|
|
62
66
|
test_cmd: pnpm --filter api test
|
|
63
67
|
# Bridled variants — what agents actually RUN (dot reporter / failures-only /
|
|
64
68
|
# --quiet), so a green run costs lines, not pages. "" ⇒ callers fall back to
|
|
@@ -76,10 +80,14 @@ surfaces:
|
|
|
76
80
|
label: frontend (React/TanStack)
|
|
77
81
|
agent: frontend
|
|
78
82
|
tools: [Read, Write, Edit, Bash, Grep, Glob, DesignSync, mcp__serena]
|
|
83
|
+
<!-- cohorte:if runtime:codex -->
|
|
84
|
+
model: inherit # use the session model unless explicitly configured
|
|
85
|
+
<!-- cohorte:else -->
|
|
79
86
|
model: sonnet # default even for design surfaces — designs + contract are
|
|
80
87
|
# frozen inputs the agent applies; `inherit` (bills at the
|
|
81
88
|
# lead's tier, often Opus) ONLY if this surface must make
|
|
82
89
|
# novel design decisions
|
|
90
|
+
<!-- cohorte:endif -->
|
|
83
91
|
test_cmd: pnpm --filter web test
|
|
84
92
|
test_quiet_cmd: pnpm --filter web test --reporter=dot
|
|
85
93
|
lint_cmd: pnpm --filter web lint
|
package/profile/SCHEMA.md
CHANGED
|
@@ -100,6 +100,28 @@ follow is provider-agnostic: _"prefer the retrieval MCP tools over Grep/Glob + w
|
|
|
100
100
|
|
|
101
101
|
**Wiring (done by `/cohorte-init-pipeline`, or `/cohorte-update-pipeline` retroactively):**
|
|
102
102
|
|
|
103
|
+
<!-- cohorte:if runtime:codex -->
|
|
104
|
+
For `serena`, install its CLI if missing (`uv tool install -p 3.13 serena-agent`), then merge
|
|
105
|
+
this project-scoped table into `.codex/config.toml`, preserving all existing settings:
|
|
106
|
+
|
|
107
|
+
```toml
|
|
108
|
+
[mcp_servers.serena]
|
|
109
|
+
command = "sh"
|
|
110
|
+
args = ["-c", 'exec "$(command -v serena || echo "$HOME/.local/bin/serena")" start-mcp-server --context codex --project-from-cwd --open-web-dashboard False']
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
On Windows without `sh`, use `command = "serena"` and the server arguments directly; ensure
|
|
114
|
+
the CLI is on PATH. Gitignore `.serena/`. Keep `CODEX_HOME` at its normal user location;
|
|
115
|
+
the project table is discovered natively once the project is trusted.
|
|
116
|
+
For `graphify`, install its CLI and build/update the graph according to the provider's instructions;
|
|
117
|
+
verify any required MCP registration in `.codex/config.toml` rather than `.mcp.json`.
|
|
118
|
+
|
|
119
|
+
**Health check:** verify (1) `command -v serena`, (2) the `[mcp_servers.serena]` table,
|
|
120
|
+
(3) `.serena/` ignored, (4) actual tools in the session. `codex mcp list` inspects registration,
|
|
121
|
+
but is not proof of a live connection; restart the session when needed and report that limitation.
|
|
122
|
+
Codex agents inherit MCP configuration; do not write a Claude `tools:` allowlist.
|
|
123
|
+
Teammates receive `.codex/config.toml` and need the provider CLI installed and the project trusted.
|
|
124
|
+
<!-- cohorte:else -->
|
|
103
125
|
- `serena` — requires the `serena` CLI (`uv tool install -p 3.13 serena-agent`). For day-to-day CLI
|
|
104
126
|
use it should also be on PATH (`uv tool update-shell`; uv installs to `~/.local/bin`). Register at
|
|
105
127
|
**project scope** so the registration is committed and portable (`--project-from-cwd` resolves the
|
|
@@ -143,6 +165,7 @@ Report each check's result; never report Serena "wired" on registration alone.
|
|
|
143
165
|
Teammates cloning the repo get the committed `.mcp.json` and only need the provider CLI installed
|
|
144
166
|
and on PATH — if either is missing, the MCP server fails to start and agents silently fall back to
|
|
145
167
|
Grep/Read; the health check above is the diagnostic.
|
|
168
|
+
<!-- cohorte:endif -->
|
|
146
169
|
|
|
147
170
|
## Specialization — when to split one surface into more agents
|
|
148
171
|
|
|
@@ -412,6 +435,11 @@ this exact procedure so a surface is always defined the same way. To add surface
|
|
|
412
435
|
scaffolding; `inherit` only when the surface makes real design decisions worth the lead's model),
|
|
413
436
|
the five `*_cmd`s (derive from the surface's `package.json` / workspace
|
|
414
437
|
filter, mirroring a sibling surface), and `uses_design`.
|
|
438
|
+
<!-- cohorte:if runtime:codex -->
|
|
439
|
+
**Codex model policy:** use `model: inherit` by default, or a model explicitly selected for
|
|
440
|
+
Codex. Legacy `sonnet`/`haiku` values are not executable Codex pins: omit them in the rendered
|
|
441
|
+
agent and report inheritance. Preserve explicit Codex `model`/`model_reasoning_effort` choices.
|
|
442
|
+
<!-- cohorte:endif -->
|
|
415
443
|
2. **Render the agent file** `<agents>/<agent>.md` from `<core>/pipeline/implementer.template.md`
|
|
416
444
|
— the template is already rendered for this runtime, so only the placeholders are yours to fill —
|
|
417
445
|
substituting `<SURFACE_AGENT>`, `<SURFACE_LABEL>`,
|
|
@@ -440,6 +468,16 @@ this exact procedure so a surface is always defined the same way. To add surface
|
|
|
440
468
|
says `none`): `DesignSync get_file(<projectId>, <file>)` for each link in the slot and translate
|
|
441
469
|
each into the code design system (`@/components/ui/*`, `cn()` + CVA), mobile-first — never ad-hoc
|
|
442
470
|
CSS. Then:"_
|
|
471
|
+
<!-- cohorte:if runtime:codex -->
|
|
472
|
+
**Codex destination and format:** always write `.codex/agents/<agent>.toml` in the current
|
|
473
|
+
project, including with a global core. The source template has a `.md` filename but contains
|
|
474
|
+
TOML for this runtime. Validate TOML after substitutions; keep `name`, `description`, and
|
|
475
|
+
`developer_instructions`. Do not add Claude `tools:`/`model: sonnet` frontmatter.
|
|
476
|
+
Keep generic agents under `<fixed-agents>/`; never write surface agents there in global mode.
|
|
477
|
+
No `CODEX_HOME` override, auth symlink or per-project launcher is needed. When migrating an
|
|
478
|
+
old global surface agent, compare ownership/content with this project's profile before
|
|
479
|
+
removing its old copy; do not overwrite local customizations or touch other projects' agents.
|
|
480
|
+
<!-- cohorte:endif -->
|
|
443
481
|
3. **Add a §Conventions + §Testing stanza** for `S` in `PIPELINE.md` (mirror a sibling surface; keep it
|
|
444
482
|
rule-shaped). If `S` is a shared-code surface, its convention is "single owner of shared X; slices
|
|
445
483
|
consume, never redefine."
|
|
@@ -695,4 +733,3 @@ added vs. moved vs. already-correct.
|
|
|
695
733
|
`<obsidian.vault_path>/<folder>/Tasks.md` with the `kanban-plugin: board` front-matter, one `## <heading>`
|
|
696
734
|
per configured column in pipeline order, and the closing `%% kanban:settings %%` block
|
|
697
735
|
(`{"kanban-plugin":"board","list-collapse":[false,…]}` with one `false` per column).
|
|
698
|
-
|
package/scripts/test-adapter.mjs
CHANGED
|
@@ -269,7 +269,76 @@ for (const id of RUNTIMES) {
|
|
|
269
269
|
const text = readFileSync(step, "utf8");
|
|
270
270
|
check(`${id}: templates carry no unresolved marker`, !/cohorte:(if|else|endif)/.test(text));
|
|
271
271
|
check(`${id}: the settings/hook step matches this runtime`,
|
|
272
|
-
text.includes("Write `.claude/settings.json`") === rt.capabilities.hooks);
|
|
272
|
+
text.includes("Write `.claude/settings.json`") === (rt.capabilities.hooks && id !== "codex"));
|
|
273
|
+
if (id === "codex") {
|
|
274
|
+
check("codex: init generates native project agents and MCP config",
|
|
275
|
+
text.includes('.codex/agents/<agent>.toml') && text.includes('.codex/config.toml')
|
|
276
|
+
&& !text.includes('render `<agents>/<agent>.md`') && !text.includes('`review.md`'));
|
|
277
|
+
const schema = readFileSync(join(p.core, 'pipeline', 'SCHEMA.md'), 'utf8');
|
|
278
|
+
check("codex: reconciliation preserves TOML destinations and Codex MCP context",
|
|
279
|
+
schema.includes('`<agents>/<agent>.toml`') && schema.includes('--context codex')
|
|
280
|
+
&& !schema.includes('claude mcp add'));
|
|
281
|
+
const doctor = readFileSync(join(cmdDir, 'cohorte-doctor', 'SKILL.md'), 'utf8');
|
|
282
|
+
check("codex: doctor understands inherited models and TOML",
|
|
283
|
+
doctor.includes('Missing `model` means inheritance, not an error')
|
|
284
|
+
&& !doctor.includes('sonnet/haiku/haiku'));
|
|
285
|
+
const update = readFileSync(join(cmdDir, 'cohorte-update-pipeline', 'SKILL.md'), 'utf8');
|
|
286
|
+
check("codex: update selects its runtime and does not demand Claude workflows",
|
|
287
|
+
update.includes('update --runtime=codex') && !update.includes('claude mcp add')
|
|
288
|
+
&& !update.includes('`<core>/workflows/` + `agents/profile-reader.md`'));
|
|
289
|
+
const template = readFileSync(join(p.core, 'pipeline', 'PIPELINE.template.md'), 'utf8');
|
|
290
|
+
check("codex: new profiles default to inheritance",
|
|
291
|
+
/model: inherit/.test(template) && !/model: sonnet/.test(template));
|
|
292
|
+
const implementer = readFileSync(join(p.core, 'pipeline', 'implementer.template.md'), 'utf8');
|
|
293
|
+
check('codex: implementers are not accidentally pinned to a read-only sandbox',
|
|
294
|
+
!/^sandbox_mode = "read-only"/m.test(implementer));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
group("codex — global core, project-local surfaces, native TOML");
|
|
300
|
+
{
|
|
301
|
+
const env = { ...process.env, HOME: home, CLAUDE_CONFIG_DIR: '' };
|
|
302
|
+
const r = spawnSync(process.execPath,
|
|
303
|
+
[join(root, 'bin/cli.js'), 'install', '--global', '--runtime=codex'],
|
|
304
|
+
{ cwd: proj, env, encoding: 'utf8' });
|
|
305
|
+
check('global Codex install succeeds', r.status === 0, r.stderr);
|
|
306
|
+
const init = readFileSync(join(home, '.agents/skills/cohorte-init-pipeline/SKILL.md'), 'utf8');
|
|
307
|
+
check('global skill resolves surfaces relative to whichever project invokes it',
|
|
308
|
+
init.includes('`<agents>` = `.codex/agents`') && !init.includes(proj));
|
|
309
|
+
check('generic agents retain their global destination',
|
|
310
|
+
existsSync(join(home, '.codex/agents/review.toml'))
|
|
311
|
+
&& init.includes('`<fixed-agents>` = `~/.codex/agents`'));
|
|
312
|
+
const registry = JSON.parse(readFileSync(join(home, '.cohorte/codex/pipeline/runtimes.json'), 'utf8'));
|
|
313
|
+
check('global registry does not pin surface agents to the installer cwd',
|
|
314
|
+
registry.codex.paths.surface_agents === '.codex/agents'
|
|
315
|
+
&& registry.codex.paths.agent_ext === '.toml');
|
|
316
|
+
const hook = JSON.parse(readFileSync(join(home, '.codex/hooks.json'), 'utf8')).hooks.PreToolUse[0];
|
|
317
|
+
check('installed Codex hook matches real shell and subagent calls',
|
|
318
|
+
['Bash', 'spawn_agent', 'Agent'].every(n => new RegExp(hook.matcher).test(n)));
|
|
319
|
+
const rt = adapter.loadRuntime('codex');
|
|
320
|
+
const paths = adapter.resolvePaths(rt, 'global', proj);
|
|
321
|
+
const render = model => adapter.renderAgent({
|
|
322
|
+
source: `---\nname: example\ndescription: Test agent\nmodel: ${model}\nmodel_reasoning_effort: high\n---\nHandle literal ''' in instructions.`,
|
|
323
|
+
name: 'example', runtime: rt, paths, projectRoot: proj,
|
|
324
|
+
}).content;
|
|
325
|
+
const explicit = render('gpt-5.6-terra');
|
|
326
|
+
check('explicit Codex model and reasoning choices survive rendering',
|
|
327
|
+
explicit.includes('model = "gpt-5.6-terra"') && explicit.includes('model_reasoning_effort = "high"'));
|
|
328
|
+
check('inherit and legacy Anthropic aliases never become executable pins',
|
|
329
|
+
['inherit', 'sonnet', 'haiku', 'opus'].every(m => !/^model =/m.test(render(m))));
|
|
330
|
+
const python = [process.env.COHORTE_TEST_PYTHON, 'python3', 'python'].filter(Boolean)
|
|
331
|
+
.find(p => spawnSync(p, ['-c', 'import tomllib']).status === 0);
|
|
332
|
+
if (!python) { check('Python 3.11+ available to validate emitted TOML', false); }
|
|
333
|
+
else {
|
|
334
|
+
const parsed = spawnSync(python, ['-c', 'import sys,tomllib; print(tomllib.loads(sys.stdin.read())["developer_instructions"])'],
|
|
335
|
+
{ input: explicit, encoding: 'utf8' });
|
|
336
|
+
check('instructions containing triple apostrophes remain valid TOML',
|
|
337
|
+
parsed.status === 0 && parsed.stdout.includes("literal '''"), parsed.stderr);
|
|
338
|
+
const native = spawnSync(python, ['-c',
|
|
339
|
+
'import pathlib,sys,tomllib; [tomllib.loads(p.read_text()) for p in pathlib.Path(sys.argv[1]).glob("*.toml")]',
|
|
340
|
+
join(home, '.codex/agents')], { encoding: 'utf8' });
|
|
341
|
+
check('all installed generic agents parse as TOML', native.status === 0, native.stderr);
|
|
273
342
|
}
|
|
274
343
|
}
|
|
275
344
|
|
package/scripts/test-gate.mjs
CHANGED
|
@@ -395,6 +395,21 @@ console.log("gate.py — runtime dialects");
|
|
|
395
395
|
cx.json?.hookSpecificOutput?.permissionDecision === "deny");
|
|
396
396
|
check("…and the reason says why it was refused rather than queried",
|
|
397
397
|
/no confirmation tier/.test(cx.json?.hookSpecificOutput?.permissionDecisionReason || ""));
|
|
398
|
+
for (const tool_name of ['spawn_agent', 'Agent']) {
|
|
399
|
+
const dispatch = raw({ tool_name, tool_input: { agent_type: 'review' }, cwd: d }, '--runtime', 'codex');
|
|
400
|
+
check(`codex: ${tool_name} with agent_type is denied without a preflight stamp`,
|
|
401
|
+
dispatch.json?.hookSpecificOutput?.permissionDecision === 'deny'
|
|
402
|
+
&& /preflight/i.test(dispatch.json?.hookSpecificOutput?.permissionDecisionReason || ''));
|
|
403
|
+
check(`codex: ${tool_name} for an ungated implementer passes`,
|
|
404
|
+
raw({ tool_name, tool_input: { agent_type: 'backend' }, cwd: d }, '--runtime', 'codex').json === null);
|
|
405
|
+
}
|
|
406
|
+
const nativeCwd = spawnSync(python, [GATE, '--runtime', 'codex'], {
|
|
407
|
+
input: JSON.stringify({ tool_name: 'spawn_agent', tool_input: { agent_type: 'review' }, cwd: d }),
|
|
408
|
+
cwd: root, encoding: 'utf8',
|
|
409
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: '', COHORTE_PROJECT_DIR: '' },
|
|
410
|
+
});
|
|
411
|
+
check('codex: payload cwd locates project config without Claude environment variables',
|
|
412
|
+
JSON.parse(nativeCwd.stdout || '{}').hookSpecificOutput?.permissionDecision === 'deny');
|
|
398
413
|
|
|
399
414
|
// Cursor sends the command at the top level and names no tool.
|
|
400
415
|
const cu = raw({ hook_event_name: "beforeShellExecution", command: "git push", cwd: d },
|
package/scripts/test-lib.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const require = createRequire(import.meta.url);
|
|
|
18
18
|
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
19
19
|
const { parse, parseProfileBlock } = require(join(root, "lib/yaml.js"));
|
|
20
20
|
const { state, scanSpecs } = require(join(root, "lib/doctor.js"));
|
|
21
|
+
const adapter = require(join(root, 'core/adapter/render.js'));
|
|
21
22
|
|
|
22
23
|
let failures = 0;
|
|
23
24
|
const check = (name, cond, detail = "") => {
|
|
@@ -30,6 +31,11 @@ const eq = (name, got, want) =>
|
|
|
30
31
|
const tmps = [];
|
|
31
32
|
const scratch = () => { const d = mkdtempSync(join(tmpdir(), "cohorte-lib-")); tmps.push(d); return d; };
|
|
32
33
|
const write = (p, s) => { mkdirSync(join(p, ".."), { recursive: true }); writeFileSync(p, s); };
|
|
34
|
+
// Runtime discovery must not pick up Cohorte installations from the developer's real home.
|
|
35
|
+
const os = require('node:os');
|
|
36
|
+
const originalHomedir = os.homedir;
|
|
37
|
+
os.homedir = () => isolatedHome;
|
|
38
|
+
const isolatedHome = scratch();
|
|
33
39
|
|
|
34
40
|
// ── yaml.js ──────────────────────────────────────────────────────────────────
|
|
35
41
|
console.log("yaml.js — the profile parser");
|
|
@@ -55,7 +61,7 @@ console.log("yaml.js — the profile parser");
|
|
|
55
61
|
|
|
56
62
|
// The real thing: the shipped template must round-trip.
|
|
57
63
|
const tpl = readFileSync(join(root, "profile/PIPELINE.template.md"), "utf8");
|
|
58
|
-
const p = parseProfileBlock(tpl);
|
|
64
|
+
const p = parseProfileBlock(adapter.adaptInstructions(tpl, adapter.loadRuntime('claude')));
|
|
59
65
|
check("the shipped PIPELINE.template.md parses", !!p);
|
|
60
66
|
eq("…surfaces are a list of 2", (p.surfaces || []).length, 2);
|
|
61
67
|
eq("…surface tools survive as an array", p.surfaces[0].tools.length, 7);
|
|
@@ -263,6 +269,46 @@ console.log("doctor.js — a non-Claude runtime layout");
|
|
|
263
269
|
s.summary.bad === 0 && s.summary.warn === 0, JSON.stringify(s.summary));
|
|
264
270
|
}
|
|
265
271
|
|
|
272
|
+
// Codex's global core supplies fixed agents; each consuming repo owns its surfaces.
|
|
273
|
+
{
|
|
274
|
+
const d = scratch();
|
|
275
|
+
const g = join(d, 'global');
|
|
276
|
+
write(join(g, 'pipeline/VERSION'), '9.9.9\n');
|
|
277
|
+
write(join(g, 'pipeline/runtimes.json'), JSON.stringify({ codex: {
|
|
278
|
+
label: 'Codex', scope: 'global', capabilities: { subagents: true, hooks: true, workflows: false },
|
|
279
|
+
paths: { core: g, agents: join(g, 'agents'), hooks_config: join(g, 'hooks.json'), state: '.cohorte' },
|
|
280
|
+
} }));
|
|
281
|
+
// Legacy registry intentionally has no surface_agents or agent_ext fields.
|
|
282
|
+
write(join(g, 'agents/unrelated.toml'), 'name = "unrelated"\n');
|
|
283
|
+
const hook = matcher => write(join(g, 'hooks.json'), JSON.stringify({ hooks: {
|
|
284
|
+
PreToolUse: [{ matcher, hooks: [{ command: `python3 ${g}/hooks/gate.py --runtime codex` }] }],
|
|
285
|
+
} }));
|
|
286
|
+
hook('Bash|shell');
|
|
287
|
+
const a = join(d, 'project-a'), b = join(d, 'project-b');
|
|
288
|
+
for (const repo of [a, b]) {
|
|
289
|
+
write(join(repo, 'PIPELINE.md'), '```yaml pipeline-profile\nname: example\nsurfaces:\n - key: api\n agent: api\nretrieval:\n provider: serena\n```\n');
|
|
290
|
+
write(join(repo, '.codex/agents/api.toml'), 'name = "api"\ndescription = "API"\ndeveloper_instructions = "Implement"\n');
|
|
291
|
+
write(join(repo, '.codex/config.toml'), '[mcp_servers.serena]\ncommand = "serena"\n');
|
|
292
|
+
}
|
|
293
|
+
const inspect = repo => state({ projectRoot: repo, globalDir: g, cliVersion: '9.9.9' });
|
|
294
|
+
const status = (s, id) => s.checks.find(c => c.id === id)?.status;
|
|
295
|
+
let s = await inspect(a);
|
|
296
|
+
check('Codex doctor accepts project TOML agents with a legacy global registry', status(s, 'agents') === 'ok');
|
|
297
|
+
check('Codex doctor rejects a shell-only preflight hook', status(s, 'hooks') === 'warn');
|
|
298
|
+
check('Codex doctor finds native project MCP configuration', status(s, 'retrieval') === 'ok');
|
|
299
|
+
hook('Bash|spawn_agent');
|
|
300
|
+
s = await inspect(b);
|
|
301
|
+
check('a second project resolves its own agents without a launcher', status(s, 'agents') === 'ok');
|
|
302
|
+
check('Codex doctor accepts the native dispatch matcher', status(s, 'hooks') === 'ok');
|
|
303
|
+
write(join(b, '.codex/agents/stale.toml'), 'name = "stale"\n');
|
|
304
|
+
s = await inspect(b);
|
|
305
|
+
check('Codex doctor detects local orphans even with a global core', status(s, 'agents') === 'warn');
|
|
306
|
+
write(join(a, '.codex/config.toml'), '# [mcp_servers.serena]\n');
|
|
307
|
+
write(join(a, '.mcp.json'), JSON.stringify({ mcpServers: { serena: { command: 'serena' } } }));
|
|
308
|
+
s = await inspect(a);
|
|
309
|
+
check('a comment or Claude MCP config does not count as Codex registration', status(s, 'retrieval') === 'warn');
|
|
310
|
+
}
|
|
311
|
+
|
|
266
312
|
// ── runtime.js — stale absolute registry paths (a cloned/moved bundled core) ─
|
|
267
313
|
// runtimes.json records install-time ABSOLUTE paths. A committed core cloned to
|
|
268
314
|
// another machine (or a checkout simply moved) still carries the original paths;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
const { applyConditionals, loadRuntime } = createRequire(import.meta.url)('../core/adapter/render.js');
|
|
8
10
|
|
|
9
11
|
// fileURLToPath, not .pathname — on Windows the latter yields "/C:/…", which
|
|
10
12
|
// join() then resolves against the cwd drive ("C:\C:\…") and every read ENOENTs.
|
|
@@ -121,11 +123,14 @@ for (const p of ["profile/PIPELINE.template.md", "profile/SCHEMA.md",
|
|
|
121
123
|
"profile/cohorte.config.template.yaml"])
|
|
122
124
|
if (!existsSync(join(root, p))) fail(p, "missing");
|
|
123
125
|
|
|
124
|
-
const tpl = read("profile/PIPELINE.template.md");
|
|
126
|
+
const tpl = applyConditionals(read("profile/PIPELINE.template.md"), loadRuntime('claude'));
|
|
125
127
|
if (/^\s*model:\s*inherit\b/m.test(tpl))
|
|
126
128
|
fail("profile/PIPELINE.template.md",
|
|
127
129
|
"a surfaces[] example pins `model: inherit` — examples must default to sonnet " +
|
|
128
130
|
"(inherit bills at the lead session's model)");
|
|
131
|
+
const codexTpl = applyConditionals(read('profile/PIPELINE.template.md'), loadRuntime('codex'));
|
|
132
|
+
if (/^\s*model:\s*(sonnet|haiku|opus)\b/m.test(codexTpl))
|
|
133
|
+
fail('profile/PIPELINE.template.md', 'Codex examples must not pin Anthropic model aliases');
|
|
129
134
|
|
|
130
135
|
// ── init-pipeline router steps ──────────────────────────────────────────────
|
|
131
136
|
const steps = join(root, "core/templates/steps/init-pipeline");
|