cohorte 2.8.0 → 2.10.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 (38) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +41 -46
  3. package/bin/cli.js +8 -41
  4. package/bin/report.js +2 -2
  5. package/core/commands/cohorte-brainstorm.md +5 -1
  6. package/core/commands/cohorte-fleet.md +103 -0
  7. package/core/commands/cohorte-intake.md +92 -0
  8. package/core/commands/cohorte-patch.md +6 -1
  9. package/core/commands/cohorte-retro.md +85 -0
  10. package/core/commands/cohorte-review.md +53 -1
  11. package/core/commands/cohorte-ship.md +2 -2
  12. package/core/hooks/gate.py +1 -1
  13. package/core/workflows/loop.js +27 -3
  14. package/core/workflows/review.js +13 -2
  15. package/{dashboard/server → lib}/doctor.js +3 -2
  16. package/{dashboard/server → lib}/runtime.js +1 -1
  17. package/{dashboard/server → lib}/versions.js +2 -2
  18. package/package.json +3 -8
  19. package/profile/SCHEMA.md +16 -8
  20. package/scripts/{test-dashboard.mjs → test-lib.mjs} +12 -222
  21. package/scripts/test-workflows.mjs +31 -2
  22. package/scripts/validate-core.mjs +7 -21
  23. package/dashboard/README.md +0 -71
  24. package/dashboard/dist/apple-touch-icon-180.png +0 -0
  25. package/dashboard/dist/assets/index-BZ_LQlEj.css +0 -1
  26. package/dashboard/dist/assets/index-DO3_nq2Q.js +0 -43
  27. package/dashboard/dist/favicon-16.png +0 -0
  28. package/dashboard/dist/favicon-32.png +0 -0
  29. package/dashboard/dist/favicon-48.png +0 -0
  30. package/dashboard/dist/icon-192.png +0 -0
  31. package/dashboard/dist/icon-512.png +0 -0
  32. package/dashboard/dist/index.html +0 -16
  33. package/dashboard/server/fleet.js +0 -133
  34. package/dashboard/server/index.js +0 -408
  35. package/dashboard/server/kanban.js +0 -169
  36. package/dashboard/server/metrics.js +0 -113
  37. package/dashboard/server/usage.js +0 -61
  38. /package/{dashboard/server → lib}/yaml.js +0 -0
