cohorte 2.10.1 → 3.0.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +155 -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 +50 -0
- package/dist/agent-host.mjs.map +1 -0
- package/dist/bundle-manifest.json +102 -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-D8rCVY33.mjs +84 -0
- package/dist/chunks/auth-D8rCVY33.mjs.map +1 -0
- package/dist/chunks/brainstorm-CU10T1tm.mjs +7 -0
- package/dist/chunks/brainstorm-CU10T1tm.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-Cr0hP0qm.mjs +7303 -0
- package/dist/chunks/compose-Cr0hP0qm.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-B0ra2-bm.mjs +295 -0
- package/dist/chunks/contract-B0ra2-bm.mjs.map +1 -0
- package/dist/chunks/contract-vYbwYr0Y.mjs +123 -0
- package/dist/chunks/contract-vYbwYr0Y.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-30Ciya_0.mjs +20 -0
- package/dist/chunks/discover-30Ciya_0.mjs.map +1 -0
- package/dist/chunks/doctor-DfCrKpfT.mjs +222 -0
- package/dist/chunks/doctor-DfCrKpfT.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-Okz4mlhW.mjs +30 -0
- package/dist/chunks/init-Okz4mlhW.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-DRoFUg53.mjs +19 -0
- package/dist/chunks/migrate-DRoFUg53.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-BfD785gd.mjs +26 -0
- package/dist/chunks/reconcile-BfD785gd.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/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-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-JxawBQBn.mjs +446 -0
- package/dist/chunks/src-JxawBQBn.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
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Validates the structural invariants of the cohorte core — the things a
|
|
3
|
-
// prose refactor can silently break. Run by CI on every push/PR; run it
|
|
4
|
-
// locally with `node scripts/validate-core.mjs`.
|
|
5
|
-
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
|
-
const { applyConditionals, loadRuntime } = createRequire(import.meta.url)('../core/adapter/render.js');
|
|
10
|
-
|
|
11
|
-
// fileURLToPath, not .pathname — on Windows the latter yields "/C:/…", which
|
|
12
|
-
// join() then resolves against the cwd drive ("C:\C:\…") and every read ENOENTs.
|
|
13
|
-
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
14
|
-
const errors = [];
|
|
15
|
-
const fail = (file, msg) => errors.push(`${file}: ${msg}`);
|
|
16
|
-
|
|
17
|
-
const read = (p) => readFileSync(join(root, p), "utf8");
|
|
18
|
-
const frontmatter = (text) => {
|
|
19
|
-
const m = text.match(/^---\n([\s\S]*?)\n---\n/);
|
|
20
|
-
return m ? m[1] : null;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// ── commands ────────────────────────────────────────────────────────────────
|
|
24
|
-
// Mechanical commands must pin model: sonnet (otherwise the lead's
|
|
25
|
-
// orchestration turn silently bills at the session model — Opus/Fable).
|
|
26
|
-
// Interactive commands must stay unpinned (they inherit on purpose).
|
|
27
|
-
const PINNED = ["cohorte-build", "cohorte-review", "cohorte-fix", "cohorte-ship",
|
|
28
|
-
"cohorte-audit", "cohorte-refactor", "cohorte-doctor", "cohorte-align-ds",
|
|
29
|
-
"cohorte-update-pipeline", "cohorte-fleet"];
|
|
30
|
-
const UNPINNED = ["cohorte-brainstorm", "cohorte-spec", "cohorte-init-pipeline", "cohorte-patch",
|
|
31
|
-
"cohorte-intake", "cohorte-retro"];
|
|
32
|
-
|
|
33
|
-
// Every command must carry the `cohorte-` prefix. This replaces the old RESERVED
|
|
34
|
-
// blocklist, which chased collisions one name at a time and always lagged: a command
|
|
35
|
-
// that collides with a Claude Code built-in is not overridden, it is SHADOWED — the
|
|
36
|
-
// built-in answers the slash, our file is never read, and the session confidently
|
|
37
|
-
// reports on a run that never happened. `/loop` did exactly that (Claude Code's own
|
|
38
|
-
// `/loop` runs a prompt on an interval) and went unnoticed until a user found the
|
|
39
|
-
// driver had never started; `/doctor` sat on a watchlist waiting to do the same.
|
|
40
|
-
// A blocklist can only forbid the collisions we already know about. The prefix makes
|
|
41
|
-
// the whole class unreachable, so this check is structural, not a list to maintain.
|
|
42
|
-
const PREFIX = "cohorte-";
|
|
43
|
-
|
|
44
|
-
for (const f of readdirSync(join(root, "core/commands"))) {
|
|
45
|
-
const path = `core/commands/${f}`;
|
|
46
|
-
if (!f.startsWith(PREFIX))
|
|
47
|
-
fail(path, `command name lacks the \`${PREFIX}\` prefix — an unprefixed command can be ` +
|
|
48
|
-
`SHADOWED by a Claude Code built-in of the same name (the built-in answers the slash ` +
|
|
49
|
-
`and this file is never read); rename it to ${PREFIX}${f}`);
|
|
50
|
-
const fm = frontmatter(read(path));
|
|
51
|
-
if (!fm) { fail(path, "missing or malformed YAML frontmatter"); continue; }
|
|
52
|
-
if (!/^description:\s*\S/m.test(fm)) fail(path, "frontmatter lacks a description");
|
|
53
|
-
const name = f.replace(/\.md$/, "");
|
|
54
|
-
const pinned = /^model:\s*sonnet\s*$/m.test(fm);
|
|
55
|
-
if (PINNED.includes(name) && !pinned)
|
|
56
|
-
fail(path, "mechanical command must carry `model: sonnet` in frontmatter");
|
|
57
|
-
if (UNPINNED.includes(name) && /^model:/m.test(fm))
|
|
58
|
-
fail(path, "interactive command must NOT pin a model (inherits the session)");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── fixed agents ────────────────────────────────────────────────────────────
|
|
62
|
-
// Every non-template agent needs name/tools/model, and every one must be
|
|
63
|
-
// ASSERTED by the ci.yml install dry-run (a new agent the CLI's copy rules miss
|
|
64
|
-
// would otherwise never reach an install and nothing would notice — the exact
|
|
65
|
-
// bug that motivated this check). The old form grepped install.sh/install.ps1
|
|
66
|
-
// for copy commands, but those scripts have been thin delegators to bin/cli.js
|
|
67
|
-
// since 2.2.0 — the text being matched was unreachable dead code, so the check
|
|
68
|
-
// passed vacuously. CI postconditions test the copy that actually runs.
|
|
69
|
-
const AGENT_MODEL = { review: "sonnet", release: "haiku",
|
|
70
|
-
"profile-reader": "haiku" };
|
|
71
|
-
const ciYmlText = existsSync(join(root, ".github/workflows/ci.yml"))
|
|
72
|
-
? read(".github/workflows/ci.yml") : "";
|
|
73
|
-
|
|
74
|
-
for (const f of readdirSync(join(root, "core/agents"))) {
|
|
75
|
-
const path = `core/agents/${f}`;
|
|
76
|
-
const text = read(path);
|
|
77
|
-
const fm = frontmatter(text);
|
|
78
|
-
if (!fm) { fail(path, "missing or malformed YAML frontmatter"); continue; }
|
|
79
|
-
if (f === "implementer.template.md") {
|
|
80
|
-
for (const ph of ["<SURFACE_AGENT>", "<SURFACE_LABEL>", "<SURFACE_PATH>",
|
|
81
|
-
"<SURFACE_TOOLS>", "<SURFACE_MODEL>", "<PROJECT_NAME>", "<SURFACE_CONVENTIONS>",
|
|
82
|
-
"<SURFACE_EXTRA_NEVER>", "<SURFACE_DESIGN_INPUT>", "<SURFACE_TDD_STEP1>"])
|
|
83
|
-
if (!text.includes(ph)) fail(path, `render placeholder ${ph} disappeared`);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
const name = f.replace(/\.md$/, "");
|
|
87
|
-
if (!/^name:\s*\S/m.test(fm)) fail(path, "frontmatter lacks name");
|
|
88
|
-
if (!/^tools:\s*\S/m.test(fm)) fail(path, "frontmatter lacks tools");
|
|
89
|
-
const want = AGENT_MODEL[name];
|
|
90
|
-
if (want && !new RegExp(`^model:\\s*${want}\\s*$`, "m").test(fm))
|
|
91
|
-
fail(path, `frontmatter must pin \`model: ${want}\``);
|
|
92
|
-
if (text.includes("<SURFACE_"))
|
|
93
|
-
fail(path, "unrendered <SURFACE_*> placeholder in a non-template agent");
|
|
94
|
-
if (ciYmlText && !ciYmlText.includes(`agents/${f}`))
|
|
95
|
-
fail(".github/workflows/ci.yml", `install dry-run never asserts .claude/agents/${f} — a fixed agent the CLI's copy rules miss would ship silently`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ── cross-references ────────────────────────────────────────────────────────
|
|
99
|
-
// Any `.claude/templates/<x>.md` referenced by a command or agent must exist
|
|
100
|
-
// in core/templates (they resolve to the installed copy of exactly that file).
|
|
101
|
-
const allDocs = [];
|
|
102
|
-
for (const dir of ["core/commands", "core/agents", "core/templates"])
|
|
103
|
-
for (const f of readdirSync(join(root, dir)))
|
|
104
|
-
if (f.endsWith(".md")) allDocs.push(`${dir}/${f}`);
|
|
105
|
-
|
|
106
|
-
for (const path of allDocs) {
|
|
107
|
-
const text = read(path);
|
|
108
|
-
for (const m of text.matchAll(/\.claude\/templates\/([a-z0-9.-]+\.md)/g)) {
|
|
109
|
-
if (!existsSync(join(root, "core/templates", m[1])))
|
|
110
|
-
fail(path, `references .claude/templates/${m[1]} which is not in core/templates/`);
|
|
111
|
-
}
|
|
112
|
-
for (const m of text.matchAll(/subagent_type:\s*(?:`|)([a-z-]+)(?:`|)/g)) {
|
|
113
|
-
const t = m[1];
|
|
114
|
-
if (["review", "release", "profile-reader"].includes(t)) continue;
|
|
115
|
-
if (t.startsWith("<")) continue; // <surface.agent> placeholder
|
|
116
|
-
if (!existsSync(join(root, "core/agents", `${t}.md`)))
|
|
117
|
-
fail(path, `dispatches subagent_type ${t} with no core/agents/${t}.md`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ── profile ─────────────────────────────────────────────────────────────────
|
|
122
|
-
for (const p of ["profile/PIPELINE.template.md", "profile/SCHEMA.md",
|
|
123
|
-
"profile/cohorte.config.template.yaml"])
|
|
124
|
-
if (!existsSync(join(root, p))) fail(p, "missing");
|
|
125
|
-
|
|
126
|
-
const tpl = applyConditionals(read("profile/PIPELINE.template.md"), loadRuntime('claude'));
|
|
127
|
-
if (/^\s*model:\s*inherit\b/m.test(tpl))
|
|
128
|
-
fail("profile/PIPELINE.template.md",
|
|
129
|
-
"a surfaces[] example pins `model: inherit` — examples must default to sonnet " +
|
|
130
|
-
"(inherit bills at the lead session's model)");
|
|
131
|
-
const codexTpl = applyConditionals(read('profile/PIPELINE.template.md'), loadRuntime('codex'));
|
|
132
|
-
if (/^\s*model:\s*(sonnet|haiku|opus)\b/m.test(codexTpl))
|
|
133
|
-
fail('profile/PIPELINE.template.md', 'Codex examples must not pin Anthropic model aliases');
|
|
134
|
-
|
|
135
|
-
// ── init-pipeline router steps ──────────────────────────────────────────────
|
|
136
|
-
const steps = join(root, "core/templates/steps/init-pipeline");
|
|
137
|
-
if (!existsSync(steps) || readdirSync(steps).length === 0)
|
|
138
|
-
fail("core/templates/steps/init-pipeline", "router step files missing/empty");
|
|
139
|
-
|
|
140
|
-
// ── no telemetry ────────────────────────────────────────────────────────────
|
|
141
|
-
// Telemetry was removed wholesale in 2.3.0: the shipped `telemetry-send.sh`, the
|
|
142
|
-
// per-phase pings, the consent question, the `telemetry:` config block, the collector
|
|
143
|
-
// endpoint. This check is the ratchet — it fails if any of it creeps back into the
|
|
144
|
-
// core, which is easy to do by copying an old command file that still chains a ping.
|
|
145
|
-
// Deliberately broad: the whole point is that there is nothing left to send with.
|
|
146
|
-
const NO_TELEMETRY = /telemetry|usage ping/i;
|
|
147
|
-
// One exemption, and it is the opposite of a regression: /cohorte-update-pipeline is what
|
|
148
|
-
// DELETES the leftover `telemetry:` block from configs seeded before 2.3.0, so it is the one
|
|
149
|
-
// file that must still name the thing. Narrow on purpose — a filename, not a pattern.
|
|
150
|
-
const TELEMETRY_SCRUBBER = "core/commands/cohorte-update-pipeline.md";
|
|
151
|
-
const walk = (dir) => readdirSync(join(root, dir), { withFileTypes: true }).flatMap((e) =>
|
|
152
|
-
e.isDirectory() ? walk(`${dir}/${e.name}`) : [`${dir}/${e.name}`]);
|
|
153
|
-
for (const dir of ["core/commands", "core/agents", "core/templates", "core/workflows"])
|
|
154
|
-
for (const rel of walk(dir)) {
|
|
155
|
-
if (!/\.(md|js)$/.test(rel) || rel === TELEMETRY_SCRUBBER) continue;
|
|
156
|
-
// Collapse whitespace first: prose wraps mid-phrase, and "usage\n ping" sat in a
|
|
157
|
-
// command file for two releases because this regex only saw one line at a time.
|
|
158
|
-
if (NO_TELEMETRY.test(read(rel).replace(/\s+/g, " ")))
|
|
159
|
-
fail(rel, "mentions telemetry — it was removed in 2.3.0; nothing may ping or ask for consent");
|
|
160
|
-
}
|
|
161
|
-
// …and the exempt file may only REMOVE it: naming a send/ping/consent path there is still a bug.
|
|
162
|
-
if (/usage ping|telemetry-send|consent/i.test(read(TELEMETRY_SCRUBBER)))
|
|
163
|
-
fail(TELEMETRY_SCRUBBER, "may reference the retired telemetry block only to delete it — no ping, sender or consent flow");
|
|
164
|
-
|
|
165
|
-
// ── kanban call sites ───────────────────────────────────────────────────────
|
|
166
|
-
// Every pipeline stage moves a card, and a stage that only *describes* the move
|
|
167
|
-
// ("move card #<id> → Building, no-op silently if no board") leaves the agent to
|
|
168
|
-
// decide whether a board exists — which it does by not looking. That is not
|
|
169
|
-
// hypothetical: a /cohorte-ship session declared "no kanban board configured",
|
|
170
|
-
// having opened neither the config nor PIPELINE.md, and a merged feature's card
|
|
171
|
-
// stayed in "Ready to build". `kanban-move.sh auto` moved resolution into the
|
|
172
|
-
// script; this keeps it there. Prose is not a call site — the literal invocation is.
|
|
173
|
-
const KANBAN_STAGES = ["brainstorm", "spec", "build", "review", "fix", "ship", "patch", "intake"];
|
|
174
|
-
for (const c of KANBAN_STAGES) {
|
|
175
|
-
const path = `core/commands/${PREFIX}${c}.md`;
|
|
176
|
-
const text = read(path);
|
|
177
|
-
if (!/kanban-move\.sh\s+auto\s+\S/.test(text))
|
|
178
|
-
fail(path, "moves a kanban card without a literal `kanban-move.sh auto …` call — the agent is left to infer whether a board exists");
|
|
179
|
-
// The one sentence that turns an unread config into a reported no-op. Match on
|
|
180
|
-
// unwrapped text: these live in `>` blockquotes and wrap mid-sentence.
|
|
181
|
-
const flat = text.replace(/\n>?\s*/g, " ");
|
|
182
|
-
if (!/without\s+running\s+it/i.test(flat))
|
|
183
|
-
fail(path, "no instruction to run the resolver before concluding there is no board");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ── shipped scripts + thin installers ───────────────────────────────────────
|
|
187
|
-
// Every shipped scripts/*.sh must be asserted by the ci.yml install dry-run —
|
|
188
|
-
// bin/cli.js copies by rule rather than by name, so only a postcondition on the
|
|
189
|
-
// copy that actually runs can catch a rule that misses a new script. Callers
|
|
190
|
-
// chain these with `|| true`, so a missing one is a silent no-op forever.
|
|
191
|
-
// A `<name>.sh` with a `<name>.sh.template` sibling is a locally-rendered artifact
|
|
192
|
-
// (this repo dogfoods its own /cohorte-init-pipeline), not a core asset — skip those.
|
|
193
|
-
const shipped = readdirSync(join(root, "scripts"));
|
|
194
|
-
for (const f of shipped.filter((f) => f.endsWith(".sh") && !shipped.includes(`${f}.template`)))
|
|
195
|
-
if (ciYmlText && !ciYmlText.includes(`pipeline/scripts/${f}`))
|
|
196
|
-
fail(".github/workflows/ci.yml", `install dry-run never asserts pipeline/scripts/${f} (a copy rule that misses it is a silent no-op at runtime)`);
|
|
197
|
-
// The shell installers are THIN DELEGATORS to bin/cli.js — their legacy copy-verbatim
|
|
198
|
-
// path was unreachable dead code from 2.2.0 (removed in 2.7.0), and its text was what
|
|
199
|
-
// this file's copy checks used to vacuously match. Pin the shape: both must hand off
|
|
200
|
-
// to the CLI and neither may grow its own copy logic back.
|
|
201
|
-
for (const [name, marker] of [["install.sh", "cp -R \"$src/core"], ["install.ps1", "Copy-Tree"]]) {
|
|
202
|
-
const text = read(name);
|
|
203
|
-
if (!text.includes("bin/cli.js") && !text.includes("bin\\cli.js"))
|
|
204
|
-
fail(name, "no longer delegates to bin/cli.js — the only renderer of runtime-neutral sources");
|
|
205
|
-
if (text.includes(marker))
|
|
206
|
-
fail(name, "carries its own core-copy logic again — installs must go through bin/cli.js (no shell renderer exists)");
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ── workflow scripts ────────────────────────────────────────────────────────
|
|
210
|
-
// core/workflows/*.js run inside the Claude Code Workflow runtime: an async
|
|
211
|
-
// function body with agent()/pipeline()/… injected, plus one `export const
|
|
212
|
-
// meta` line. Validate the syntax the same way the runtime parses it (plain
|
|
213
|
-
// `node --check` would reject the top-level return/await), and the invariants:
|
|
214
|
-
// a meta literal, phase 0 through profile-reader, and no Date.now()-family
|
|
215
|
-
// calls (they would break workflow resume).
|
|
216
|
-
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
217
|
-
const workflowsDir = join(root, "core/workflows");
|
|
218
|
-
if (!existsSync(workflowsDir) || readdirSync(workflowsDir).length === 0)
|
|
219
|
-
fail("core/workflows", "workflow scripts missing/empty");
|
|
220
|
-
else for (const f of readdirSync(workflowsDir)) {
|
|
221
|
-
if (!f.endsWith(".js")) continue;
|
|
222
|
-
const path = `core/workflows/${f}`;
|
|
223
|
-
const text = read(path);
|
|
224
|
-
if (!/^export const meta = \{/m.test(text))
|
|
225
|
-
fail(path, "missing the `export const meta = {…}` literal");
|
|
226
|
-
if (!text.includes("agentType: 'profile-reader'"))
|
|
227
|
-
fail(path, "phase 0 must read the profile via the profile-reader agent");
|
|
228
|
-
if (/\bDate\.now\(\)|\bMath\.random\(\)|new Date\(\)/.test(text))
|
|
229
|
-
fail(path, "Date.now()/Math.random()/new Date() are unavailable in workflow scripts");
|
|
230
|
-
// Prompts hand agents literal `<core>/…` paths; an agent can only resolve that
|
|
231
|
-
// token if the same script also spells out what <core> means. A bare token +
|
|
232
|
-
// `|| true` = the command fails silently and the ping/metrics never happen.
|
|
233
|
-
if (text.includes("<core>/") && !/<core> = /.test(text))
|
|
234
|
-
fail(path, "uses <core>/ paths in prompts without defining `<core> = …` anywhere");
|
|
235
|
-
try {
|
|
236
|
-
new AsyncFunction("agent", "parallel", "pipeline", "phase", "log", "args",
|
|
237
|
-
"budget", "workflow", text.replace(/^export const meta/m, "const meta"));
|
|
238
|
-
} catch (e) {
|
|
239
|
-
fail(path, `does not parse as a workflow body: ${e.message}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// Workflow meta.names and command names share one mental namespace — both are the
|
|
243
|
-
// "/cohorte-…" way a human asks for a phase. A meta.name that MATCHES a
|
|
244
|
-
// core/commands/<name>.md is a declared variant pair (same phase, two execution paths —
|
|
245
|
-
// review/audit/refactor, on purpose). A meta.name matching neither a command nor the
|
|
246
|
-
// deliberate command-less list below is a typo'd variant: the human asks for "the
|
|
247
|
-
// review workflow", the lead resolves the name, and a mismatched name runs nothing.
|
|
248
|
-
// cohorte-loop is command-less BY DECISION (SCHEMA.md §Workflows): when the Workflow
|
|
249
|
-
// runtime is unavailable it must refuse explicitly, never degrade to a conversational
|
|
250
|
-
// loop — so a core/commands/cohorte-loop.md appearing later is a regression, not an
|
|
251
|
-
// addition, and the check below this one pins that too.
|
|
252
|
-
const COMMANDLESS = ["cohorte-loop"];
|
|
253
|
-
if (existsSync(workflowsDir)) for (const f of readdirSync(workflowsDir)) {
|
|
254
|
-
if (!f.endsWith(".js")) continue;
|
|
255
|
-
const path = `core/workflows/${f}`;
|
|
256
|
-
const m = read(path).match(/name:\s*'([^']+)'/);
|
|
257
|
-
if (!m) { fail(path, "meta has no parseable `name: '…'`"); continue; }
|
|
258
|
-
const name = m[1];
|
|
259
|
-
if (!name.startsWith(PREFIX))
|
|
260
|
-
fail(path, `meta.name '${name}' lacks the \`${PREFIX}\` prefix`);
|
|
261
|
-
else if (!existsSync(join(root, "core/commands", `${name}.md`)) && !COMMANDLESS.includes(name))
|
|
262
|
-
fail(path, `meta.name '${name}' matches no core/commands/${name}.md and is not in the ` +
|
|
263
|
-
`deliberate command-less list — a variant pair must share the name exactly, or the ` +
|
|
264
|
-
`workflow-only decision must be recorded in COMMANDLESS`);
|
|
265
|
-
}
|
|
266
|
-
if (existsSync(join(root, "core/commands/cohorte-loop.md")))
|
|
267
|
-
fail("core/commands/cohorte-loop.md", "must not exist — /cohorte-loop is workflow-only " +
|
|
268
|
-
"(no conversational fallback, by decision; see core/workflows/loop.js header)");
|
|
269
|
-
|
|
270
|
-
// The workflows dir reaching installs is covered per-file by the ci.yml dry-run
|
|
271
|
-
// assertions checked just below (the shell installers delegate to bin/cli.js).
|
|
272
|
-
|
|
273
|
-
// A new workflow script must also be KNOWN to the things that check for it, or it
|
|
274
|
-
// ships and nothing notices when an installer stops copying it. This check exists
|
|
275
|
-
// because a workflow once shipped while three call sites still named only the
|
|
276
|
-
// three that preceded it.
|
|
277
|
-
const workflowNames = existsSync(workflowsDir)
|
|
278
|
-
? readdirSync(workflowsDir).filter((f) => f.endsWith(".js"))
|
|
279
|
-
: [];
|
|
280
|
-
const ci = existsSync(join(root, ".github/workflows/ci.yml")) ? read(".github/workflows/ci.yml") : "";
|
|
281
|
-
const libDoctor = read("lib/doctor.js");
|
|
282
|
-
for (const f of workflowNames) {
|
|
283
|
-
if (!ci.includes(`workflows/${f}`))
|
|
284
|
-
fail(".github/workflows/ci.yml", `install dry-run never asserts .claude/workflows/${f}`);
|
|
285
|
-
if (!libDoctor.includes(`'${f}'`))
|
|
286
|
-
fail("lib/doctor.js", `checkWorkflows() does not list ${f}`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ── every test suite must run in BOTH workflows ──────────────────────────────
|
|
290
|
-
// publish.yml re-runs the test suites under the comment "same gate as CI", because
|
|
291
|
-
// it has no dependency on the CI workflow's conclusion — a merge whose CI failed
|
|
292
|
-
// would otherwise still ship to npm. That only holds if the two lists agree, and
|
|
293
|
-
// they drift the moment a suite is added to one: test-loop.mjs landed in ci.yml and
|
|
294
|
-
// publish.yml kept publishing without it. Neither list is the source of truth —
|
|
295
|
-
// the directory is.
|
|
296
|
-
const ciYml = existsSync(join(root, ".github/workflows/ci.yml")) ? read(".github/workflows/ci.yml") : "";
|
|
297
|
-
const publishYml = existsSync(join(root, ".github/workflows/publish.yml"))
|
|
298
|
-
? read(".github/workflows/publish.yml") : "";
|
|
299
|
-
for (const f of readdirSync(join(root, "scripts")).filter((f) => /^test-.*\.mjs$/.test(f))) {
|
|
300
|
-
if (ciYml && !ciYml.includes(`scripts/${f}`))
|
|
301
|
-
fail(".github/workflows/ci.yml", `never runs scripts/${f} — a suite CI does not run is a suite that does not exist`);
|
|
302
|
-
if (publishYml && !publishYml.includes(`scripts/${f}`))
|
|
303
|
-
fail(".github/workflows/publish.yml", `never runs scripts/${f} — publish would ship past a failure that gate is meant to catch`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ── packaging: no build artifacts in the published tarball ──────────────────
|
|
307
|
-
// `.npmignore` is INERT under an explicit package.json `files` allowlist, so its
|
|
308
|
-
// `__pycache__/` rule never fired — a maintainer who had compiled gate.py shipped
|
|
309
|
-
// their machine's bytecode cache. The negations in `files` are what actually work.
|
|
310
|
-
const pkg = JSON.parse(read("package.json"));
|
|
311
|
-
for (const negation of ["!core/hooks/__pycache__", "!**/*.pyc"])
|
|
312
|
-
if (!(pkg.files || []).includes(negation))
|
|
313
|
-
fail("package.json", `\`files\` must carry the ${negation} negation (.npmignore cannot do this)`);
|
|
314
|
-
if (!read("bin/cli.js").includes("__pycache__"))
|
|
315
|
-
fail("bin/cli.js", "copyCore() never excludes __pycache__ from the hooks copy");
|
|
316
|
-
if (ciYmlText && !ciYmlText.includes("hooks/__pycache__"))
|
|
317
|
-
fail(".github/workflows/ci.yml", "install dry-run never asserts hooks/__pycache__ is absent — a compiled cache would ride into the user's .claude unnoticed");
|
|
318
|
-
|
|
319
|
-
// ── report ──────────────────────────────────────────────────────────────────
|
|
320
|
-
if (errors.length) {
|
|
321
|
-
console.error(`validate-core: ${errors.length} error(s)\n`);
|
|
322
|
-
for (const e of errors) console.error(" ✗ " + e);
|
|
323
|
-
process.exit(1);
|
|
324
|
-
}
|
|
325
|
-
console.log("validate-core: OK");
|