cohorte 2.8.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 +47 -0
- package/README.md +4 -1
- package/core/commands/cohorte-brainstorm.md +5 -1
- 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 +53 -1
- package/core/workflows/loop.js +27 -3
- package/core/workflows/review.js +12 -1
- package/dashboard/dist/assets/{index-DO3_nq2Q.js → index-vtFc6Gyc.js} +12 -12
- package/dashboard/dist/index.html +1 -1
- package/dashboard/server/metrics.js +8 -1
- package/package.json +1 -1
- package/profile/SCHEMA.md +12 -4
- package/scripts/test-dashboard.mjs +11 -4
- package/scripts/test-workflows.mjs +31 -2
- package/scripts/validate-core.mjs +4 -3
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="icon" type="image/png" sizes="16x16" href="./favicon-16.png" />
|
|
8
8
|
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon-180.png" />
|
|
9
9
|
<title>cohorte · dashboard</title>
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-vtFc6Gyc.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="./assets/index-BZ_LQlEj.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -33,6 +33,10 @@ function parseBatches(raw) {
|
|
|
33
33
|
// (never inside) `surfaces` — carry them through for the aggregate.
|
|
34
34
|
batches.push({
|
|
35
35
|
ts: e.ts || '', feature: String(e.feature), phase: String(e.phase), seconds,
|
|
36
|
+
// Approximate output tokens for the batch — stamped only by the workflow paths
|
|
37
|
+
// (loop.js / review.js), which read the runtime's own counter; conversational
|
|
38
|
+
// lines simply lack the field and aggregate as 0.
|
|
39
|
+
tokens: Number(e.tokens) || 0,
|
|
36
40
|
surfaces: e.surfaces, rounds: e.rounds, smoke: e.smoke,
|
|
37
41
|
});
|
|
38
42
|
} else if (e.surface) {
|
|
@@ -69,6 +73,7 @@ function aggregate(batches) {
|
|
|
69
73
|
feature: b.feature,
|
|
70
74
|
firstTs: b.ts, lastTs: b.ts,
|
|
71
75
|
totalSeconds: 0,
|
|
76
|
+
totalTokens: 0,
|
|
72
77
|
fixRounds: 0,
|
|
73
78
|
phases: {}, // phase → { seconds, rounds }
|
|
74
79
|
surfaces: {}, // surface → { phase → latest result }, plus failure count
|
|
@@ -81,8 +86,10 @@ function aggregate(batches) {
|
|
|
81
86
|
if (b.ts && (!f.firstTs || b.ts < f.firstTs)) f.firstTs = b.ts;
|
|
82
87
|
if (b.ts && b.ts > f.lastTs) f.lastTs = b.ts;
|
|
83
88
|
f.totalSeconds += b.seconds;
|
|
84
|
-
|
|
89
|
+
f.totalTokens += b.tokens || 0;
|
|
90
|
+
const ph = f.phases[b.phase] || (f.phases[b.phase] = { seconds: 0, rounds: 0, tokens: 0 });
|
|
85
91
|
ph.seconds += b.seconds;
|
|
92
|
+
ph.tokens += b.tokens || 0;
|
|
86
93
|
ph.rounds += 1;
|
|
87
94
|
if (b.phase === 'fix') f.fixRounds += 1;
|
|
88
95
|
for (const [key, result] of Object.entries(b.surfaces)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code, Codex CLI, Cursor, Gemini CLI and OpenCode — install the core, run /cohorte-init-pipeline, and it adapts to your project's stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cohorte": "bin/cli.js"
|
package/profile/SCHEMA.md
CHANGED
|
@@ -185,8 +185,13 @@ proposing a split: split the surface that actually dominates wall-clock, not the
|
|
|
185
185
|
## Measuring cost — what's slow vs what's expensive
|
|
186
186
|
|
|
187
187
|
`pipeline-metrics.jsonl` records **wall-clock seconds** per phase batch (§Specialization) — it tells you
|
|
188
|
-
what's SLOW.
|
|
189
|
-
|
|
188
|
+
what's SLOW. Tokens are recorded only where they can be read honestly: the **workflow paths**
|
|
189
|
+
(`loop.js`, `review.js`) stamp an approximate `tokens` field per batch from the runtime's own
|
|
190
|
+
counter (`budget.spent()` deltas), and the loop's return carries a per-round breakdown in its
|
|
191
|
+
`history`. The **conversational** commands still record none — a lead cannot reliably read a
|
|
192
|
+
subagent's token count, and a guessed number is worse than a missing one. The dashboard sums
|
|
193
|
+
whatever is stamped (a token-less line aggregates as 0, rendered as absent, never as "free").
|
|
194
|
+
For exact spend, use Claude Code's own accounting:
|
|
190
195
|
|
|
191
196
|
- **`/cost`** (built-in, zero setup) — reports per-**subagent** and per-**slash-command** share of your usage
|
|
192
197
|
over the last 24 h / 7 d (e.g. _"Top subagents: frontend 7 %, backend 4 % · Top skills: /cohorte-build 1 %,
|
|
@@ -352,11 +357,14 @@ project has *decided*. Without somewhere for those, every `/cohorte-spec` re-dis
|
|
|
352
357
|
`specs/_decisions.md` (from `core/templates/decisions.template.md`) is that place, deliberately small:
|
|
353
358
|
|
|
354
359
|
- **Append-only, one line per decision, ≤ ~160 chars:**
|
|
355
|
-
`- <YYYY-MM-DD> · <area> · <decision> — because <reason> · <
|
|
360
|
+
`- <YYYY-MM-DD> · <area> · <decision> — because <reason> · <origin>`, where `<origin>` is the
|
|
361
|
+
`feature_id` that decided it — or the originating command (`retro`) when no single feature owns
|
|
362
|
+
it. Reversal never edits a line:
|
|
356
363
|
append a superseding one (`· supersedes <date> <area>`) and move the old one to `## Superseded`. When
|
|
357
364
|
`## Live` passes ~100 lines, sweep the superseded ones down.
|
|
358
365
|
- **Written by** `/cohorte-spec` at freeze (the decisions that outlive the feature — typically 0–3 lines, and
|
|
359
|
-
zero is a normal outcome)
|
|
366
|
+
zero is a normal outcome), `/cohorte-build` §1.5 when it adds or splits a surface, and
|
|
367
|
+
`/cohorte-retro` §4 when the human ratifies a convention rule (one line per adopted rule).
|
|
360
368
|
- **Read by the deciding stages only** — `/cohorte-brainstorm` (so the panel argues about the idea, not about
|
|
361
369
|
settled ground), `/cohorte-spec` (so a new spec does not silently un-decide something), `/cohorte-audit` (standing
|
|
362
370
|
decisions are part of the rulebook it audits against).
|
|
@@ -464,12 +464,19 @@ console.log("doctor.js — a non-Claude runtime layout");
|
|
|
464
464
|
check("nothing is reported broken on a healthy non-Claude install",
|
|
465
465
|
s.summary.bad === 0 && s.summary.warn === 0, JSON.stringify(s.summary));
|
|
466
466
|
|
|
467
|
-
// The metrics sink follows `<state>` too
|
|
467
|
+
// The metrics sink follows `<state>` too — and workflow-stamped `tokens` aggregate
|
|
468
|
+
// per feature/phase while token-less conversational lines read as 0, not NaN.
|
|
468
469
|
writeFileSync(join(d, ".cohorte", "pipeline-metrics.jsonl"),
|
|
469
470
|
JSON.stringify({ ts: "2026-01-01T00:00:00Z", feature: "f", phase: "build", seconds: 10,
|
|
470
|
-
surfaces: { api: "ok" } }) + "\n"
|
|
471
|
-
|
|
472
|
-
|
|
471
|
+
tokens: 12000, surfaces: { api: "ok" } }) + "\n" +
|
|
472
|
+
JSON.stringify({ ts: "2026-01-01T01:00:00Z", feature: "f", phase: "review", seconds: 5,
|
|
473
|
+
surfaces: { api: "SHIP:0" } }) + "\n");
|
|
474
|
+
const m = metrics({ projectRoot: d, globalDir: g });
|
|
475
|
+
check("metrics are read from the runtime's state dir", m.batches === 2);
|
|
476
|
+
check("workflow tokens aggregate; token-less lines count as 0",
|
|
477
|
+
m.features[0].totalTokens === 12000 && m.features[0].phases.build.tokens === 12000
|
|
478
|
+
&& m.features[0].phases.review.tokens === 0,
|
|
479
|
+
JSON.stringify(m.features[0] && { t: m.features[0].totalTokens, p: m.features[0].phases }));
|
|
473
480
|
}
|
|
474
481
|
|
|
475
482
|
// ── runtime.js — stale absolute registry paths (a cloned/moved bundled core) ─
|
|
@@ -52,7 +52,7 @@ const finding = (over = {}) => ({
|
|
|
52
52
|
// every agent call, and an optional `wf(name, args)` stub in place of nested
|
|
53
53
|
// workflow() calls (loop.js runs the review workflow as a child). Returns
|
|
54
54
|
// { result, calls, prompts } — prompts keyed by label, for byte-identity asserts.
|
|
55
|
-
async function run(script, reply, args = { feature: "feat-x" }, wf) {
|
|
55
|
+
async function run(script, reply, args = { feature: "feat-x" }, wf, budgetStub) {
|
|
56
56
|
const text = readFileSync(join(root, "core/workflows", script), "utf8")
|
|
57
57
|
.replace(/^export const meta/m, "const meta");
|
|
58
58
|
const calls = [];
|
|
@@ -80,11 +80,18 @@ async function run(script, reply, args = { feature: "feat-x" }, wf) {
|
|
|
80
80
|
"agent", "parallel", "pipeline", "phase", "log", "args", "budget", "workflow", text);
|
|
81
81
|
const result = await fn(
|
|
82
82
|
agent, parallel, pipeline, () => {}, () => {}, args,
|
|
83
|
-
{ total: null, spent: () => 0, remaining: () => Infinity },
|
|
83
|
+
budgetStub || { total: null, spent: () => 0, remaining: () => Infinity },
|
|
84
84
|
wf || (async () => {}));
|
|
85
85
|
return { result, calls, prompts };
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// A budget stub whose spent() grows with every agent/workflow call — what the runtime's
|
|
89
|
+
// counter does — so token deltas in the scripts come out non-zero and orderable.
|
|
90
|
+
const tickingBudget = () => {
|
|
91
|
+
let n = 0;
|
|
92
|
+
return { total: null, spent: () => (n += 1000), remaining: () => Infinity };
|
|
93
|
+
};
|
|
94
|
+
|
|
88
95
|
// A reply table keyed by label prefix; the first matching prefix wins.
|
|
89
96
|
const replier = table => (prompt, opts) => {
|
|
90
97
|
const label = opts.label || "";
|
|
@@ -715,6 +722,28 @@ const SHIP_CLEAN = { verdict: "SHIP", blocking: 0, blockingItems: [], unreviewed
|
|
|
715
722
|
check("dead ingest agent ⇒ abort/ingest-died, items never invented",
|
|
716
723
|
result.reason === "ingest-died", result.reason);
|
|
717
724
|
}
|
|
725
|
+
{
|
|
726
|
+
// Token accounting: the run's history and metrics lines carry approximate output
|
|
727
|
+
// tokens from budget.spent() — the figure the conversational path cannot record.
|
|
728
|
+
const wf = async () => reviewOf();
|
|
729
|
+
const { result, prompts } = await run("loop.js", loopReply(loopFacts()),
|
|
730
|
+
{ feature: "feat-x" }, wf, tickingBudget());
|
|
731
|
+
check("loop: history rounds carry a tokens figure",
|
|
732
|
+
result.history.length > 0 && result.history.every(h => typeof h.tokens === "number" && h.tokens > 0),
|
|
733
|
+
JSON.stringify(result.history));
|
|
734
|
+
check("loop: build metrics line carries tokens",
|
|
735
|
+
/"phase":"build".*"tokens":[1-9]/.test(prompts["state:built"] || ""),
|
|
736
|
+
(prompts["state:built"] || "").slice(-200));
|
|
737
|
+
check("loop: the run total is returned", typeof result.tokens === "number" && result.tokens > 0,
|
|
738
|
+
String(result.tokens));
|
|
739
|
+
let stagePrompt = "";
|
|
740
|
+
await run("review.js", (prompt, opts) => {
|
|
741
|
+
if (opts.label === "stage-report") { stagePrompt = prompt; return "done"; }
|
|
742
|
+
return replier([["review:", { verdict: "SHIP", findings: [] }], ...BASE_REVIEW])(prompt, opts);
|
|
743
|
+
}, { feature: "feat-x" }, undefined, tickingBudget());
|
|
744
|
+
check("review: metrics line carries tokens",
|
|
745
|
+
/"phase":"review".*"tokens":[1-9]/.test(stagePrompt), stagePrompt.slice(-200));
|
|
746
|
+
}
|
|
718
747
|
{
|
|
719
748
|
// workflow() unavailable (no runtime / review.js not installed) ⇒ explicit refusal,
|
|
720
749
|
// never a conversational fallback.
|
|
@@ -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
|
|
@@ -164,7 +165,7 @@ if (/usage ping|telemetry-send|consent/i.test(read(TELEMETRY_SCRUBBER)))
|
|
|
164
165
|
// having opened neither the config nor PIPELINE.md, and a merged feature's card
|
|
165
166
|
// stayed in "Ready to build". `kanban-move.sh auto` moved resolution into the
|
|
166
167
|
// script; this keeps it there. Prose is not a call site — the literal invocation is.
|
|
167
|
-
const KANBAN_STAGES = ["brainstorm", "spec", "build", "review", "fix", "ship", "patch"];
|
|
168
|
+
const KANBAN_STAGES = ["brainstorm", "spec", "build", "review", "fix", "ship", "patch", "intake"];
|
|
168
169
|
for (const c of KANBAN_STAGES) {
|
|
169
170
|
const path = `core/commands/${PREFIX}${c}.md`;
|
|
170
171
|
const text = read(path);
|