@@ -0,0 +1,85 @@
1
+ ---
2
+ description: Mine the accumulated review findings across features for repeating patterns, and turn the ones the human ratifies into PIPELINE.md §Conventions rules the next build already follows.
3
+ argument-hint: [last <n> | all] (default: every feature with a report on disk)
4
+ ---
5
+
6
+ You are running the **retrospective**. Every review this pipeline has run left structured
7
+ residue on disk — verdicts, Remediation rounds, deferred backlog items. A finding that shows up
8
+ once is a bug; the same finding shape across features is a **missing rule**, and the pipeline
9
+ has a place for rules the implementers actually read: `PIPELINE.md` §Conventions, baked into
10
+ each surface agent at render time. This command closes that loop: findings → patterns →
11
+ ratified rules → re-rendered agents. The next build then never produces the finding again —
12
+ which is cheaper than any number of review rounds catching it.
13
+
14
+ > Read `PIPELINE.md` §`pipeline-profile` (`surfaces`, and skim the existing §Conventions
15
+ > stanzas — a rule that already exists is a finding about *enforcement*, not a missing rule)
16
+ > and `specs/_decisions.md` §Live. _Skip the re-read if already in context and unmodified._
17
+
18
+ ## 1. Collect — mechanical, redirected, no judgment yet
19
+
20
+ Scope: `all` (default), or `last <n>` features by spec mtime. In as few Bash calls as possible,
21
+ **always redirected to `specs/reports/retro-scan.txt` and grepped — never into your context**:
22
+
23
+ - `specs/reports/*.verdict.json` — per feature: `blocking_items` (normalized identities),
24
+ `severity`, per-surface counts. The freshest machine record, one per feature.
25
+ - Every non-`_` spec's `## Remediation` section — the **persistent** history (verdict.json is
26
+ overwritten per round; Remediation accumulates, and collapsed rounds still carry their
27
+ count line). Grep the item lines: `- [x?] <SEVERITY> · <file:line> · <kind> · <fix>`.
28
+ - `specs/refactor-backlog.md` — the `deferred:<id>` tagged items: debt reviews kept finding
29
+ but no feature owned.
30
+
31
+ A repo with fewer than two features' worth of residue ⇒ say the retro has nothing statistical
32
+ to stand on yet, name what exists, and stop — one feature's findings are that feature's story,
33
+ not a pattern.
34
+
35
+ ## 2. Patterns — what repeats, with the evidence attached
36
+
37
+ A **pattern** is a finding shape that recurs where recurrence means something:
38
+
39
+ - same `kind` × same surface across **≥ 2 features** (e.g. `security` findings on `backend`
40
+ twice running — the strongest signal there is);
41
+ - the same file/module named by findings from **≥ 2 features** (a hotspot no single fix loop
42
+ owns);
43
+ - the same *fix wording* family recurring (e.g. three "add the authz check" fixes = one
44
+ missing authorization convention);
45
+ - a `deferred:` cluster in one domain — debt the reviews keep re-discovering because no rule
46
+ makes the implementers avoid adding to it.
47
+
48
+ For each pattern, produce: the evidence lines **verbatim** (`<feature> · <severity> · <file> ·
49
+ <problem/fix>`), the count, and ONE drafted convention rule — **rule-shaped**: a sentence an
50
+ implementer can follow and a reviewer can test a diff against, placed under `### Shared` or the
51
+ owning `### Surface: <key>`. Not advice ("be careful with auth") — a rule ("every route under
52
+ `apps/api/src/routes/` calls `authorize()` before its handler; no exceptions without a spec
53
+ §5 note"). Skip any pattern the existing §Conventions already covers — report those separately
54
+ as **enforcement gaps** (the rule exists and reviews keep finding violations: that is input for
55
+ the human, not a new rule).
56
+
57
+ ## 3. Ratify — the human picks; nothing lands without them
58
+
59
+ Present a compact table: pattern → evidence count → drafted rule → target stanza. Then **stop
60
+ and ask** which to adopt (all / some / none — none is a fine outcome and says the rulebook fits
61
+ the codebase). Rules are standing law every future dispatch pays tokens to carry: the human
62
+ decides what becomes law, exactly as they freeze specs.
63
+
64
+ ## 4. Apply — and keep the baked slices honest
65
+
66
+ For each adopted rule, in this order:
67
+
68
+ 1. Append it to `PIPELINE.md` §Conventions under its stanza (create the `### Surface: <key>`
69
+ stanza if absent; keep it rule-shaped, one line each).
70
+ 2. **Re-render every affected surface agent** per SCHEMA.md §Rendering step 2 — the
71
+ `<SURFACE_CONVENTIONS>` slice is **baked at render time**, so a §Conventions edit without a
72
+ re-render produces the worst outcome available: reviewers (who read the prose live) enforce
73
+ a rule implementers (who carry the stale bake) have never seen, and every future review
74
+ round re-finds the pattern this retro just paid to close. If you cannot re-render on this
75
+ runtime, say so and route to `/cohorte-update-pipeline` (its reconcile step 2 re-renders) —
76
+ but then the rule is **pending**, and you say that too.
77
+ 3. Append ONE line per adopted rule to `specs/_decisions.md` §Live (SCHEMA.md §Decisions):
78
+ `- <date> · conventions · <rule, compressed> — because <kind>×<n> across <features> · retro`.
79
+ A retro rule is a standing decision; the journal is where the next `/cohorte-spec` learns it
80
+ without re-mining the reports.
81
+
82
+ In chat print ONLY: patterns found / adopted / skipped (one line each), enforcement gaps, which
83
+ agents were re-rendered (or the pending route), and the decisions lines appended. The evidence
84
+ stays in `specs/reports/retro-scan.txt`. **Recommend a `/clear`** — everything that matters is
85
+ now in `PIPELINE.md`, the rendered agents, and the journal.
@@ -12,7 +12,8 @@ You are the **lead**. Dispatch the review for feature **$ARGUMENTS**.
12
12
  > **Kanban** (SCHEMA.md §Kanban): run
13
13
  > `<core>/pipeline/scripts/kanban-move.sh auto $ARGUMENTS review`. `auto` resolves the board from the config itself and
14
14
  > exits 0 with a `kanban: <reason>` line when there is none — so **never decide "no board is
15
- > configured" without running it**.
15
+ > configured" without running it**. _(PR mode — `--pr` below — skips this entirely: an incoming
16
+ > PR is not a pipeline feature and has no card.)_
16
17
  >
