cohorte 2.7.0 → 2.9.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 (40) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +15 -10
  3. package/bin/cli.js +15 -2
  4. package/core/agents/review.md +4 -2
  5. package/core/commands/cohorte-brainstorm.md +5 -1
  6. package/core/commands/cohorte-build.md +3 -1
  7. package/core/commands/cohorte-doctor.md +5 -3
  8. package/core/commands/cohorte-fix.md +6 -5
  9. package/core/commands/cohorte-fleet.md +104 -0
  10. package/core/commands/cohorte-intake.md +92 -0
  11. package/core/commands/cohorte-patch.md +6 -1
  12. package/core/commands/cohorte-retro.md +85 -0
  13. package/core/commands/cohorte-review.md +85 -23
  14. package/core/commands/cohorte-ship.md +2 -1
  15. package/core/commands/cohorte-spec.md +1 -1
  16. package/core/hooks/gate.py +10 -4
  17. package/core/workflows/audit.js +14 -2
  18. package/core/workflows/loop.js +641 -0
  19. package/core/workflows/refactor.js +21 -8
  20. package/core/workflows/review.js +92 -12
  21. package/dashboard/dist/assets/{index-D1rsbLat.js → index-vtFc6Gyc.js} +12 -12
  22. package/dashboard/dist/index.html +1 -1
  23. package/dashboard/server/doctor.js +8 -2
  24. package/dashboard/server/index.js +6 -1
  25. package/dashboard/server/kanban.js +15 -4
  26. package/dashboard/server/metrics.js +8 -1
  27. package/dashboard/server/runtime.js +20 -1
  28. package/install.ps1 +16 -333
  29. package/install.sh +27 -297
  30. package/package.json +1 -1
  31. package/profile/SCHEMA.md +45 -14
  32. package/profile/cohorte.config.template.yaml +1 -1
  33. package/scripts/kanban-move.sh +15 -5
  34. package/scripts/new-feature.sh.template +8 -1
  35. package/scripts/preflight.sh +10 -2
  36. package/scripts/remove-feature.sh.template +3 -1
  37. package/scripts/test-dashboard.mjs +53 -5
  38. package/scripts/test-gate.mjs +6 -0
  39. package/scripts/test-workflows.mjs +415 -6
  40. package/scripts/validate-core.mjs +68 -34
@@ -24,8 +24,9 @@ const frontmatter = (text) => {
24
24
  // Interactive commands must stay unpinned (they inherit on purpose).
25
25
  const PINNED = ["cohorte-build", "cohorte-review", "cohorte-fix", "cohorte-ship",
26
26
  "cohorte-audit", "cohorte-refactor", "cohorte-doctor", "cohorte-align-ds",
27
- "cohorte-update-pipeline"];
28
- const UNPINNED = ["cohorte-brainstorm", "cohorte-spec", "cohorte-init-pipeline", "cohorte-patch"];
27
+ "cohorte-update-pipeline", "cohorte-fleet"];
28
+ const UNPINNED = ["cohorte-brainstorm", "cohorte-spec", "cohorte-init-pipeline", "cohorte-patch",
29
+ "cohorte-intake", "cohorte-retro"];
29
30
 
30
31
  // Every command must carry the `cohorte-` prefix. This replaces the old RESERVED
31
32
  // blocklist, which chased collisions one name at a time and always lagged: a command
@@ -56,13 +57,17 @@ for (const f of readdirSync(join(root, "core/commands"))) {
56
57
  }
57
58
 
58
59
  // ── fixed agents ────────────────────────────────────────────────────────────
59
- // Every non-template agent needs name/tools/model, and must be shipped by
60
- // both installers (a new agent that install.sh doesn't copy never reaches
61
- // a global install the exact bug that motivated this check).
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.
62
67
  const AGENT_MODEL = { review: "sonnet", release: "haiku",
63
68
  "profile-reader": "haiku" };
64
- const installSh = read("install.sh");
65
- const installPs1 = read("install.ps1");
69
+ const ciYmlText = existsSync(join(root, ".github/workflows/ci.yml"))
70
+ ? read(".github/workflows/ci.yml") : "";
66
71
 
