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.
@@ -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-DO3_nq2Q.js"></script>
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
- const ph = f.phases[b.phase] || (f.phases[b.phase] = { seconds: 0, rounds: 0 });
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.8.0",
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. It deliberately does NOT record tokens: the lead can't reliably read a subagent's token count
189
- to log it. For what's EXPENSIVE, use Claude Code's own accounting:
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> · <feature_id>`. Reversal never edits a line:
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) and `/cohorte-build` §1.5 when it adds or splits a surface.
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
- check("metrics are read from the runtime's state dir",
472
- metrics({ projectRoot: d, globalDir: g }).batches === 1);
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);