17
18
  <!-- cohorte:if workflows -->
18
19
  > **Workflow variant** (opt-in — SCHEMA.md §Workflows): on Claude Code ≥ 2.1.154 with workflows
@@ -20,6 +21,57 @@ You are the **lead**. Dispatch the review for feature **$ARGUMENTS**.
20
21
  > This conversational path stays the default and the fallback; `/cohorte-doctor` shows which is available.
21
22
  <!-- cohorte:endif -->
22
23
 
24
+ ## PR mode — review an INCOMING pull request (`--pr <num>` or a PR URL)
25
+
26
+ If `$ARGUMENTS` contains `--pr <num>` (or a GitHub PR URL — extract the number), this is not a
27
+ pipeline feature: it is **someone else's work arriving**, reviewed with the same reviewers and the
28
+ same report format, without pretending it went through the pipeline. What that changes, explicitly:
29
+ **no spec** (nothing to check conformance against — reviewers run in audit mode: conventions +
30
+ correctness + security + TDD coverage only), **no kanban move**, **no verdict.json**, **no DoD
31
+ tick, no freshness stamp, no metrics line** — those certify pipeline features, and this is not one.
32
+ Requires `vcs.host: github` + `gh`; otherwise say so and stop.
33
+
34
+ 1. **Fetch into a throwaway worktree — never touch the current checkout.** One Bash call:
35
+ `gh pr view <num> --json title,baseRefName,headRefName,author` (relay the one-line summary), then
36
+ `git fetch -f origin pull/<num>/head:cohorte-pr-<num> <baseRefName> && git worktree add ../<repo>-pr-<num> cohorte-pr-<num>`
37
+ — `-f` so a branch left behind by a crashed earlier run updates instead of failing, and the
38
+ **base ref is fetched too**: the diff below is against `origin/<baseRefName>`, and a stale
39
+ local base blames the PR for every commit it merely hasn't rebased onto yet.
40
+ The human's tree, branch and stamps stay untouched; every later step runs `-C` that worktree.
41
+ 2. **Mechanical checks inside the worktree — the commands directly, NEVER `preflight.sh`.** The
42
+ script's whole point is stamping `<state>/preflight.ok` in the MAIN checkout (it resolves
43
+ through `git-common-dir` on purpose), so running it here would overwrite the human's stamp
44
+ with the PR tree's digest — greening pipeline review dispatches of a tree nobody preflighted,
45
+ then blocking them once the worktree is gone. Instead: install dependencies first
46
+ (`commands.install` — a bare `worktree add` has no `node_modules`), then run the profile's
47
+ typecheck/lint-quiet/test-quiet yourself, each appended to the worktree's
48
+ `specs/reports/pr-<num>.preflight.txt`, stopping at the first failure. **Red does NOT get
49
+ reviewers** — same economics as §0 (a reviewer on code that doesn't compile rediscovers what
50
+ `tsc` printed for free) — but unlike §0 it is not an abort: the mechanical failures ARE the
51
+ review (severity CRITICAL, kind `quality`, the failing command + last lines) — jump to
52
+ step 4. An *environmental* failure (install itself failed, a missing service) is reported as
53
+ "not verifiable here", never as a finding against the PR.
54
+ 3. **Stage + dispatch — §1's staging procedure and §2's dispatch, minus everything spec-shaped**
55
+ (there is no `specs/<id>.md` to confirm and no small-diff fast path — an unknown author's
56
+ diff always gets a full reviewer), with three substitutions: the diff is
57
+ `git diff origin/<baseRefName>...HEAD` (**three-dot** — only the PR's own commits, not the
58
+ base's drift), staged diffs and file paths are **absolute paths into the worktree**
59
+ (reviewers are stateless — they must not read the main checkout), and the dispatch template's
60
+ spec/contract slots read
61
+ `spec: none (incoming PR — audit mode: PIPELINE.md conventions + quality/security/TDD only,
62
+ skip spec conformance)` · `contract: none`. Roll call + merge per §3 (retry once, `unreviewed`
63
+ forbids a clean verdict), same capped REVIEW REPORT.
64
+ 4. **Stage the report to the MAIN checkout's `specs/reports/pr-<num>.md`** (it must outlive the
65
+ worktree), print the verdict + severity table + CRITICAL/security digests in chat. Then —
66
+ **always ask the human first, posting is outward-facing** — offer:
67
+ `gh pr comment <num> --body-file specs/reports/pr-<num>.md` (prefix the body with one line:
68
+ `Automated review — cohorte pipeline (advisory)`). Never post without the explicit go-ahead;
69
+ "no" leaves the report on disk and that is a complete outcome.
70
+ 5. **Teardown, always** (also after a red preflight or a dead reviewer):
71
+ `git worktree remove --force ../<repo>-pr-<num> && git branch -D cohorte-pr-<num>`.
72
+
73
+ Everything below this line is the normal pipeline-feature flow.
74
+
23
75
  ## 0. Deterministic pre-flight — no agents while red
