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.
- package/CHANGELOG.md +72 -0
- package/README.md +41 -46
- package/bin/cli.js +8 -41
- package/bin/report.js +2 -2
- package/core/commands/cohorte-brainstorm.md +5 -1
- package/core/commands/cohorte-fleet.md +103 -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/commands/cohorte-ship.md +2 -2
- package/core/hooks/gate.py +1 -1
- package/core/workflows/loop.js +27 -3
- package/core/workflows/review.js +13 -2
- package/{dashboard/server → lib}/doctor.js +3 -2
- package/{dashboard/server → lib}/runtime.js +1 -1
- package/{dashboard/server → lib}/versions.js +2 -2
- package/package.json +3 -8
- package/profile/SCHEMA.md +16 -8
- package/scripts/{test-dashboard.mjs → test-lib.mjs} +12 -222
- package/scripts/test-workflows.mjs +31 -2
- package/scripts/validate-core.mjs +7 -21
- package/dashboard/README.md +0 -71
- package/dashboard/dist/apple-touch-icon-180.png +0 -0
- package/dashboard/dist/assets/index-BZ_LQlEj.css +0 -1
- package/dashboard/dist/assets/index-DO3_nq2Q.js +0 -43
- package/dashboard/dist/favicon-16.png +0 -0
- package/dashboard/dist/favicon-32.png +0 -0
- package/dashboard/dist/favicon-48.png +0 -0
- package/dashboard/dist/icon-192.png +0 -0
- package/dashboard/dist/icon-512.png +0 -0
- package/dashboard/dist/index.html +0 -16
- package/dashboard/server/fleet.js +0 -133
- package/dashboard/server/index.js +0 -408
- package/dashboard/server/kanban.js +0 -169
- package/dashboard/server/metrics.js +0 -113
- package/dashboard/server/usage.js +0 -61
- /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
|
|
102
|
-
|
|
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
|
package/core/hooks/gate.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/core/workflows/loop.js
CHANGED
|
@@ -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({
|
|
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',
|
|
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
|
package/core/workflows/review.js
CHANGED
|
@@ -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
|
|
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),
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
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.
|
|
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. `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,
|
|
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> · <
|
|
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).
|
|
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
|
|
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
|
|
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
|