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.
Files changed (213) 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 +154 -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 +2187 -0
  41. package/dist/agent-host.mjs.map +1 -0
  42. package/dist/bundle-manifest.json +104 -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-CYOh60KG.mjs +93 -0
  50. package/dist/chunks/auth-CYOh60KG.mjs.map +1 -0
  51. package/dist/chunks/brainstorm-BwKA6yZP.mjs +7 -0
  52. package/dist/chunks/brainstorm-BwKA6yZP.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-Dvxunpto.mjs +7307 -0
  60. package/dist/chunks/compose-Dvxunpto.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-BkMjU1kR.mjs +295 -0
  64. package/dist/chunks/contract-BkMjU1kR.mjs.map +1 -0
  65. package/dist/chunks/contract-bQ9FJl4N.mjs +123 -0
  66. package/dist/chunks/contract-bQ9FJl4N.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-eS01-me1.mjs +21 -0
  72. package/dist/chunks/discover-eS01-me1.mjs.map +1 -0
  73. package/dist/chunks/doctor-B-H2UK5e.mjs +222 -0
  74. package/dist/chunks/doctor-B-H2UK5e.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-BUnzIkeE.mjs +63 -0
  80. package/dist/chunks/init-BUnzIkeE.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-DybihKzZ.mjs +33 -0
  86. package/dist/chunks/migrate-DybihKzZ.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-Bhy3IIYo.mjs +50 -0
  96. package/dist/chunks/reconcile-Bhy3IIYo.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/scan-8Gy_B6kI.mjs +122 -0
  110. package/dist/chunks/scan-8Gy_B6kI.mjs.map +1 -0
  111. package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
  112. package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
  113. package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
  114. package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
  115. package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
  116. package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
  117. package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
  118. package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
  119. package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
  120. package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
  121. package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
  122. package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
  123. package/dist/chunks/src-CId6kMsY.mjs +1689 -0
  124. package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
  125. package/dist/chunks/src-CfAOde7C.mjs +5403 -0
  126. package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
  127. package/dist/chunks/src-CmFjh4M0.mjs +928 -0
  128. package/dist/chunks/src-CmFjh4M0.mjs.map +1 -0
  129. package/dist/chunks/src-Cu3TxNob.mjs +783 -0
  130. package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
  131. package/dist/chunks/src-DcU4IXMe.mjs +117 -0
  132. package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
  133. package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
  134. package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
  135. package/dist/chunks/status-SMjiOMYy.mjs +29 -0
  136. package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
  137. package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
  138. package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
  139. package/dist/chunks/update-BXIppsbi.mjs +16 -0
  140. package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
  141. package/dist/cli.mjs +175 -0
  142. package/dist/cli.mjs.map +1 -0
  143. package/package.json +17 -32
  144. package/CHANGELOG.md +0 -1461
  145. package/bin/cli.js +0 -817
  146. package/bin/report.js +0 -141
  147. package/core/adapter/render.js +0 -415
  148. package/core/agents/implementer.template.md +0 -142
  149. package/core/agents/profile-reader.md +0 -50
  150. package/core/agents/release.md +0 -58
  151. package/core/agents/review.md +0 -179
  152. package/core/commands/cohorte-align-ds.md +0 -33
  153. package/core/commands/cohorte-audit.md +0 -58
  154. package/core/commands/cohorte-brainstorm.md +0 -70
  155. package/core/commands/cohorte-build.md +0 -219
  156. package/core/commands/cohorte-doctor.md +0 -194
  157. package/core/commands/cohorte-fix.md +0 -80
  158. package/core/commands/cohorte-fleet.md +0 -103
  159. package/core/commands/cohorte-init-pipeline.md +0 -31
  160. package/core/commands/cohorte-intake.md +0 -92
  161. package/core/commands/cohorte-patch.md +0 -118
  162. package/core/commands/cohorte-refactor.md +0 -59
  163. package/core/commands/cohorte-retro.md +0 -85
  164. package/core/commands/cohorte-review.md +0 -270
  165. package/core/commands/cohorte-ship.md +0 -122
  166. package/core/commands/cohorte-spec.md +0 -101
  167. package/core/commands/cohorte-update-pipeline.md +0 -165
  168. package/core/hooks/gate.py +0 -588
  169. package/core/runtimes/claude.json +0 -74
  170. package/core/runtimes/codex.json +0 -85
  171. package/core/runtimes/cursor.json +0 -76
  172. package/core/runtimes/gemini.json +0 -76
  173. package/core/runtimes/opencode.json +0 -73
  174. package/core/templates/agent-handoff.md +0 -34
  175. package/core/templates/brainstorm-return.md +0 -36
  176. package/core/templates/decisions.template.md +0 -42
  177. package/core/templates/design-brief.md +0 -44
  178. package/core/templates/patch.template.md +0 -86
  179. package/core/templates/pr-body.md +0 -25
  180. package/core/templates/review-feedback.md +0 -38
  181. package/core/templates/spec.template.md +0 -87
  182. package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
  183. package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
  184. package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
  185. package/core/templates/steps/init-pipeline/04-write-render.md +0 -145
  186. package/core/templates/steps/init-pipeline/05-report.md +0 -12
  187. package/core/workflows/audit.js +0 -240
  188. package/core/workflows/loop.js +0 -641
  189. package/core/workflows/refactor.js +0 -276
  190. package/core/workflows/review.js +0 -456
  191. package/install.ps1 +0 -103
  192. package/install.sh +0 -114
  193. package/lib/doctor.js +0 -534
  194. package/lib/runtime.js +0 -139
  195. package/lib/versions.js +0 -149
  196. package/lib/yaml.js +0 -129
  197. package/profile/PIPELINE.template.md +0 -259
  198. package/profile/SCHEMA.md +0 -735
  199. package/profile/cohorte.config.template.yaml +0 -43
  200. package/scripts/assert-gate-hook.mjs +0 -75
  201. package/scripts/kanban-move.sh +0 -313
  202. package/scripts/metrics/collect.mjs +0 -531
  203. package/scripts/metrics/prices.json +0 -42
  204. package/scripts/new-feature.sh.template +0 -98
  205. package/scripts/preflight.sh +0 -124
  206. package/scripts/remove-feature.sh.template +0 -56
  207. package/scripts/test-adapter.mjs +0 -437
  208. package/scripts/test-gate.mjs +0 -443
  209. package/scripts/test-kanban.mjs +0 -304
  210. package/scripts/test-lib.mjs +0 -356
  211. package/scripts/test-metrics.mjs +0 -156
  212. package/scripts/test-workflows.mjs +0 -758
  213. 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");