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.
- package/CHANGELOG.md +118 -0
- package/README.md +15 -10
- package/bin/cli.js +15 -2
- package/core/agents/review.md +4 -2
- package/core/commands/cohorte-brainstorm.md +5 -1
- package/core/commands/cohorte-build.md +3 -1
- package/core/commands/cohorte-doctor.md +5 -3
- package/core/commands/cohorte-fix.md +6 -5
- package/core/commands/cohorte-fleet.md +104 -0
- package/core/commands/cohorte-intake.md +92 -0
- package/core/commands/cohorte-patch.md +6 -1
- package/core/commands/cohorte-retro.md +85 -0
- package/core/commands/cohorte-review.md +85 -23
- package/core/commands/cohorte-ship.md +2 -1
- package/core/commands/cohorte-spec.md +1 -1
- package/core/hooks/gate.py +10 -4
- package/core/workflows/audit.js +14 -2
- package/core/workflows/loop.js +641 -0
- package/core/workflows/refactor.js +21 -8
- package/core/workflows/review.js +92 -12
- package/dashboard/dist/assets/{index-D1rsbLat.js → index-vtFc6Gyc.js} +12 -12
- package/dashboard/dist/index.html +1 -1
- package/dashboard/server/doctor.js +8 -2
- package/dashboard/server/index.js +6 -1
- package/dashboard/server/kanban.js +15 -4
- package/dashboard/server/metrics.js +8 -1
- package/dashboard/server/runtime.js +20 -1
- package/install.ps1 +16 -333
- package/install.sh +27 -297
- package/package.json +1 -1
- package/profile/SCHEMA.md +45 -14
- package/profile/cohorte.config.template.yaml +1 -1
- package/scripts/kanban-move.sh +15 -5
- package/scripts/new-feature.sh.template +8 -1
- package/scripts/preflight.sh +10 -2
- package/scripts/remove-feature.sh.template +3 -1
- package/scripts/test-dashboard.mjs +53 -5
- package/scripts/test-gate.mjs +6 -0
- package/scripts/test-workflows.mjs +415 -6
- 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
|
|
60
|
-
//
|
|
61
|
-
//
|
|
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
|
|
65
|
-
|
|
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 (!
|
|
88
|
-
fail("
|
|
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
|
-
|
|
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
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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) {
|