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