67
72
  for (const f of readdirSync(join(root, "core/agents"))) {
68
73
  const path = `core/agents/${f}`;
@@ -84,10 +89,8 @@ for (const f of readdirSync(join(root, "core/agents"))) {
84
89
  fail(path, `frontmatter must pin \`model: ${want}\``);
85
90
  if (text.includes("<SURFACE_"))
86
91
  fail(path, "unrendered <SURFACE_*> placeholder in a non-template agent");
87
- if (!installSh.includes(`core/agents/${f}`))
88
- fail("install.sh", `does not copy core/agents/${f} (copy_fixed_agents)`);
89
- if (!installPs1.includes(`core\\agents\\${f}`))
90
- fail("install.ps1", `does not copy core\\agents\\${f} (Copy-FixedAgents)`);
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`);
91
94
  }
92
95
 
93
96
  // ── cross-references ────────────────────────────────────────────────────────
@@ -145,7 +148,9 @@ const walk = (dir) => readdirSync(join(root, dir), { withFileTypes: true }).flat
145
148
  for (const dir of ["core/commands", "core/agents", "core/templates", "core/workflows"])
146
149
  for (const rel of walk(dir)) {
147
150
  if (!/\.(md|js)$/.test(rel) || rel === TELEMETRY_SCRUBBER) continue;
148
- if (NO_TELEMETRY.test(read(rel)))
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, " ")))
149
154
  fail(rel, "mentions telemetry — it was removed in 2.3.0; nothing may ping or ask for consent");
150
155
  }
151
156
  // …and the exempt file may only REMOVE it: naming a send/ping/consent path there is still a bug.
@@ -160,7 +165,7 @@ if (/usage ping|telemetry-send|consent/i.test(read(TELEMETRY_SCRUBBER)))
160
165
  // having opened neither the config nor PIPELINE.md, and a merged feature's card
161
166
  // stayed in "Ready to build". `kanban-move.sh auto` moved resolution into the
162
167
  // script; this keeps it there. Prose is not a call site — the literal invocation is.
163
- const KANBAN_STAGES = ["brainstorm", "spec", "build", "review", "fix", "ship", "patch"];
168
+ const KANBAN_STAGES = ["brainstorm", "spec", "build", "review", "fix", "ship", "patch", "intake"];
164
169
  for (const c of KANBAN_STAGES) {
165
170
  const path = `core/commands/${PREFIX}${c}.md`;
166
171
  const text = read(path);
@@ -173,22 +178,28 @@ for (const c of KANBAN_STAGES) {
173
178
  fail(path, "no instruction to run the resolver before concluding there is no board");
174
179
  }
175
180
 
176
- // ── shipped scripts ─────────────────────────────────────────────────────────
177
- // Every scripts/*.sh must be copied by BOTH shell installers. Callers chain these
178
- // with `|| true`, so one an installer forgets is a silent no-op forever — no kanban
179
- // card moves, no error. CI is the only place this is loud.
180
- // The third installer, bin/cli.js (what the `cohorte` CLI runs), copies by rule rather
181
- // than by name, so grepping for filenames can't see it — ci.yml dry-runs it into a
182
- // scratch HOME and asserts the same postconditions instead. Both are needed: this
183
- // check catches a forgotten name, that one catches a drifted rule.
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.
184
186
  // A `<name>.sh` with a `<name>.sh.template` sibling is a locally-rendered artifact
185
187
  // (this repo dogfoods its own /cohorte-init-pipeline), not a core asset — skip those.
186
- const installers = { "install.sh": read("install.sh"), "install.ps1": read("install.ps1") };
187
188
  const shipped = readdirSync(join(root, "scripts"));
188
189
  for (const f of shipped.filter((f) => f.endsWith(".sh") && !shipped.includes(`${f}.template`)))
189
- for (const [name, src] of Object.entries(installers))
190
- if (!src.includes(`scripts/${f}`) && !src.includes(`scripts\\${f}`))
191
- fail(name, `never copies scripts/${f} into pipeline/scripts/ (silent no-op at runtime)`);
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
+ }
192
203
 
193
204
  // ── workflow scripts ────────────────────────────────────────────────────────
194
205
  // core/workflows/*.js run inside the Claude Code Workflow runtime: an async
@@ -223,12 +234,36 @@ else for (const f of readdirSync(workflowsDir)) {
223
234
  fail(path, `does not parse as a workflow body: ${e.message}`);
224
235
  }
225
236
  }
226
- // Both shell installers must copy the workflows dir (bin/cli.js copies by rule,
227
- // covered by the ci.yml dry-run).
228
- if (!installSh.includes("core/workflows"))
229
- fail("install.sh", "does not copy core/workflows (copy_core)");
230
- if (!installPs1.includes("core\\workflows"))
231
- fail("install.ps1", "does not copy core\\workflows (Copy-Core)");
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).
232
267
 
233
268
  // A new workflow script must also be KNOWN to the things that check for it, or it
234
269
  // ships and nothing notices when an installer stops copying it. This check exists
@@ -286,11 +321,10 @@ const pkg = JSON.parse(read("package.json"));
286
321
  for (const negation of ["!core/hooks/__pycache__", "!**/*.pyc"])
287
322
  if (!(pkg.files || []).includes(negation))
288
323
  fail("package.json", `\`files\` must carry the ${negation} negation (.npmignore cannot do this)`);
289
- for (const [name, src] of Object.entries(installers))
290
- if (!src.includes("__pycache__"))
291
- fail(name, "never scrubs hooks/__pycache__ — cp -R would carry it into the user's .claude");
292
324
  if (!read("bin/cli.js").includes("__pycache__"))
293
325
  fail("bin/cli.js", "copyCore() never excludes __pycache__ from the hooks copy");
326
+ if (ciYmlText && !ciYmlText.includes("hooks/__pycache__"))
327
+ 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");
294
328
 
295
329
  // ── report ──────────────────────────────────────────────────────────────────
296
330
  if (errors.length) {