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.
- package/CHANGELOG.md +33 -0
- package/README.md +37 -45
- package/bin/cli.js +15 -43
- package/bin/report.js +2 -2
- package/core/adapter/render.js +33 -7
- package/core/commands/cohorte-doctor.md +28 -4
- package/core/commands/cohorte-fleet.md +2 -3
- package/core/commands/cohorte-ship.md +2 -2
- package/core/commands/cohorte-update-pipeline.md +34 -3
- package/core/hooks/gate.py +13 -5
- package/core/runtimes/codex.json +5 -3
- package/core/templates/steps/init-pipeline/04-write-render.md +32 -2
- package/core/workflows/review.js +1 -1
- package/{dashboard/server → lib}/doctor.js +48 -17
- package/{dashboard/server → lib}/runtime.js +6 -1
- package/{dashboard/server → lib}/versions.js +2 -2
- package/package.json +3 -8
- package/profile/PIPELINE.template.md +10 -2
- package/profile/SCHEMA.md +43 -6
- package/scripts/test-adapter.mjs +70 -1
- package/scripts/test-gate.mjs +15 -0
- package/scripts/{test-dashboard.mjs → test-lib.mjs} +58 -229
- package/scripts/validate-core.mjs +9 -19
- package/dashboard/README.md +0 -71
- package/dashboard/dist/apple-touch-icon-180.png +0 -0
- package/dashboard/dist/assets/index-BZ_LQlEj.css +0 -1
- package/dashboard/dist/assets/index-vtFc6Gyc.js +0 -43
- package/dashboard/dist/favicon-16.png +0 -0
- package/dashboard/dist/favicon-32.png +0 -0
- package/dashboard/dist/favicon-48.png +0 -0
- package/dashboard/dist/icon-192.png +0 -0
- package/dashboard/dist/icon-512.png +0 -0
- package/dashboard/dist/index.html +0 -16
- package/dashboard/server/fleet.js +0 -133
- package/dashboard/server/index.js +0 -408
- package/dashboard/server/kanban.js +0 -169
- package/dashboard/server/metrics.js +0 -120
- package/dashboard/server/usage.js +0 -61
- /package/{dashboard/server → lib}/yaml.js +0 -0
package/core/hooks/gate.py
CHANGED
|
@@ -23,7 +23,7 @@ and subagents spawned by the Workflow runtime alike. Workflow subagents run in
|
|
|
23
23
|
acceptEdits regardless of the session's permission mode (their Write/Edit calls
|
|
24
24
|
are auto-approved), but acceptEdits does NOT auto-approve Bash or Task, so this
|
|
25
25
|
gate still sees and can block them. In bypassPermissions (headless `claude -p`,
|
|
26
|
-
|
|
26
|
+
unattended workflow runs) there is no human to answer a prompt, so every `ask` match is
|
|
27
27
|
escalated to a hard deny with the reason attached — a clear refusal beats a
|
|
28
28
|
prompt that can never be answered.
|
|
29
29
|
|
|
@@ -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/core/workflows/review.js
CHANGED
|
@@ -395,7 +395,7 @@ const staging = await agent(
|
|
|
395
395
|
: 'the blocking list is empty, so <FP> is the empty string "":\n') +
|
|
396
396
|
`${verdictJson}\n` +
|
|
397
397
|
// Per-surface verdicts (not the merged one stamped on every row — one BLOCK used to
|
|
398
|
-
// mark ALL surfaces failed
|
|
398
|
+
// mark ALL surfaces failed), and dead reviewers logged as "dead"
|
|
399
399
|
// per SCHEMA.md §Dead agents — an incomplete batch is the batch worth recording.
|
|
400
400
|
`3. Append one line to $(dirname "$(git rev-parse --git-common-dir)")/.claude/pipeline-metrics.jsonl: ` +
|
|
401
401
|
`{"ts":"<ISO now>","feature":"${feature}","phase":"review","seconds":0,"tokens":${Math.max(0, spentNow() - spentStart)},"surfaces":{${
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// Programmatic port of the /cohorte-doctor checks (core/commands/doctor.md),
|
|
2
|
+
// Programmatic port of the /cohorte-doctor checks (core/commands/cohorte-doctor.md), so
|
|
3
|
+
// `cohorte doctor` can report without a coding agent in the loop.
|
|
3
4
|
// Read-only: inspects files only. Checks that need a live process (MCP connectivity,
|
|
4
5
|
// git worktree state, DesignSync) are reported as `skip` with a note — the node server
|
|
5
6
|
// can't run them, and honest "not checked here" beats a false green.
|
|
6
7
|
|
|
7
8
|
const fs = require('fs');
|
|
8
9
|
const path = require('path');
|
|
9
|
-
const { layouts,
|
|
10
|
+
const { layouts, stateDirs } = require('./runtime.js');
|
|
10
11
|
const { parseProfileBlock } = require('./yaml');
|
|
11
12
|
const { versions } = require('./versions');
|
|
12
13
|
|
|
@@ -53,6 +54,15 @@ function mk(id, label, status, detail, fix) {
|
|
|
53
54
|
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
|
|
54
55
|
}
|
|
55
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
|
+
|
|
56
66
|
// --- individual checks -------------------------------------------------------
|
|
57
67
|
|
|
58
68
|
function checkCore(v) {
|
|
@@ -98,19 +108,20 @@ function checkAgents(profile, projectRoot, layout) {
|
|
|
98
108
|
if (!layout) {
|
|
99
109
|
return mk('agents', 'Surfaces ↔ agents', 'skip', 'no core installed — nothing renders agents yet');
|
|
100
110
|
}
|
|
101
|
-
const agentsDir = layout.agents;
|
|
111
|
+
const agentsDir = layout.surfaceAgents || layout.agents;
|
|
112
|
+
const ext = layout.agentExt || (layout.id === 'codex' ? '.toml' : '.md');
|
|
102
113
|
const surfaceAgents = profile.surfaces.map(s => s.agent).filter(Boolean);
|
|
103
114
|
|
|
104
|
-
const missing = surfaceAgents.filter(a => !exists(path.join(agentsDir, `${a}
|
|
115
|
+
const missing = surfaceAgents.filter(a => !exists(path.join(agentsDir, `${a}${ext}`)));
|
|
105
116
|
|
|
106
117
|
let files = [];
|
|
107
|
-
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)); }
|
|
108
119
|
catch { /* dir absent → handled by `missing` */ }
|
|
109
120
|
// Orphan detection only makes sense against a PROJECT agents dir. A global install's
|
|
110
121
|
// agents dir (~/.claude/agents) is the user's shared Claude Code space — their personal
|
|
111
122
|
// agents and other cohorte projects' surface agents live there legitimately, and
|
|
112
123
|
// flagging them sent humans deleting files that were not this project's to judge.
|
|
113
|
-
const orphans = layout.scope === 'global'
|
|
124
|
+
const orphans = layout.scope === 'global' && layout.id !== 'codex'
|
|
114
125
|
? []
|
|
115
126
|
: files.filter(f => !FIXED_AGENTS.has(f) && !surfaceAgents.includes(f));
|
|
116
127
|
|
|
@@ -247,7 +258,7 @@ function gateRegs(settingsPath, event) {
|
|
|
247
258
|
|
|
248
259
|
function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
249
260
|
// A registration can exist without a discoverable core — a repo whose core lives in a global
|
|
250
|
-
// dir the
|
|
261
|
+
// dir the caller was not pointed at, or one predating `runtimes.json`. Falling through to
|
|
251
262
|
// "no runtime installed" there would hide a real double-registration, so assume the Claude
|
|
252
263
|
// layout: before the adapter it was the only one, and it is the only one whose hook can be
|
|
253
264
|
// registered outside its own core dir.
|
|
@@ -275,7 +286,7 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
275
286
|
|
|
276
287
|
const problems = [];
|
|
277
288
|
const okLines = [];
|
|
278
|
-
for (const l of hosts) {
|
|
289
|
+
for (const l of hosts.filter((h, i) => hosts.findIndex(other => other.id === h.id) === i)) {
|
|
279
290
|
const event = (l.id === 'cursor') ? 'beforeShellExecution'
|
|
280
291
|
: (l.id === 'gemini') ? 'BeforeTool' : 'PreToolUse';
|
|
281
292
|
// A Claude registration serves the project from EITHER scope: bundled repos get it from
|
|
@@ -286,7 +297,10 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
286
297
|
? (installMode === 'global'
|
|
287
298
|
? [path.join(globalDir, 'settings.json'), path.join(projectRoot, '.claude', 'settings.json')]
|
|
288
299
|
: [path.join(projectRoot, '.claude', 'settings.json'), path.join(globalDir, 'settings.json')])
|
|
289
|
-
:
|
|
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];
|
|
290
304
|
const found = paths.map(p2 => ({ path: p2, regs: gateRegs(p2, event) })).filter(f => f.regs.length);
|
|
291
305
|
if (!found.length) {
|
|
292
306
|
problems.push(`${l.label}: not registered (${event})`);
|
|
@@ -297,11 +311,16 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
297
311
|
problems.push(`${l.label}: registered ${total}× — it will double-prompt`);
|
|
298
312
|
continue;
|
|
299
313
|
}
|
|
300
|
-
//
|
|
301
|
-
// 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.
|
|
302
315
|
const matcher = String(found[0].regs[0].matcher || '');
|
|
303
|
-
|
|
304
|
-
|
|
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('/')}`);
|
|
305
324
|
continue;
|
|
306
325
|
}
|
|
307
326
|
okLines.push(`${l.label} (${event}${matcher ? `, ${matcher}` : ''})`);
|
|
@@ -318,11 +337,24 @@ function checkHooks(projectRoot, globalDir, installMode, all) {
|
|
|
318
337
|
return mk('hooks', 'Gate hook', 'ok', `registered once for ${okLines.join(', ')}${tail}`);
|
|
319
338
|
}
|
|
320
339
|
|
|
321
|
-
function checkRetrieval(profile, projectRoot) {
|
|
340
|
+
function checkRetrieval(profile, projectRoot, layout) {
|
|
322
341
|
const provider = profile && profile.retrieval && profile.retrieval.provider;
|
|
323
342
|
if (!provider || provider === 'none' || String(provider).startsWith('<')) {
|
|
324
343
|
return mk('retrieval', 'Code retrieval', 'skip', 'provider: none');
|
|
325
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
|
+
}
|
|
326
358
|
// The profile alone isn't proof the provider was ever wired: /cohorte-init-pipeline
|
|
327
359
|
// registers it at project scope in .mcp.json. Verify the entry exists on disk;
|
|
328
360
|
// live connectivity still needs a session — note it, don't fake green.
|
|
@@ -460,18 +492,17 @@ async function state({ projectRoot, globalDir, cliVersion }) {
|
|
|
460
492
|
// this rather than assuming `.claude/` — on a Cursor-only repo that assumption reported the
|
|
461
493
|
// core, the agents, the artifacts and the hook as all broken, and every one of them was fine.
|
|
462
494
|
const all = layouts({ projectRoot, globalDir });
|
|
463
|
-
const main = primary(all);
|
|
464
495
|
const stateAbs = stateDirs(all, projectRoot);
|
|
465
496
|
const stateRels = stateAbs.map(rel(projectRoot));
|
|
466
497
|
|
|
467
498
|
const checks = [
|
|
468
499
|
checkCore(v),
|
|
469
500
|
checkProfile(profile, pipelineMd != null),
|
|
470
|
-
checkAgents(profile, projectRoot,
|
|
501
|
+
acrossRuntimes(all, layout => checkAgents(profile, projectRoot, layout)),
|
|
471
502
|
checkGate(profile, projectRoot, stateAbs),
|
|
472
503
|
checkLocalArtifacts(projectRoot, stateRels),
|
|
473
504
|
checkHooks(projectRoot, globalDir, v.installMode, all),
|
|
474
|
-
checkRetrieval(profile, projectRoot),
|
|
505
|
+
acrossRuntimes(all, layout => checkRetrieval(profile, projectRoot, layout)),
|
|
475
506
|
checkDesign(profile, projectRoot),
|
|
476
507
|
checkIsolation(profile, projectRoot),
|
|
477
508
|
checkWorkflows(projectRoot, globalDir, v.installMode, all),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// Which coding agent(s) this project is wired for, and where each one keeps its files.
|
|
3
3
|
//
|
|
4
|
-
// Before 2.2.0 every path
|
|
4
|
+
// Before 2.2.0 every path here was `.claude/…`. That is now one layout of five:
|
|
5
5
|
// a repo driven from Cursor keeps its core in `.cohorte/cursor/`, its agents in
|
|
6
6
|
// `.cursor/agents/` and its gate registration in `.cursor/hooks.json`. Checking the Claude
|
|
7
7
|
// paths there reports a perfectly healthy install as broken — "no core", "no rendered agent",
|
|
@@ -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.
|
|
@@ -30,12 +30,12 @@ function pointerAt(projectRoot) {
|
|
|
30
30
|
|
|
31
31
|
// Latest published version — registry fetch first, `npm view` as a fallback (it uses the
|
|
32
32
|
// user's configured registry/proxy, which works where a raw fetch may be blocked). Cached
|
|
33
|
-
// briefly so
|
|
33
|
+
// briefly so repeated calls don't hammer the network. null only if both fail.
|
|
34
34
|
let _cache = { value: null, at: 0 };
|
|
35
35
|
const CACHE_MS = 5 * 60 * 1000;
|
|
36
36
|
// Failures are cached too, briefly. Without this an offline machine paid the FULL
|
|
37
37
|
// 5s fetch timeout + 8s `npm view` timeout on every call — and /api/fleet calls
|
|
38
|
-
// this once per tracked project, so
|
|
38
|
+
// this once per tracked project, so a batch of probes never finished.
|
|
39
39
|
const FAIL_CACHE_MS = 60 * 1000;
|
|
40
40
|
|
|
41
41
|
async function fetchRegistry(timeoutMs = 5000) {
|
package/package.json
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "2.
|
|
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"
|
|
7
7
|
},
|
|
8
|
-
"scripts": {
|
|
9
|
-
"build:dashboard": "npm --prefix dashboard/app ci && npm --prefix dashboard/app run build",
|
|
10
|
-
"prepack": "npm run build:dashboard"
|
|
11
|
-
},
|
|
12
8
|
"files": [
|
|
13
9
|
"bin",
|
|
10
|
+
"lib",
|
|
14
11
|
"core",
|
|
15
12
|
"profile",
|
|
16
13
|
"scripts",
|
|
17
14
|
"!scripts/new-feature.sh",
|
|
18
15
|
"!scripts/remove-feature.sh",
|
|
16
|
+
"!scripts/demo",
|
|
19
17
|
"!core/hooks/__pycache__",
|
|
20
18
|
"!**/*.pyc",
|
|
21
|
-
"dashboard/server",
|
|
22
|
-
"dashboard/dist",
|
|
23
|
-
"dashboard/README.md",
|
|
24
19
|
"install.sh",
|
|
25
20
|
"install.ps1",
|
|
26
21
|
"CHANGELOG.md"
|
|
@@ -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
|
|
|
@@ -176,7 +199,7 @@ the **main checkout's** `<state>/pipeline-metrics.jsonl` (gitignored) — one JS
|
|
|
176
199
|
and `/cohorte-fix`.
|
|
177
200
|
**`surfaces` keys are surface keys, nothing else** — run-level facts go in their own top-level
|
|
178
201
|
fields. Anything put inside `surfaces` is read
|
|
179
|
-
as a surface:
|
|
202
|
+
as a surface: `cohorte doctor` renders it as a row in the per-surface table and scores a non-`ok`
|
|
180
203
|
value as that surface failing. Always the main checkout, never the feature worktree (which dies at teardown while
|
|
181
204
|
metrics must accumulate across features) — resolve from anywhere with
|
|
182
205
|
`$(dirname "$(git rev-parse --git-common-dir)")/<state>/pipeline-metrics.jsonl`. Read it before
|
|
@@ -189,7 +212,7 @@ what's SLOW. Tokens are recorded only where they can be read honestly: the **wor
|
|
|
189
212
|
(`loop.js`, `review.js`) stamp an approximate `tokens` field per batch from the runtime's own
|
|
190
213
|
counter (`budget.spent()` deltas), and the loop's return carries a per-round breakdown in its
|
|
191
214
|
`history`. The **conversational** commands still record none — a lead cannot reliably read a
|
|
192
|
-
subagent's token count, and a guessed number is worse than a missing one.
|
|
215
|
+
subagent's token count, and a guessed number is worse than a missing one. `cohorte metrics` sums
|
|
193
216
|
whatever is stamped (a token-less line aggregates as 0, rendered as absent, never as "free").
|
|
194
217
|
For exact spend, use Claude Code's own accounting:
|
|
195
218
|
|
|
@@ -231,7 +254,7 @@ storing a bare `pnpm test` as the thing agents execute; `/cohorte-update-pipelin
|
|
|
231
254
|
## Spec status — the lifecycle state machine
|
|
232
255
|
|
|
233
256
|
A spec's front-matter `status` is not a label, it is the pipeline's **state**: every command routes on
|
|
234
|
-
it,
|
|
257
|
+
it, `cohorte specs` boards on it, and the kanban backfill maps it to a column. Six states:
|
|
235
258
|
|
|
236
259
|
| status | meaning | written by | who may build it |
|
|
237
260
|
| --- | --- | --- | --- |
|
|
@@ -371,7 +394,7 @@ project has *decided*. Without somewhere for those, every `/cohorte-spec` re-dis
|
|
|
371
394
|
- **Never read by implementers or reviewers.** They work from the frozen contract, which already tells
|
|
372
395
|
them what to do; shipping them the rationale would cost `surfaces × dispatches` tokens per feature
|
|
373
396
|
for a fact they cannot act on. This is what keeps the journal cheap enough to be worth having.
|
|
374
|
-
- The `_` prefix is load-bearing: `/cohorte-doctor`, the
|
|
397
|
+
- The `_` prefix is load-bearing: `/cohorte-doctor`, the `cohorte specs` scanner and the kanban backfill all skip
|
|
375
398
|
`specs/_*.md`, so the journal is never mistaken for a spec (no phantom card, no bogus stage).
|
|
376
399
|
|
|
377
400
|
## Preflight — the deterministic phase gate
|
|
@@ -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."
|
|
@@ -626,7 +664,7 @@ notes a human writes as sub-bullets under an Ideas card are seed context for `/c
|
|
|
626
664
|
the trailing `%% kanban:settings … %%` block or the `kanban-plugin: board` front-matter.
|
|
627
665
|
|
|
628
666
|
Once shipped, `/cohorte-ship` appends the **PR number** to the card — `- [ ] <title> #<feature_id> — PR #<num>`.
|
|
629
|
-
The bare `#<num>` is what
|
|
667
|
+
The bare `#<num>` is what a board reader renders as a clickable link to the GitHub PR, so `/cohorte-ship` always
|
|
630
668
|
writes it when a PR was actually created.
|
|
631
669
|
|
|
632
670
|
**Move a card (the core op).** One call — the script does resolution AND the move outside the
|
|
@@ -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
|
|