cohorte 2.10.1 → 3.0.0-dev.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/README.md +7 -0
- package/assets/manifest.json +193 -0
- package/assets/migrations/config/README.md +12 -0
- package/assets/migrations/state/0001_init.sql +154 -0
- package/assets/prompts/README.md +11 -0
- package/assets/prompts/agents/fixer.md +7 -0
- package/assets/prompts/agents/implementer.md +12 -0
- package/assets/prompts/agents/reviewer.md +8 -0
- package/assets/prompts/agents/security-reviewer.md +7 -0
- package/assets/prompts/phases/build.md +5 -0
- package/assets/prompts/phases/review.md +5 -0
- package/assets/prompts/phases/test.md +5 -0
- package/assets/prompts/system.md +11 -0
- package/assets/schemas/agent-output.schema.json +225 -0
- package/assets/schemas/auth-status.schema.json +87 -0
- package/assets/schemas/command-result.schema.json +192 -0
- package/assets/schemas/commands.schema.json +952 -0
- package/assets/schemas/config.schema.json +2259 -0
- package/assets/schemas/doctor-report.schema.json +104 -0
- package/assets/schemas/events.schema.json +8514 -0
- package/assets/schemas/fake-script.schema.json +549 -0
- package/assets/schemas/inspect.schema.json +3133 -0
- package/assets/schemas/manifest.schema.json +79 -0
- package/assets/schemas/ownership.schema.json +52 -0
- package/assets/schemas/policy-verdict.schema.json +282 -0
- package/assets/schemas/project-model.schema.json +783 -0
- package/assets/schemas/project-status.schema.json +217 -0
- package/assets/schemas/reconcile-plan.schema.json +154 -0
- package/assets/schemas/run-diff.schema.json +177 -0
- package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
- package/assets/schemas/run-state.schema.json +1755 -0
- package/assets/schemas/runtime-capabilities.schema.json +1545 -0
- package/assets/schemas/sandbox-capabilities.schema.json +116 -0
- package/assets/schemas/skill.schema.json +87 -0
- package/assets/schemas/spec.schema.json +202 -0
- package/assets/schemas/tool-catalogue.schema.json +655 -0
- package/assets/schemas/trust-record.schema.json +38 -0
- package/assets/skills/README.md +9 -0
- package/assets/skills/inspect-surface.md +4 -0
- package/dist/agent-host.mjs +2187 -0
- package/dist/agent-host.mjs.map +1 -0
- package/dist/bundle-manifest.json +104 -0
- package/dist/chunks/__host-DgGPyco9.mjs +18 -0
- package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
- package/dist/chunks/approve-Cv135UqI.mjs +19 -0
- package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
- package/dist/chunks/auth-BT5oVp76.mjs +132 -0
- package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
- package/dist/chunks/auth-CYOh60KG.mjs +93 -0
- package/dist/chunks/auth-CYOh60KG.mjs.map +1 -0
- package/dist/chunks/brainstorm-BwKA6yZP.mjs +7 -0
- package/dist/chunks/brainstorm-BwKA6yZP.mjs.map +1 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
- package/dist/chunks/compose-Dvxunpto.mjs +7307 -0
- package/dist/chunks/compose-Dvxunpto.mjs.map +1 -0
- package/dist/chunks/config-DefHA2Lw.mjs +96 -0
- package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
- package/dist/chunks/contract-BkMjU1kR.mjs +295 -0
- package/dist/chunks/contract-BkMjU1kR.mjs.map +1 -0
- package/dist/chunks/contract-bQ9FJl4N.mjs +123 -0
- package/dist/chunks/contract-bQ9FJl4N.mjs.map +1 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
- package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
- package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
- package/dist/chunks/discover-eS01-me1.mjs +21 -0
- package/dist/chunks/discover-eS01-me1.mjs.map +1 -0
- package/dist/chunks/doctor-B-H2UK5e.mjs +222 -0
- package/dist/chunks/doctor-B-H2UK5e.mjs.map +1 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
- package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
- package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
- package/dist/chunks/init-BUnzIkeE.mjs +63 -0
- package/dist/chunks/init-BUnzIkeE.mjs.map +1 -0
- package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
- package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
- package/dist/chunks/logs-Giq5sUau.mjs +25 -0
- package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
- package/dist/chunks/migrate-DybihKzZ.mjs +33 -0
- package/dist/chunks/migrate-DybihKzZ.mjs.map +1 -0
- package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
- package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
- package/dist/chunks/pause-BASBfKLO.mjs +16 -0
- package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
- package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
- package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
- package/dist/chunks/providers-Dx78imKj.mjs +19 -0
- package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
- package/dist/chunks/reconcile-Bhy3IIYo.mjs +50 -0
- package/dist/chunks/reconcile-Bhy3IIYo.mjs.map +1 -0
- package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
- package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
- package/dist/chunks/retry-CLrUem1H.mjs +15 -0
- package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
- package/dist/chunks/review-BDdjb6kB.mjs +17 -0
- package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
- package/dist/chunks/run-CgTWYlcd.mjs +82 -0
- package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
- package/dist/chunks/scan-8Gy_B6kI.mjs +122 -0
- package/dist/chunks/scan-8Gy_B6kI.mjs.map +1 -0
- package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
- package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
- package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
- package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
- package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
- package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
- package/dist/chunks/src-CId6kMsY.mjs +1689 -0
- package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
- package/dist/chunks/src-CfAOde7C.mjs +5403 -0
- package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
- package/dist/chunks/src-CmFjh4M0.mjs +928 -0
- package/dist/chunks/src-CmFjh4M0.mjs.map +1 -0
- package/dist/chunks/src-Cu3TxNob.mjs +783 -0
- package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
- package/dist/chunks/src-DcU4IXMe.mjs +117 -0
- package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
- package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
- package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
- package/dist/chunks/status-SMjiOMYy.mjs +29 -0
- package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
- package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
- package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
- package/dist/chunks/update-BXIppsbi.mjs +16 -0
- package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
- package/dist/cli.mjs +175 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +17 -32
- package/CHANGELOG.md +0 -1461
- package/bin/cli.js +0 -817
- package/bin/report.js +0 -141
- package/core/adapter/render.js +0 -415
- package/core/agents/implementer.template.md +0 -142
- package/core/agents/profile-reader.md +0 -50
- package/core/agents/release.md +0 -58
- package/core/agents/review.md +0 -179
- package/core/commands/cohorte-align-ds.md +0 -33
- package/core/commands/cohorte-audit.md +0 -58
- package/core/commands/cohorte-brainstorm.md +0 -70
- package/core/commands/cohorte-build.md +0 -219
- package/core/commands/cohorte-doctor.md +0 -194
- package/core/commands/cohorte-fix.md +0 -80
- package/core/commands/cohorte-fleet.md +0 -103
- package/core/commands/cohorte-init-pipeline.md +0 -31
- package/core/commands/cohorte-intake.md +0 -92
- package/core/commands/cohorte-patch.md +0 -118
- package/core/commands/cohorte-refactor.md +0 -59
- package/core/commands/cohorte-retro.md +0 -85
- package/core/commands/cohorte-review.md +0 -270
- package/core/commands/cohorte-ship.md +0 -122
- package/core/commands/cohorte-spec.md +0 -101
- package/core/commands/cohorte-update-pipeline.md +0 -165
- package/core/hooks/gate.py +0 -588
- package/core/runtimes/claude.json +0 -74
- package/core/runtimes/codex.json +0 -85
- package/core/runtimes/cursor.json +0 -76
- package/core/runtimes/gemini.json +0 -76
- package/core/runtimes/opencode.json +0 -73
- package/core/templates/agent-handoff.md +0 -34
- package/core/templates/brainstorm-return.md +0 -36
- package/core/templates/decisions.template.md +0 -42
- package/core/templates/design-brief.md +0 -44
- package/core/templates/patch.template.md +0 -86
- package/core/templates/pr-body.md +0 -25
- package/core/templates/review-feedback.md +0 -38
- package/core/templates/spec.template.md +0 -87
- package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
- package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
- package/core/templates/steps/init-pipeline/04-write-render.md +0 -145
- package/core/templates/steps/init-pipeline/05-report.md +0 -12
- package/core/workflows/audit.js +0 -240
- package/core/workflows/loop.js +0 -641
- package/core/workflows/refactor.js +0 -276
- package/core/workflows/review.js +0 -456
- package/install.ps1 +0 -103
- package/install.sh +0 -114
- package/lib/doctor.js +0 -534
- package/lib/runtime.js +0 -139
- package/lib/versions.js +0 -149
- package/lib/yaml.js +0 -129
- package/profile/PIPELINE.template.md +0 -259
- package/profile/SCHEMA.md +0 -735
- package/profile/cohorte.config.template.yaml +0 -43
- package/scripts/assert-gate-hook.mjs +0 -75
- package/scripts/kanban-move.sh +0 -313
- package/scripts/metrics/collect.mjs +0 -531
- package/scripts/metrics/prices.json +0 -42
- package/scripts/new-feature.sh.template +0 -98
- package/scripts/preflight.sh +0 -124
- package/scripts/remove-feature.sh.template +0 -56
- package/scripts/test-adapter.mjs +0 -437
- package/scripts/test-gate.mjs +0 -443
- package/scripts/test-kanban.mjs +0 -304
- package/scripts/test-lib.mjs +0 -356
- package/scripts/test-metrics.mjs +0 -156
- package/scripts/test-workflows.mjs +0 -758
- package/scripts/validate-core.mjs +0 -325
package/scripts/test-kanban.mjs
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Behavioural tests for scripts/kanban-move.sh — the board mirror's only writer.
|
|
3
|
-
//
|
|
4
|
-
// The mirror's failure mode is not a crash, it is silence: every call site chains
|
|
5
|
-
// `|| true`, so a card that stops moving looks exactly like a project with no
|
|
6
|
-
// board. That is how a shipped feature sat in "Ready to build" for a day — the
|
|
7
|
-
// ship session decided, without opening the config, that no board was configured.
|
|
8
|
-
// `auto` moved that decision out of the agent and into here, so here is where it
|
|
9
|
-
// has to be pinned: what resolves, what refuses to resolve, and the fact that a
|
|
10
|
-
// refusal SAYS WHY on stdout instead of exiting quietly.
|
|
11
|
-
//
|
|
12
|
-
// node scripts/test-kanban.mjs
|
|
13
|
-
|
|
14
|
-
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
17
|
-
import { join } from "node:path";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
19
|
-
|
|
20
|
-
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
21
|
-
const SCRIPT = join(root, "scripts", "kanban-move.sh");
|
|
22
|
-
|
|
23
|
-
let failures = 0;
|
|
24
|
-
const check = (name, cond, detail = "") => {
|
|
25
|
-
if (cond) console.log(` ✓ ${name}`);
|
|
26
|
-
else { failures++; console.error(` ✗ ${name}${detail ? ` — ${detail}` : ""}`); }
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const tmps = [];
|
|
30
|
-
function scratch() {
|
|
31
|
-
const d = mkdtempSync(join(tmpdir(), "kanban-"));
|
|
32
|
-
tmps.push(d);
|
|
33
|
-
return d;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const COLUMNS = ["Ideas", "Brainstorm", "Spec", "Ready to build", "Building",
|
|
37
|
-
"Review", "Fix", "Ship", "Shipped"];
|
|
38
|
-
|
|
39
|
-
function board(dir, cards = {}, name = "Tasks.md") {
|
|
40
|
-
const body = [
|
|
41
|
-
"---", "", "kanban-plugin: board", "", "---", "",
|
|
42
|
-
...COLUMNS.flatMap((c) => ["## " + c, "", ...(cards[c] || []), ""]),
|
|
43
|
-
"%% kanban:settings", "```",
|
|
44
|
-
JSON.stringify({ "kanban-plugin": "board", "list-collapse": COLUMNS.map(() => false) }),
|
|
45
|
-
"```", "%%", "",
|
|
46
|
-
].join("\n");
|
|
47
|
-
const p = join(dir, name);
|
|
48
|
-
writeFileSync(p, body);
|
|
49
|
-
return p;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function config(dir, { project = "demo", vault, boardRel = "Demo/Tasks.md",
|
|
53
|
-
enabled = true, kanbanEnabled = true, columns, boardColumns } = {}) {
|
|
54
|
-
const lines = [
|
|
55
|
-
`enabled: ${enabled} # cfg:enabled`,
|
|
56
|
-
"",
|
|
57
|
-
"obsidian:",
|
|
58
|
-
` vault_path: "${vault}" # cfg:vault_path`,
|
|
59
|
-
"",
|
|
60
|
-
"kanban:",
|
|
61
|
-
` enabled: ${kanbanEnabled} # cfg:kanban_enabled`,
|
|
62
|
-
" columns:",
|
|
63
|
-
...Object.entries(columns || {
|
|
64
|
-
ideas: "Ideas", brainstorm: "Brainstorm", spec: "Spec", ready: "Ready to build",
|
|
65
|
-
building: "Building", review: "Review", fix: "Fix", ship: "Ship", shipped: "Shipped",
|
|
66
|
-
}).map(([k, v]) => ` ${k}: "${v}"`),
|
|
67
|
-
" boards:",
|
|
68
|
-
` ${project}:`,
|
|
69
|
-
` board: "${boardRel}"`,
|
|
70
|
-
...(boardColumns ? [` columns: { ${boardColumns} }`] : []),
|
|
71
|
-
];
|
|
72
|
-
const p = join(dir, "cohorte.config.yaml");
|
|
73
|
-
writeFileSync(p, lines.join("\n") + "\n");
|
|
74
|
-
return p;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function profile(dir, name) {
|
|
78
|
-
writeFileSync(join(dir, "PIPELINE.md"), [
|
|
79
|
-
"# PIPELINE.md", "",
|
|
80
|
-
"```yaml pipeline-profile",
|
|
81
|
-
"# ── identity ──",
|
|
82
|
-
`name: ${name}`,
|
|
83
|
-
"ui_language: French",
|
|
84
|
-
"surfaces:",
|
|
85
|
-
" - key: backend",
|
|
86
|
-
" name: not-the-project-name",
|
|
87
|
-
"```", "",
|
|
88
|
-
].join("\n"));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function run(args, { cwd, config: cfg } = {}) {
|
|
92
|
-
const r = spawnSync("sh", [SCRIPT, ...args], {
|
|
93
|
-
cwd, encoding: "utf8",
|
|
94
|
-
env: { ...process.env, ...(cfg ? { COHORTE_CONFIG: cfg } : {}) },
|
|
95
|
-
});
|
|
96
|
-
return { status: r.status, out: (r.stdout || "").trim(), err: (r.stderr || "").trim() };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const columnOf = (path, id) => {
|
|
100
|
-
let col = null;
|
|
101
|
-
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
102
|
-
if (line.startsWith("## ")) col = line.slice(3).trim();
|
|
103
|
-
else if (line.includes("#" + id)) return col;
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// ── explicit board path (the pre-existing contract) ─────────────────────────
|
|
109
|
-
console.log("explicit board");
|
|
110
|
-
{
|
|
111
|
-
const d = scratch();
|
|
112
|
-
const b = board(d, { "Ready to build": ["- [ ] Some feature #feat-a"] });
|
|
113
|
-
const r = run([b, "feat-a", "Shipped", "--pr", "42"]);
|
|
114
|
-
check("moves a card to a literal heading", r.status === 0 && columnOf(b, "feat-a") === "Shipped", r.err);
|
|
115
|
-
check("appends the PR number", readFileSync(b, "utf8").includes("#feat-a — PR #42"));
|
|
116
|
-
}
|
|
117
|
-
{
|
|
118
|
-
const d = scratch();
|
|
119
|
-
const b = board(d);
|
|
120
|
-
const r = run([b, "feat-new", "Spec", "--title", "Brand new"]);
|
|
121
|
-
check("creates a missing card", r.status === 0 && columnOf(b, "feat-new") === "Spec", r.err);
|
|
122
|
-
check("uses --title for the new card", readFileSync(b, "utf8").includes("- [ ] Brand new #feat-new"));
|
|
123
|
-
}
|
|
124
|
-
{
|
|
125
|
-
const d = scratch();
|
|
126
|
-
const b = board(d, { Ideas: ["- [ ] Dup #dup", "- [ ] Dup again #dup"] });
|
|
127
|
-
run([b, "dup", "Building"]);
|
|
128
|
-
const hits = readFileSync(b, "utf8").split("\n").filter((l) => l.includes("#dup")).length;
|
|
129
|
-
check("collapses duplicates to one card", hits === 1, `found ${hits}`);
|
|
130
|
-
}
|
|
131
|
-
{
|
|
132
|
-
const d = scratch();
|
|
133
|
-
const b = board(d, { Ideas: ["- [ ] Parent #par", " - a sub-note", " - another"] });
|
|
134
|
-
run([b, "par", "Brainstorm"]);
|
|
135
|
-
const txt = readFileSync(b, "utf8");
|
|
136
|
-
check("carries sub-notes along", columnOf(b, "par") === "Brainstorm" && txt.includes(" - a sub-note"));
|
|
137
|
-
check("leaves the settings block intact", txt.includes("%% kanban:settings"));
|
|
138
|
-
}
|
|
139
|
-
{
|
|
140
|
-
const d = scratch();
|
|
141
|
-
const b = board(d, { Ideas: ["- [ ] X #x"] });
|
|
142
|
-
const r = run([b, "x", "Nonexistent column"]);
|
|
143
|
-
check("unknown literal column is a loud failure (exit 3)", r.status === 3, `exit ${r.status}`);
|
|
144
|
-
const r2 = run(["/nope/board.md", "x", "Ideas"]);
|
|
145
|
-
check("missing explicit board is a loud failure (exit 3)", r2.status === 3, `exit ${r2.status}`);
|
|
146
|
-
}
|
|
147
|
-
{
|
|
148
|
-
const d = scratch();
|
|
149
|
-
const b = board(d, { Ideas: ["- [ ] X #x"] });
|
|
150
|
-
for (let i = 0; i < 6; i++) run([b, "x", i % 2 ? "Ideas" : "Building"]);
|
|
151
|
-
const blanks = (readFileSync(b, "utf8").match(/\n\n\n/g) || []).length;
|
|
152
|
-
check("repeated moves do not pad the board with blank lines", blanks === 0,
|
|
153
|
-
`${blanks} runs of blank lines after 6 moves`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ── auto resolution (the fix) ───────────────────────────────────────────────
|
|
157
|
-
console.log("auto resolution");
|
|
158
|
-
{
|
|
159
|
-
const d = scratch();
|
|
160
|
-
const vault = join(d, "vault");
|
|
161
|
-
mkdirSync(join(vault, "Demo"), { recursive: true });
|
|
162
|
-
const b = board(join(vault, "Demo"), { "Ready to build": ["- [ ] Feature #feat-b"] });
|
|
163
|
-
const cfg = config(d, { project: "demo", vault });
|
|
164
|
-
const repo = scratch();
|
|
165
|
-
profile(repo, "demo");
|
|
166
|
-
const r = run(["auto", "feat-b", "shipped", "--pr", "7"], { cwd: repo, config: cfg });
|
|
167
|
-
check("resolves vault + board + stage from the config",
|
|
168
|
-
r.status === 0 && columnOf(b, "feat-b") === "Shipped", `${r.err} ${r.out}`);
|
|
169
|
-
check("reports the move", r.out.startsWith("moved #feat-b"), r.out);
|
|
170
|
-
}
|
|
171
|
-
{
|
|
172
|
-
const d = scratch();
|
|
173
|
-
const vault = join(d, "vault");
|
|
174
|
-
mkdirSync(join(vault, "Demo"), { recursive: true });
|
|
175
|
-
const b = board(join(vault, "Demo"));
|
|
176
|
-
const cfg = config(d, { project: "demo", vault });
|
|
177
|
-
const repo = scratch();
|
|
178
|
-
profile(repo, "demo");
|
|
179
|
-
const stages = { ideas: "Ideas", brainstorm: "Brainstorm", spec: "Spec", ready: "Ready to build",
|
|
180
|
-
building: "Building", review: "Review", fix: "Fix", ship: "Ship", shipped: "Shipped" };
|
|
181
|
-
let ok = true, bad = "";
|
|
182
|
-
for (const [stage, heading] of Object.entries(stages)) {
|
|
183
|
-
run(["auto", "st", stage], { cwd: repo, config: cfg });
|
|
184
|
-
if (columnOf(b, "st") !== heading) { ok = false; bad = `${stage} → ${columnOf(b, "st")}`; }
|
|
185
|
-
}
|
|
186
|
-
check("every one of the nine stage keys maps to its heading", ok, bad);
|
|
187
|
-
}
|
|
188
|
-
{
|
|
189
|
-
const d = scratch();
|
|
190
|
-
const vault = join(d, "vault");
|
|
191
|
-
mkdirSync(join(vault, "Demo"), { recursive: true });
|
|
192
|
-
const b = board(join(vault, "Demo"), { Ideas: [], Prêt: [] }, "Tasks.md");
|
|
193
|
-
// a board whose headings are localised, declared per-board in the config
|
|
194
|
-
writeFileSync(b, readFileSync(b, "utf8").replace("## Ready to build", "## Prêt"));
|
|
195
|
-
const cfg = config(d, { project: "demo", vault, boardColumns: 'ready: "Prêt"' });
|
|
196
|
-
const repo = scratch();
|
|
197
|
-
profile(repo, "demo");
|
|
198
|
-
const r = run(["auto", "loc", "ready", "--title", "Localisé"], { cwd: repo, config: cfg });
|
|
199
|
-
check("per-board column override beats the global mapping",
|
|
200
|
-
r.status === 0 && columnOf(b, "loc") === "Prêt", `${r.err} ${r.out}`);
|
|
201
|
-
}
|
|
202
|
-
{
|
|
203
|
-
const repo = scratch();
|
|
204
|
-
profile(repo, "demo");
|
|
205
|
-
const r = run(["auto", "x", "shipped"], { cwd: repo, config: join(repo, "nope.yaml") });
|
|
206
|
-
check("no config ⇒ exit 0", r.status === 0, `exit ${r.status}`);
|
|
207
|
-
check("no config ⇒ says why on stdout", /^kanban: no config at /.test(r.out), r.out);
|
|
208
|
-
}
|
|
209
|
-
{
|
|
210
|
-
const d = scratch();
|
|
211
|
-
const cfg = config(d, { project: "other", vault: d });
|
|
212
|
-
const repo = scratch();
|
|
213
|
-
profile(repo, "demo");
|
|
214
|
-
const r = run(["auto", "x", "shipped"], { cwd: repo, config: cfg });
|
|
215
|
-
check("project not in boards ⇒ exit 0", r.status === 0, `exit ${r.status}`);
|
|
216
|
-
check("project not in boards ⇒ names the project it looked for",
|
|
217
|
-
r.out.includes('no board configured for project "demo"'), r.out);
|
|
218
|
-
}
|
|
219
|
-
{
|
|
220
|
-
const d = scratch();
|
|
221
|
-
const cfg = config(d, { project: "demo", vault: d, kanbanEnabled: false });
|
|
222
|
-
const repo = scratch();
|
|
223
|
-
profile(repo, "demo");
|
|
224
|
-
const r = run(["auto", "x", "shipped"], { cwd: repo, config: cfg });
|
|
225
|
-
check("kanban.enabled: false ⇒ exit 0 with a reason",
|
|
226
|
-
r.status === 0 && r.out.includes("kanban.enabled: false"), `${r.status} ${r.out}`);
|
|
227
|
-
}
|
|
228
|
-
{
|
|
229
|
-
const d = scratch();
|
|
230
|
-
const cfg = config(d, { project: "demo", vault: d, enabled: false });
|
|
231
|
-
const repo = scratch();
|
|
232
|
-
profile(repo, "demo");
|
|
233
|
-
const r = run(["auto", "x", "shipped"], { cwd: repo, config: cfg });
|
|
234
|
-
check("master enabled: false ⇒ exit 0 with a reason",
|
|
235
|
-
r.status === 0 && r.out.includes("enabled: false"), `${r.status} ${r.out}`);
|
|
236
|
-
}
|
|
237
|
-
{
|
|
238
|
-
const d = scratch();
|
|
239
|
-
const cfg = config(d, { project: "demo", vault: join(d, "vault"), boardRel: "Demo/Gone.md" });
|
|
240
|
-
const repo = scratch();
|
|
241
|
-
profile(repo, "demo");
|
|
242
|
-
const r = run(["auto", "x", "shipped"], { cwd: repo, config: cfg });
|
|
243
|
-
check("configured board file missing ⇒ exit 0, names the path",
|
|
244
|
-
r.status === 0 && r.out.includes("board file not found"), `${r.status} ${r.out}`);
|
|
245
|
-
}
|
|
246
|
-
{
|
|
247
|
-
const repo = scratch(); // no PIPELINE.md at all
|
|
248
|
-
const d = scratch();
|
|
249
|
-
const cfg = config(d, { project: "demo", vault: d });
|
|
250
|
-
const r = run(["auto", "x", "shipped"], { cwd: repo, config: cfg });
|
|
251
|
-
check("no profile ⇒ exit 0 with a reason",
|
|
252
|
-
r.status === 0 && r.out.includes("no project name"), `${r.status} ${r.out}`);
|
|
253
|
-
}
|
|
254
|
-
{
|
|
255
|
-
// The profile's `name:` is the one in the pipeline-profile block — not a
|
|
256
|
-
// `name:` nested under a surface, which is what a naive grep would find.
|
|
257
|
-
const d = scratch();
|
|
258
|
-
const vault = join(d, "vault");
|
|
259
|
-
mkdirSync(join(vault, "Demo"), { recursive: true });
|
|
260
|
-
const b = board(join(vault, "Demo"));
|
|
261
|
-
const cfg = config(d, { project: "demo", vault });
|
|
262
|
-
const repo = scratch();
|
|
263
|
-
profile(repo, "demo");
|
|
264
|
-
const r = run(["auto", "nested", "spec"], { cwd: repo, config: cfg });
|
|
265
|
-
check("reads the profile name from the pipeline-profile block only",
|
|
266
|
-
r.status === 0 && columnOf(b, "nested") === "Spec", `${r.err} ${r.out}`);
|
|
267
|
-
}
|
|
268
|
-
{
|
|
269
|
-
const d = scratch();
|
|
270
|
-
const vault = join(d, "vault");
|
|
271
|
-
mkdirSync(join(vault, "Demo"), { recursive: true });
|
|
272
|
-
const b = board(join(vault, "Demo"));
|
|
273
|
-
const cfg = config(d, { project: "demo", vault });
|
|
274
|
-
const r = run(["auto", "ov", "ship", "--project", "demo"], { cwd: scratch(), config: cfg });
|
|
275
|
-
check("--project overrides profile lookup",
|
|
276
|
-
r.status === 0 && columnOf(b, "ov") === "Ship", `${r.err} ${r.out}`);
|
|
277
|
-
}
|
|
278
|
-
{
|
|
279
|
-
const d = scratch();
|
|
280
|
-
const vault = join(d, "vault");
|
|
281
|
-
mkdirSync(join(vault, "Demo"), { recursive: true });
|
|
282
|
-
const b = board(join(vault, "Demo"));
|
|
283
|
-
const cfg = config(d, { project: "demo", vault });
|
|
284
|
-
const repo = scratch();
|
|
285
|
-
profile(repo, "demo");
|
|
286
|
-
const before = readFileSync(b, "utf8");
|
|
287
|
-
const r = run(["--check"], { cwd: repo, config: cfg });
|
|
288
|
-
check("--check names the resolved board",
|
|
289
|
-
r.status === 0 && r.out === `kanban: project "demo" -> ${b}`, r.out);
|
|
290
|
-
check("--check does not touch the board", readFileSync(b, "utf8") === before);
|
|
291
|
-
const r2 = run(["--check"], { cwd: scratch(), config: cfg });
|
|
292
|
-
check("--check reports the reason when nothing resolves",
|
|
293
|
-
r2.status === 0 && r2.out.startsWith("kanban: no project name"), r2.out);
|
|
294
|
-
}
|
|
295
|
-
{
|
|
296
|
-
const r = run(["auto", "x"]);
|
|
297
|
-
check("too few arguments is a usage error (exit 2)", r.status === 2, `exit ${r.status}`);
|
|
298
|
-
const r2 = run(["auto", "x", "shipped", "--bogus", "1"]);
|
|
299
|
-
check("unknown flag is a usage error (exit 2)", r2.status === 2, `exit ${r2.status}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
for (const d of tmps) rmSync(d, { recursive: true, force: true });
|
|
303
|
-
if (failures) { console.error(`\n${failures} failing check(s)`); process.exit(1); }
|
|
304
|
-
console.log("\nall kanban checks passed");
|
package/scripts/test-lib.mjs
DELETED
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Tests for the shared readers in lib/.
|
|
3
|
-
//
|
|
4
|
-
// These are shipped runtime code behind `cohorte doctor` and `cohorte specs`
|
|
5
|
-
// (and the Francois panels that call them): a hand-rolled YAML parser every
|
|
6
|
-
// check derives from, the JS port of /cohorte-doctor, and the runtime-layout
|
|
7
|
-
// resolver that decides where a given coding agent keeps its core.
|
|
8
|
-
//
|
|
9
|
-
// node scripts/test-lib.mjs
|
|
10
|
-
|
|
11
|
-
import { createRequire } from "node:module";
|
|
12
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
|
-
|
|
17
|
-
const require = createRequire(import.meta.url);
|
|
18
|
-
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
19
|
-
const { parse, parseProfileBlock } = require(join(root, "lib/yaml.js"));
|
|
20
|
-
const { state, scanSpecs } = require(join(root, "lib/doctor.js"));
|
|
21
|
-
const adapter = require(join(root, 'core/adapter/render.js'));
|
|
22
|
-
|
|
23
|
-
let failures = 0;
|
|
24
|
-
const check = (name, cond, detail = "") => {
|
|
25
|
-
if (cond) console.log(` ✓ ${name}`);
|
|
26
|
-
else { failures++; console.error(` ✗ ${name}${detail ? ` — ${detail}` : ""}`); }
|
|
27
|
-
};
|
|
28
|
-
const eq = (name, got, want) =>
|
|
29
|
-
check(name, JSON.stringify(got) === JSON.stringify(want), `got ${JSON.stringify(got)}`);
|
|
30
|
-
|
|
31
|
-
const tmps = [];
|
|
32
|
-
const scratch = () => { const d = mkdtempSync(join(tmpdir(), "cohorte-lib-")); tmps.push(d); return d; };
|
|
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();
|
|
39
|
-
|
|
40
|
-
// ── yaml.js ──────────────────────────────────────────────────────────────────
|
|
41
|
-
console.log("yaml.js — the profile parser");
|
|
42
|
-
{
|
|
43
|
-
eq("scalars: bool / int / null / quoted",
|
|
44
|
-
parse("a: true\nb: 12\nc: null\nd: \"x: y\"\ne: ~"),
|
|
45
|
-
{ a: true, b: 12, c: null, d: "x: y", e: null });
|
|
46
|
-
eq("flow array", parse("t: [Read, Write, mcp__serena]").t, ["Read", "Write", "mcp__serena"]);
|
|
47
|
-
eq("flow map", parse("p: { api: 3333, web: 5173 }").p, { api: 3333, web: 5173 });
|
|
48
|
-
eq("nested map", parse("a:\n b:\n c: 1").a.b, { c: 1 });
|
|
49
|
-
eq("block sequence of scalars", parse("d:\n - one\n - two").d, ["one", "two"]);
|
|
50
|
-
eq("block sequence of maps",
|
|
51
|
-
parse("s:\n - key: a\n path: x\n - key: b\n path: y").s,
|
|
52
|
-
[{ key: "a", path: "x" }, { key: "b", path: "y" }]);
|
|
53
|
-
eq("comment after a value is stripped", parse("a: 1 # note").a, 1);
|
|
54
|
-
eq("a # inside quotes is NOT a comment", parse('a: "x # y"').a, "x # y");
|
|
55
|
-
eq("a # not preceded by whitespace is literal", parse("a: c#d").a, "c#d");
|
|
56
|
-
eq("empty value ⇒ null", parse("a:").a, null);
|
|
57
|
-
eq("a colon in the value survives", parse('m: "cd x && node ace migration:run"').m,
|
|
58
|
-
"cd x && node ace migration:run");
|
|
59
|
-
eq("empty flow array", parse("a: []").a, []);
|
|
60
|
-
check("no fenced block ⇒ null", parseProfileBlock("# just prose") === null);
|
|
61
|
-
|
|
62
|
-
// The real thing: the shipped template must round-trip.
|
|
63
|
-
const tpl = readFileSync(join(root, "profile/PIPELINE.template.md"), "utf8");
|
|
64
|
-
const p = parseProfileBlock(adapter.adaptInstructions(tpl, adapter.loadRuntime('claude')));
|
|
65
|
-
check("the shipped PIPELINE.template.md parses", !!p);
|
|
66
|
-
eq("…surfaces are a list of 2", (p.surfaces || []).length, 2);
|
|
67
|
-
eq("…surface tools survive as an array", p.surfaces[0].tools.length, 7);
|
|
68
|
-
eq("…uses_design is a real boolean", p.surfaces[1].uses_design, true);
|
|
69
|
-
eq("…build_cmd stays an empty string, not null", p.surfaces[0].build_cmd, "");
|
|
70
|
-
eq("…gate.deny is a list of 4", p.gate.deny.length, 4);
|
|
71
|
-
eq("…gate.preflight.max_age_minutes is a number", p.gate.preflight.max_age_minutes, 30);
|
|
72
|
-
eq("…port_base flow map", p.isolation.port_base, { api: 3333, web: 5173 });
|
|
73
|
-
eq("…a chained migrate command survives", p.commands.migrate, "cd apps/api && node ace migration:run");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ── doctor.js ────────────────────────────────────────────────────────────────
|
|
77
|
-
console.log("doctor.js — the /cohorte-doctor port");
|
|
78
|
-
{
|
|
79
|
-
const spec = (fm) => `---\n${fm}\n---\n\n# x\n`;
|
|
80
|
-
const d = scratch();
|
|
81
|
-
mkdirSync(join(d, "specs"), { recursive: true });
|
|
82
|
-
writeFileSync(join(d, "specs", "a.md"), spec("feature_id: a\ntitle: A\nstatus: frozen\nbranch: feature/a"));
|
|
83
|
-
writeFileSync(join(d, "specs", "b.md"), spec("feature_id: b\nstatus: shipped # done"));
|
|
84
|
-
writeFileSync(join(d, "specs", "c.md"), "no front-matter at all");
|
|
85
|
-
writeFileSync(join(d, "specs", "_template.md"), spec("status: draft"));
|
|
86
|
-
// /cohorte-audit writes this file by design and it has no front-matter. Scanning it as a
|
|
87
|
-
// spec made /cohorte-doctor warn about a file cohorte itself had just created — it fired in
|
|
88
|
-
// every project that had ever run /cohorte-audit.
|
|
89
|
-
writeFileSync(join(d, "specs", "refactor-backlog.md"), "# Refactor Backlog\n\n## backend\n- [ ] x\n");
|
|
90
|
-
const specs = scanSpecs(d);
|
|
91
|
-
eq("_template.md is excluded", specs.length, 3);
|
|
92
|
-
eq("the /cohorte-audit backlog is not scanned as a spec",
|
|
93
|
-
specs.some(s => s.file === "refactor-backlog.md"), false);
|
|
94
|
-
eq("front-matter fields are read", specs.find(s => s.id === "a").title, "A");
|
|
95
|
-
eq("a trailing comment is stripped from status", specs.find(s => s.id === "b").status, "shipped");
|
|
96
|
-
eq("no front-matter ⇒ id falls back to the filename", specs.find(s => s.file === "c.md").id, "c");
|
|
97
|
-
}
|
|
98
|
-
{
|
|
99
|
-
// A fully-wired synthetic project must come back green on the checks that can
|
|
100
|
-
// be computed from disk.
|
|
101
|
-
const g = scratch(); // stands in for ~/.claude
|
|
102
|
-
const d = scratch();
|
|
103
|
-
const gate = {
|
|
104
|
-
deny: ["x"], ask: ["y"], ask_on_default_branch: ["git push"], default_branch: "main",
|
|
105
|
-
preflight: { enabled: true, agents: ["review"], max_age_minutes: 30 },
|
|
106
|
-
};
|
|
107
|
-
writeFileSync(join(d, "PIPELINE.md"), [
|
|
108
|
-
"```yaml pipeline-profile",
|
|
109
|
-
"name: Proj",
|
|
110
|
-
"retrieval:",
|
|
111
|
-
" provider: serena",
|
|
112
|
-
"surfaces:",
|
|
113
|
-
" - key: backend",
|
|
114
|
-
" path: apps/api",
|
|
115
|
-
" agent: backend",
|
|
116
|
-
"gate:",
|
|
117
|
-
" default_branch: main",
|
|
118
|
-
' deny: ["x"]',
|
|
119
|
-
' ask: ["y"]',
|
|
120
|
-
' ask_on_default_branch: ["git push"]',
|
|
121
|
-
" preflight:",
|
|
122
|
-
" enabled: true",
|
|
123
|
-
" agents: [review]",
|
|
124
|
-
" max_age_minutes: 30",
|
|
125
|
-
"```",
|
|
126
|
-
].join("\n"));
|
|
127
|
-
mkdirSync(join(d, ".claude", "agents"), { recursive: true });
|
|
128
|
-
mkdirSync(join(d, ".claude", "pipeline"), { recursive: true });
|
|
129
|
-
writeFileSync(join(d, ".claude", "pipeline", "VERSION"), "9.9.9\n");
|
|
130
|
-
writeFileSync(join(d, ".claude", "agents", "backend.md"), "x");
|
|
131
|
-
writeFileSync(join(d, ".claude", "gate-config.json"), JSON.stringify(gate));
|
|
132
|
-
writeFileSync(join(d, ".mcp.json"), JSON.stringify({ mcpServers: { serena: {} } }));
|
|
133
|
-
mkdirSync(join(d, ".claude", "workflows"), { recursive: true });
|
|
134
|
-
for (const w of ["review.js", "audit.js", "refactor.js", "loop.js"]) {
|
|
135
|
-
writeFileSync(join(d, ".claude", "workflows", w), "x");
|
|
136
|
-
}
|
|
137
|
-
writeFileSync(join(d, ".claude", "agents", "profile-reader.md"), "x");
|
|
138
|
-
writeFileSync(join(d, ".claude", "settings.json"), JSON.stringify({
|
|
139
|
-
hooks: { PreToolUse: [{ matcher: "Bash|Task", hooks: [{ type: "command", command: 'py "C:\\x\\gate.py"' }] }] },
|
|
140
|
-
}));
|
|
141
|
-
|
|
142
|
-
const by = (checks, id) => checks.find(c => c.id === id);
|
|
143
|
-
let s = await state({ projectRoot: d, globalDir: g, cliVersion: "9.9.9" });
|
|
144
|
-
eq("profile parses ⇒ ok", by(s.checks, "profile").status, "ok");
|
|
145
|
-
eq("surface agent present ⇒ ok", by(s.checks, "agents").status, "ok");
|
|
146
|
-
eq("gate mirrors the profile ⇒ ok", by(s.checks, "gate").status, "ok");
|
|
147
|
-
eq("Windows-quoted hook command is recognised", by(s.checks, "hooks").status, "ok");
|
|
148
|
-
eq("retrieval wired in .mcp.json ⇒ ok", by(s.checks, "retrieval").status, "ok");
|
|
149
|
-
eq("workflows + profile-reader ⇒ ok", by(s.checks, "workflows").status, "ok");
|
|
150
|
-
|
|
151
|
-
// Local artifacts: a versioned preflight stamp is what made the phase gate ask on
|
|
152
|
-
// every review dispatch forever, so its absence from .gitignore is a hard failure.
|
|
153
|
-
check("no .gitignore ⇒ local artifacts flagged bad (the stamp is the breaking one)",
|
|
154
|
-
by(s.checks, "artifacts").status === "bad"
|
|
155
|
-
&& /preflight\.ok/.test(by(s.checks, "artifacts").detail),
|
|
156
|
-
by(s.checks, "artifacts").detail);
|
|
157
|
-
writeFileSync(join(d, ".gitignore"),
|
|
158
|
-
"node_modules/\n.claude/preflight.ok\n.claude/pipeline-metrics.jsonl\nspecs/reports/\n");
|
|
159
|
-
s = await state({ projectRoot: d, globalDir: g, cliVersion: "9.9.9" });
|
|
160
|
-
eq("all local artifacts gitignored ⇒ ok", by(s.checks, "artifacts").status, "ok");
|
|
161
|
-
writeFileSync(join(d, ".gitignore"), "node_modules/\n.claude/\nspecs/reports/\n");
|
|
162
|
-
s = await state({ projectRoot: d, globalDir: g, cliVersion: "9.9.9" });
|
|
163
|
-
eq("a `.claude/` directory rule covers the files inside it",
|
|
164
|
-
by(s.checks, "artifacts").status, "ok");
|
|
165
|
-
|
|
166
|
-
// …and each check must actually FAIL when its precondition breaks.
|
|
167
|
-
writeFileSync(join(d, ".claude", "gate-config.json"),
|
|
168
|
-
JSON.stringify({ ...gate, preflight: { enabled: false } }));
|
|
169
|
-
s = await state({ projectRoot: d, globalDir: g, cliVersion: "9.9.9" });
|
|
170
|
-
check("gate-config preflight drift is detected",
|
|
171
|
-
by(s.checks, "gate").status === "warn" && /preflight/.test(by(s.checks, "gate").detail),
|
|
172
|
-
by(s.checks, "gate").detail);
|
|
173
|
-
writeFileSync(join(d, ".claude", "gate-config.json"), JSON.stringify(gate));
|
|
174
|
-
|
|
175
|
-
writeFileSync(join(d, ".claude", "settings.json"), JSON.stringify({
|
|
176
|
-
hooks: { PreToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: "python3 /x/gate.py" }] }] },
|
|
177
|
-
}));
|
|
178
|
-
s = await state({ projectRoot: d, globalDir: g, cliVersion: "9.9.9" });
|
|
179
|
-
check("a Bash-only matcher is flagged (the 1.3.0 dead phase gate)",
|
|
180
|
-
by(s.checks, "hooks").status === "warn" && /Task/.test(by(s.checks, "hooks").detail),
|
|
181
|
-
by(s.checks, "hooks").detail);
|
|
182
|
-
|
|
183
|
-
writeFileSync(join(d, ".claude", "settings.json"), JSON.stringify({
|
|
184
|
-
hooks: { PreToolUse: [
|
|
185
|
-
{ matcher: "Bash|Task", hooks: [{ type: "command", command: "python3 /x/gate.py" }] },
|
|
186
|
-
{ matcher: "Bash|Task", hooks: [{ type: "command", command: 'py "/x/gate.py"' }] },
|
|
187
|
-
] },
|
|
188
|
-
}));
|
|
189
|
-
s = await state({ projectRoot: d, globalDir: g, cliVersion: "9.9.9" });
|
|
190
|
-
check("a duplicate registration is flagged (it double-prompts)",
|
|
191
|
-
by(s.checks, "hooks").status === "warn" && /2×/.test(by(s.checks, "hooks").detail),
|
|
192
|
-
by(s.checks, "hooks").detail);
|
|
193
|
-
|
|
194
|
-
rmSync(join(d, ".mcp.json"));
|
|
195
|
-
writeFileSync(join(d, ".claude", "agents", "orphan.md"), "x");
|
|
196
|
-
s = await state({ projectRoot: d, globalDir: g, cliVersion: "9.9.9" });
|
|
197
|
-
check("provider declared but never wired ⇒ warn",
|
|
198
|
-
by(s.checks, "retrieval").status === "warn", by(s.checks, "retrieval").detail);
|
|
199
|
-
check("an agent file with no surface ⇒ orphan warn",
|
|
200
|
-
by(s.checks, "agents").status === "warn" && /orphan/.test(by(s.checks, "agents").detail),
|
|
201
|
-
by(s.checks, "agents").detail);
|
|
202
|
-
|
|
203
|
-
const bare = scratch();
|
|
204
|
-
s = await state({ projectRoot: bare, globalDir: g, cliVersion: "9.9.9" });
|
|
205
|
-
eq("no PIPELINE.md ⇒ profile bad", by(s.checks, "profile").status, "bad");
|
|
206
|
-
eq("no core ⇒ core bad", by(s.checks, "core").status, "bad");
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ── runtime.js + a non-Claude layout ────────────────────────────────────────
|
|
210
|
-
// Every path-dependent check used to assume `.claude/`. On a repo driven from Cursor that
|
|
211
|
-
// reported a healthy install as three ❌ and a ⚠️ — no core, no rendered agent, artifacts not
|
|
212
|
-
// ignored, hook not registered — and every one was wrong. A false red is worse than no check:
|
|
213
|
-
// it sends a human fixing something that is not broken.
|
|
214
|
-
console.log("doctor.js — a non-Claude runtime layout");
|
|
215
|
-
{
|
|
216
|
-
const d = scratch();
|
|
217
|
-
const g = join(d, "global-claude");
|
|
218
|
-
mkdirSync(g, { recursive: true });
|
|
219
|
-
|
|
220
|
-
const core = join(d, ".cohorte", "cursor");
|
|
221
|
-
mkdirSync(join(core, "pipeline"), { recursive: true });
|
|
222
|
-
writeFileSync(join(core, "pipeline", "VERSION"), "9.9.9\n");
|
|
223
|
-
writeFileSync(join(core, "pipeline", "runtimes.json"), JSON.stringify({
|
|
224
|
-
cursor: {
|
|
225
|
-
label: "Cursor", scope: "project", core_version: "9.9.9",
|
|
226
|
-
capabilities: { subagents: true, hooks: true, workflows: false, tool_restriction: true },
|
|
227
|
-
paths: {
|
|
228
|
-
core, commands: join(d, ".cursor", "commands"), agents: join(d, ".cursor", "agents"),
|
|
229
|
-
hooks_config: join(d, ".cursor", "hooks.json"), state: ".cohorte",
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
}));
|
|
233
|
-
mkdirSync(join(d, ".cursor", "agents"), { recursive: true });
|
|
234
|
-
writeFileSync(join(d, ".cursor", "agents", "api.md"), "---\nname: api\n---\n");
|
|
235
|
-
writeFileSync(join(d, ".cursor", "hooks.json"), JSON.stringify({
|
|
236
|
-
version: 1,
|
|
237
|
-
hooks: { beforeShellExecution: [{ command: `python3 ${core}/hooks/gate.py --runtime cursor` }] },
|
|
238
|
-
}));
|
|
239
|
-
mkdirSync(join(d, ".cohorte"), { recursive: true });
|
|
240
|
-
const gate = { deny: ["rm -rf /"], ask: [], ask_on_default_branch: [], default_branch: "main" };
|
|
241
|
-
writeFileSync(join(d, ".cohorte", "gate-config.json"),
|
|
242
|
-
JSON.stringify({ ...gate, preflight: { enabled: false } }));
|
|
243
|
-
writeFileSync(join(d, ".gitignore"),
|
|
244
|
-
".cohorte/preflight.ok\n.cohorte/pipeline-metrics.jsonl\nspecs/reports/\n");
|
|
245
|
-
writeFileSync(join(d, "PIPELINE.md"), [
|
|
246
|
-
"```yaml pipeline-profile", "name: demo",
|
|
247
|
-
"surfaces:", " - key: api", " path: src/api", " agent: api",
|
|
248
|
-
"gate:", " deny:", " - rm -rf /", " default_branch: main",
|
|
249
|
-
" preflight:", " enabled: false", "```",
|
|
250
|
-
].join("\n"));
|
|
251
|
-
|
|
252
|
-
const s = await state({ projectRoot: d, globalDir: g, cliVersion: "9.9.9" });
|
|
253
|
-
const pick = id => s.checks.find(c => c.id === id) || {};
|
|
254
|
-
const st = id => pick(id).status;
|
|
255
|
-
const dt = id => pick(id).detail;
|
|
256
|
-
|
|
257
|
-
eq("the runtime is discovered from runtimes.json", s.runtimes.map(r => r.id), ["cursor"]);
|
|
258
|
-
check("the core in .cohorte/<id>/ counts as installed", st("core") === "ok", dt("core"));
|
|
259
|
-
check("agents are looked for in .cursor/agents", st("agents") === "ok", dt("agents"));
|
|
260
|
-
check("gate-config is read from .cohorte, not .claude", st("gate") === "ok", dt("gate"));
|
|
261
|
-
check("artifact paths are named against the right state dir",
|
|
262
|
-
st("artifacts") === "ok" && !/\.claude/.test(dt("artifacts")), dt("artifacts"));
|
|
263
|
-
// Cursor's registration is a flat {command} under beforeShellExecution — read with Claude's
|
|
264
|
-
// matcher-group shape it looks absent, which is exactly the false red this guards.
|
|
265
|
-
check("the Cursor hook envelope is recognised", st("hooks") === "ok", dt("hooks"));
|
|
266
|
-
check("workflows are skipped, not reported missing",
|
|
267
|
-
st("workflows") === "skip" && /Cursor/.test(dt("workflows")), dt("workflows"));
|
|
268
|
-
check("nothing is reported broken on a healthy non-Claude install",
|
|
269
|
-
s.summary.bad === 0 && s.summary.warn === 0, JSON.stringify(s.summary));
|
|
270
|
-
}
|
|
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
|
-
|
|
312
|
-
// ── runtime.js — stale absolute registry paths (a cloned/moved bundled core) ─
|
|
313
|
-
// runtimes.json records install-time ABSOLUTE paths. A committed core cloned to
|
|
314
|
-
// another machine (or a checkout simply moved) still carries the original paths;
|
|
315
|
-
// taken verbatim, every check went red on a healthy install.
|
|
316
|
-
console.log("runtime.js — registry paths survive a clone/move");
|
|
317
|
-
{
|
|
318
|
-
const { layouts } = require(join(root, "lib/runtime.js"));
|
|
319
|
-
const d = scratch();
|
|
320
|
-
const core = join(d, ".claude");
|
|
321
|
-
mkdirSync(join(core, "pipeline"), { recursive: true });
|
|
322
|
-
const theirRoot = join("/Users", "somebody-else", "their-checkout");
|
|
323
|
-
writeFileSync(join(core, "pipeline", "runtimes.json"), JSON.stringify({
|
|
324
|
-
claude: {
|
|
325
|
-
label: "Claude Code", scope: "project", core_version: "9.9.9",
|
|
326
|
-
paths: {
|
|
327
|
-
core: join(theirRoot, ".claude"),
|
|
328
|
-
commands: join(theirRoot, ".claude", "commands"),
|
|
329
|
-
agents: join(theirRoot, ".claude", "agents"),
|
|
330
|
-
hooks_config: join(theirRoot, ".claude", "settings.json"),
|
|
331
|
-
state: ".claude",
|
|
332
|
-
},
|
|
333
|
-
},
|
|
334
|
-
}));
|
|
335
|
-
const [l] = layouts({ projectRoot: d, globalDir: join(d, "no-global") });
|
|
336
|
-
check("agents re-rooted onto the probed checkout",
|
|
337
|
-
l.agents === join(d, ".claude", "agents"), l.agents);
|
|
338
|
-
check("hooks config re-rooted too",
|
|
339
|
-
l.hooksConfig === join(d, ".claude", "settings.json"), l.hooksConfig);
|
|
340
|
-
// A path OUTSIDE the recorded project root (a genuine machine-local absolute,
|
|
341
|
-
// e.g. a global agents dir) must pass through untouched.
|
|
342
|
-
writeFileSync(join(core, "pipeline", "runtimes.json"), JSON.stringify({
|
|
343
|
-
claude: {
|
|
344
|
-
label: "Claude Code", scope: "project", core_version: "9.9.9",
|
|
345
|
-
paths: { core: join(theirRoot, ".claude"), agents: "/opt/shared-agents", state: ".claude" },
|
|
346
|
-
},
|
|
347
|
-
}));
|
|
348
|
-
const [l2] = layouts({ projectRoot: d, globalDir: join(d, "no-global") });
|
|
349
|
-
check("an absolute path outside the recorded root is untouched",
|
|
350
|
-
l2.agents === "/opt/shared-agents", l2.agents);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
for (const d of tmps) { try { rmSync(d, { recursive: true, force: true }); } catch { /* best effort */ } }
|
|
354
|
-
console.log("");
|
|
355
|
-
if (failures) { console.error(`test-lib: ${failures} failure(s)`); process.exit(1); }
|
|
356
|
-
console.log("test-lib: OK");
|