24
76
 
25
77
  Run the profile's mechanical gates in ONE Bash call via the shipped script
@@ -98,8 +98,8 @@ watch, rather than letting the job go red.
98
98
  **Move the card to Shipped — required, and verify it actually moved.** Run
99
99
  `<core>/pipeline/scripts/kanban-move.sh auto $ARGUMENTS shipped --pr <num>`, which **appends the PR
100
100
  number** so the line reads `- [ ] <title> #$ARGUMENTS — PR #<num>`. Take `<num>` from the PR URL
101
- (`…/pull/13` ⇒ `13`); **always pass it when a PR was created** (the `gh` path) — it is what the
102
- dashboard turns into a PR link. If only a compare URL was emitted (no PR yet), drop `--pr`.
101
+ (`…/pull/13` ⇒ `13`); **always pass it when a PR was created** (the `gh` path) — it is what a
102
+ board reader turns into a PR link. If only a compare URL was emitted (no PR yet), drop `--pr`.
103
103
 
104
104
  Then **read the script's own output**, which is the verification: `moved #$ARGUMENTS -> Shipped
105
105
  (PR #<num>)` means done, and a `kanban: <reason>` line means the mirror is off and says why. Both are
@@ -23,7 +23,7 @@ and subagents spawned by the Workflow runtime alike. Workflow subagents run in
23
23
  acceptEdits regardless of the session's permission mode (their Write/Edit calls
24
24
  are auto-approved), but acceptEdits does NOT auto-approve Bash or Task, so this
25
25
  gate still sees and can block them. In bypassPermissions (headless `claude -p`,
26
- dashboard actions) there is no human to answer a prompt, so every `ask` match is
26
+ unattended workflow runs) there is no human to answer a prompt, so every `ask` match is
27
27
  escalated to a hard deny with the reason attached — a clear refusal beats a
28
28
  prompt that can never be answered.
29
29
 
@@ -211,6 +211,18 @@ const ITEMS = {
211
211
  },
212
212
  }
213
213
 
214
+ // Token accounting — the one thing the conversational path cannot do (a lead cannot
215
+ // read a subagent's token count) and a workflow can: budget.spent() is the runtime's
216
+ // own output-token counter for this turn. Deltas between marks give an approximate
217
+ // per-phase cost; the review CHILD workflow stamps its own review-phase line, so the
218
+ // loop marks AROUND the child to keep build/fix deltas from double-counting it.
219
+ const spent = () => {
220
+ try {
221
+ const v = (budget && typeof budget.spent === 'function') ? budget.spent() : 0
222
+ return Number.isFinite(v) ? v : 0 // a NaN here would poison every metrics JSON downstream
223
+ } catch { return 0 }
224
+ }
225
+
214
226
  // kanban-move.sh is optional infrastructure — every prompt that runs it defines <core>
215
227
  // and tolerates its absence, per SCHEMA.md §Kanban.
216
228
  const CORE_DEF = '(<core> = .claude if .claude/pipeline/scripts/kanban-move.sh exists, else ~/.claude — ' +
@@ -403,6 +415,7 @@ const writeState = async ({ label, phaseName, outcome, reason, status: st, kanba
403
415
  if (metrics) steps.push(
404
416
  `${steps.length + 1}. Append ONE line to ${METRICS_PATH}: ` +
405
417
  `{"ts":"<ISO>","feature":"${feature}","phase":"${metrics.phase}","seconds":$(($(date +%s)-${clock || 0})),` +
418
+ `"tokens":${metrics.tokens || 0},` +
406
419
  `"surfaces":${JSON.stringify(metrics.surfaces)}}` + (metrics.buildJson
407
420
  ? `\n${steps.length + 2}. Write to specs/reports/${feature}.build.json (overwrite): ` +
408
421
  `{"id":"${feature}","phase":"build","ts":"<ISO>","surfaces":${JSON.stringify(metrics.surfaces)},"dead":${JSON.stringify(metrics.dead)}}`
@@ -434,6 +447,7 @@ const close = async (outcome, reason, extra = {}) => {
434
447
  return {
435
448
  id: feature, outcome, reason, rounds: history.length,
436
449
  blocking: history.length ? history[history.length - 1].blocking : null,
450
+ tokens: spent(), // approx output tokens for the whole run (runtime counter)
437
451
  history, report: `specs/reports/${feature}.md`,
438
452
  ...(closed ? {} : { stateWarning: 'the closing state agent died — loop.json/status/kanban may not reflect this outcome' }),
439
453
  ...extra,
@@ -446,6 +460,7 @@ const close = async (outcome, reason, extra = {}) => {
446
460
  // build.json is stale, absent, or carries dead[], the loop builds (a full re-dispatch —
447
461
  // implementers are idempotent against a frozen contract, the same property fix rounds
448
462
  // already rely on).
463
+ let mark = spent()
449
464
  if (fresh(facts.build) && !(facts.build.dead || []).length) {
450
465
  log('build.json is fresh with no dead surfaces — skipping the build phase, entering at review')
451
466
  await writeState({ label: 'state:enter', phaseName: 'review', status: 'in-progress', kanban: 'review' })
@@ -463,7 +478,7 @@ if (fresh(facts.build) && !(facts.build.dead || []).length) {
463
478
  await writeState({
464
479
  label: 'state:built', phaseName: dead.length ? 'build' : 'review',
465
480
  metrics: {
466
- phase: 'build', buildJson: true, dead,
481
+ phase: 'build', buildJson: true, dead, tokens: Math.max(0, spent() - mark),
467
482
  surfaces: Object.fromEntries(built.map(b => [b.key, b.handoff == null ? 'dead' : 'ok'])),
468
483
  },
469
484
  })
@@ -479,6 +494,7 @@ if (fresh(facts.build) && !(facts.build.dead || []).length) {
479
494
  while (true) {
480
495
  phase('Review')
481
496
  await writeState({ label: `state:round-${round}`, phaseName: 'review', status: 'in-progress', kanban: 'review' })
497
+ const roundMark = spent()
482
498
  let review
483
499
  try {
484
500
  review = await workflow('cohorte-review', { feature })
@@ -489,9 +505,16 @@ while (true) {
489
505
  })
490
506
  }
491
507
 
508
+ // The review child stamps its own review-phase metrics line; marking here keeps the
509
+ // fix phase's delta clean of it. History carries the round's review cost.
510
+ mark = spent()
492
511
  const verdict = decide(review, lastKey, round, maxRounds)
493
512
  if (review && typeof review.blocking === 'number') {
494
- history.push({ round, blocking: review.blocking, fingerprint: hash16((review.blockingItems || []).join('\n')) })
513
+ history.push({
514
+ round, blocking: review.blocking,
515
+ fingerprint: hash16((review.blockingItems || []).join('\n')),
516
+ tokens: Math.max(0, spent() - roundMark),
517
+ })
495
518
  }
496
519
 
497
520
  if (verdict.outcome === 'ship') {
@@ -587,7 +610,8 @@ while (true) {
587
610
  // exactly the batch worth recording (SCHEMA.md §Dead agents), so it lands before
588
611
  // the dead-implementers abort, with the dead surfaces named.
589
612
  await writeState({ label: `state:fixed-${round}`, phaseName: 'review', metrics: {
590
- phase: 'fix', surfaces: Object.fromEntries(fixed.map(f => [f.key, f.handoff == null ? 'dead' : 'ok'])),
613
+ phase: 'fix', tokens: Math.max(0, spent() - mark),
614
+ surfaces: Object.fromEntries(fixed.map(f => [f.key, f.handoff == null ? 'dead' : 'ok'])),
591
615
  } })
592
616
  if (fixDead.length) {
593
617
  // Their items stay `- [ ]` — a dead agent never ticks a box (/cohorte-fix §3), and
@@ -42,6 +42,17 @@ const ARGS = (() => {
42
42
  }
43
43
  return args && typeof args === 'object' ? args : {}
44
44
  })()
45
+ // Approximate cost of this run from the runtime's own output-token counter — the one
46
+ // figure the conversational path cannot record (a lead cannot read a subagent's token
47
+ // count; SCHEMA.md §Measuring cost). Sampled at start, delta stamped into the metrics line.
48
+ const spentNow = () => {
49
+ try {
50
+ const v = (budget && typeof budget.spent === 'function') ? budget.spent() : 0
51
+ return Number.isFinite(v) ? v : 0 // a NaN here would poison the metrics JSON downstream
52
+ } catch { return 0 }
53
+ }
54
+ const spentStart = spentNow()
55
+
45
56
  const isSlug = s => typeof s === 'string' && /^[A-Za-z0-9._-]+$/.test(s)
46
57
  const feature = ARGS.feature
47
58
  if (!feature) throw new Error('cohorte-review needs args = {feature: "<feature_id>"}')
@@ -384,10 +395,10 @@ const staging = await agent(
384
395
  : 'the blocking list is empty, so <FP> is the empty string "":\n') +
385
396
  `${verdictJson}\n` +
386
397
  // Per-surface verdicts (not the merged one stamped on every row — one BLOCK used to
387
- // mark ALL surfaces failed on the dashboard), and dead reviewers logged as "dead"
398
+ // mark ALL surfaces failed), and dead reviewers logged as "dead"
388
399
  // per SCHEMA.md §Dead agents — an incomplete batch is the batch worth recording.
389
400
  `3. Append one line to $(dirname "$(git rev-parse --git-common-dir)")/.claude/pipeline-metrics.jsonl: ` +
390
- `{"ts":"<ISO now>","feature":"${feature}","phase":"review","seconds":0,"surfaces":{${
401
+ `{"ts":"<ISO now>","feature":"${feature}","phase":"review","seconds":0,"tokens":${Math.max(0, spentNow() - spentStart)},"surfaces":{${
391
402
  results.map(r => `"${r.key}":"${r.report.verdict}:${r.kept.length}"`)
392
403
  .concat(unreviewed.map(k => `"${k}":"dead"`)).join(',')}}}\n` +
393
404
  // Deferred findings must land in the backlog on EVERY verdict — parked only on a
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
- // Programmatic port of the /cohorte-doctor checks (core/commands/doctor.md), for the dashboard.
2
+ // Programmatic port of the /cohorte-doctor checks (core/commands/cohorte-doctor.md), so
3
+ // `cohorte doctor` can report without a coding agent in the loop.
3
4
  // Read-only: inspects files only. Checks that need a live process (MCP connectivity,
4
5
  // git worktree state, DesignSync) are reported as `skip` with a note — the node server
5
6
  // can't run them, and honest "not checked here" beats a false green.
@@ -247,7 +248,7 @@ function gateRegs(settingsPath, event) {
247
248
 
248
249
  function checkHooks(projectRoot, globalDir, installMode, all) {
249
250
  // A registration can exist without a discoverable core — a repo whose core lives in a global
250
- // dir the dashboard was not pointed at, or one predating `runtimes.json`. Falling through to
251
+ // dir the caller was not pointed at, or one predating `runtimes.json`. Falling through to
251
252
  // "no runtime installed" there would hide a real double-registration, so assume the Claude
252
253
  // layout: before the adapter it was the only one, and it is the only one whose hook can be
253
254
  // registered outside its own core dir.
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  // Which coding agent(s) this project is wired for, and where each one keeps its files.
3
3
  //
4
- // Before 2.2.0 every path in the dashboard was `.claude/…`. That is now one layout of five:
4
+ // Before 2.2.0 every path here was `.claude/…`. That is now one layout of five:
5
5
  // a repo driven from Cursor keeps its core in `.cohorte/cursor/`, its agents in
6
6
  // `.cursor/agents/` and its gate registration in `.cursor/hooks.json`. Checking the Claude
7
7
  // paths there reports a perfectly healthy install as broken — "no core", "no rendered agent",
@@ -30,12 +30,12 @@ function pointerAt(projectRoot) {
30
30
 
31
31
  // Latest published version — registry fetch first, `npm view` as a fallback (it uses the
32
32
  // user's configured registry/proxy, which works where a raw fetch may be blocked). Cached
33
- // briefly so the dashboard's poll doesn't hammer the network. null only if both fail.
33
+ // briefly so repeated calls don't hammer the network. null only if both fail.
34
34
  let _cache = { value: null, at: 0 };
35
35
  const CACHE_MS = 5 * 60 * 1000;
36
36
  // Failures are cached too, briefly. Without this an offline machine paid the FULL
37
37
  // 5s fetch timeout + 8s `npm view` timeout on every call — and /api/fleet calls
38
- // this once per tracked project, so the dashboard's 15s poll never finished.
38
+ // this once per tracked project, so a batch of probes never finished.
39
39
  const FAIL_CACHE_MS = 60 * 1000;
40
40
 
41
41
  async function fetchRegistry(timeoutMs = 5000) {
package/package.json CHANGED
@@ -1,26 +1,21 @@
1
1
  {
2
2
  "name": "cohorte",
3
- "version": "2.8.0",
3
+ "version": "2.10.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"
7
7
  },
8
- "scripts": {
9
- "build:dashboard": "npm --prefix dashboard/app ci && npm --prefix dashboard/app run build",
10
- "prepack": "npm run build:dashboard"
11
- },
12
8
  "files": [
13
9
  "bin",
10
+ "lib",
14
11
  "core",
15
12
  "profile",
16
13
  "scripts",
17
14
  "!scripts/new-feature.sh",
18
15
  "!scripts/remove-feature.sh",
16
+ "!scripts/demo",
19
17
  "!core/hooks/__pycache__",
20
18
  "!**/*.pyc",
21
- "dashboard/server",
22
- "dashboard/dist",
23
- "dashboard/README.md",
24
19
  "install.sh",
25
20
  "install.ps1",
26
21
  "CHANGELOG.md"
package/profile/SCHEMA.md CHANGED
@@ -176,7 +176,7 @@ the **main checkout's** `<state>/pipeline-metrics.jsonl` (gitignored) — one JS
176
176
  and `/cohorte-fix`.
177
177
  **`surfaces` keys are surface keys, nothing else** — run-level facts go in their own top-level
178
178
  fields. Anything put inside `surfaces` is read
179
- as a surface: the dashboard renders it as a row in the per-surface table and scores a non-`ok`
179
+ as a surface: `cohorte doctor` renders it as a row in the per-surface table and scores a non-`ok`
180
180
  value as that surface failing. Always the main checkout, never the feature worktree (which dies at teardown while
181
181
  metrics must accumulate across features) — resolve from anywhere with
182
182
  `$(dirname "$(git rev-parse --git-common-dir)")/<state>/pipeline-metrics.jsonl`. Read it before
@@ -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. `cohorte metrics` 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 %,
@@ -226,7 +231,7 @@ storing a bare `pnpm test` as the thing agents execute; `/cohorte-update-pipelin
226
231
  ## Spec status — the lifecycle state machine
227
232
 
228
233
  A spec's front-matter `status` is not a label, it is the pipeline's **state**: every command routes on
229
- it, the dashboard boards on it, and the kanban backfill maps it to a column. Six states:
234
+ it, `cohorte specs` boards on it, and the kanban backfill maps it to a column. Six states:
230
235
 
231
236
  | status | meaning | written by | who may build it |
232
237
  | --- | --- | --- | --- |
@@ -352,18 +357,21 @@ 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).
363
371
  - **Never read by implementers or reviewers.** They work from the frozen contract, which already tells
364
372
  them what to do; shipping them the rationale would cost `surfaces × dispatches` tokens per feature
365
373
  for a fact they cannot act on. This is what keeps the journal cheap enough to be worth having.
366
- - The `_` prefix is load-bearing: `/cohorte-doctor`, the dashboard spec scanner and the kanban backfill all skip
374
+ - The `_` prefix is load-bearing: `/cohorte-doctor`, the `cohorte specs` scanner and the kanban backfill all skip
367
375
  `specs/_*.md`, so the journal is never mistaken for a spec (no phantom card, no bogus stage).
368
376
 
369
377
  ## Preflight — the deterministic phase gate
@@ -618,7 +626,7 @@ notes a human writes as sub-bullets under an Ideas card are seed context for `/c
618
626
  the trailing `%% kanban:settings … %%` block or the `kanban-plugin: board` front-matter.
619
627
 
620
628
  Once shipped, `/cohorte-ship` appends the **PR number** to the card — `- [ ] <title> #<feature_id> — PR #<num>`.
621
- The bare `#<num>` is what the dashboard renders as a clickable link to the GitHub PR, so `/cohorte-ship` always
629
+ The bare `#<num>` is what a board reader renders as a clickable link to the GitHub PR, so `/cohorte-ship` always
622
630
  writes it when a PR was actually created.
623
631
 
624
632
  **Move a card (the core op).** One call — the script does resolution AND the move outside the