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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,53 @@ short, user-facing, most recent first. One `## <version> — <YYYY-MM-DD>` secti
|
|
|
7
7
|
> They are history and are deliberately not rewritten — every command gained a `cohorte-` prefix
|
|
8
8
|
> in 2.0.0.
|
|
9
9
|
|
|
10
|
+
## 2.9.0 — 2026-08-22
|
|
11
|
+
|
|
12
|
+
- **`/cohorte-intake` — the door before the doors.** Work has always entered the pipeline
|
|
13
|
+
pre-distilled by a human: a ticket became a `/cohorte-patch` paste, an email became a
|
|
14
|
+
brainstorm idea, by hand. Intake does the distillation: paste anything that arrives and it
|
|
15
|
+
triages — bug ⇒ the exact structure patch interviews for (inferred repro steps labeled as
|
|
16
|
+
such); feature ⇒ a brainstorm seed whose open-questions list is the panel's agenda, with any
|
|
17
|
+
`_decisions.md` line it contradicts named; noise ⇒ says so and stops, because inventing a
|
|
18
|
+
spec from noise costs a whole pipeline run downstream. The distillate lands on disk and on
|
|
19
|
+
the kanban's Ideas column; freezing anything stays the human's move.
|
|
20
|
+
|
|
21
|
+
- **`/cohorte-retro` — the pipeline learns from its own findings.** Every review leaves
|
|
22
|
+
structured residue (verdict.json, the specs' Remediation history, the deferred backlog) that
|
|
23
|
+
nothing ever read back. Retro mines it for patterns — the same finding kind on the same
|
|
24
|
+
surface across features, a hotspot module, a recurring fix family — and turns each into ONE
|
|
25
|
+
rule-shaped §Conventions line the human ratifies. Adopted rules re-render the affected
|
|
26
|
+
surface agents (the conventions slice is baked at render time — a rule without a re-render
|
|
27
|
+
is one reviewers enforce and implementers never saw) and land in the decisions journal.
|
|
28
|
+
Findings → rules → the next build never produces the finding: cheaper than any number of
|
|
29
|
+
review rounds catching it.
|
|
30
|
+
|
|
31
|
+
- **`/cohorte-fleet` — parallel features get a flight controller.** The worktree isolation
|
|
32
|
+
existed; the coordination lived in your head. `plan` builds the feature×surface overlap
|
|
33
|
+
matrix from the specs themselves (contract dependencies ⇒ merge order; same-tree writes ⇒
|
|
34
|
+
serialize or drop one), provisions the worktrees, and prints one launch line per feature.
|
|
35
|
+
`status` is one row per feature ending in the single next action. `sync` does the post-merge
|
|
36
|
+
sweep everyone forgets — rebase every survivor, report conflicts verbatim to their owner's
|
|
37
|
+
session, and say out loud that a rebase invalidates the freshness stamp. What it will never
|
|
38
|
+
do is spawn the work headless: that is the retired 2.2.0 driver's grave, and each feature's
|
|
39
|
+
loop keeps running in its own supervised session.
|
|
40
|
+
|
|
41
|
+
- **`/cohorte-review --pr <num>` — the pipeline reviews incoming work.** Same reviewers, same
|
|
42
|
+
report, none of the pipeline's certifications: the PR is fetched into a throwaway worktree
|
|
43
|
+
(your checkout untouched), reviewed in audit mode (no spec to conform to), and a red
|
|
44
|
+
preflight makes the mechanical failures the review instead of wasting reviewers on code that
|
|
45
|
+
doesn't compile. Posting the report as a PR comment always asks first — it is outward-facing
|
|
46
|
+
— and declining leaves the report on disk as a complete outcome.
|
|
47
|
+
|
|
48
|
+
- **What a feature costs, finally on the dashboard.** The workflow paths now stamp an
|
|
49
|
+
approximate `tokens` field into their metrics lines from the runtime's own counter
|
|
50
|
+
(`budget.spent()` deltas — the figure a conversational lead cannot read), the loop's history
|
|
51
|
+
carries cost per round, and the dashboard shows `~Nk tok` per feature and per phase. Marks
|
|
52
|
+
are placed around the review child so build/fix deltas never double-count it; token-less
|
|
53
|
+
conversational lines aggregate as absent, never as "free".
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
10
57
|
## 2.8.0 — 2026-08-22
|
|
11
58
|
|
|
12
59
|
- **`/cohorte-loop` is back — as a workflow, which is the whole point.** The 2.2.0 driver was
|
package/README.md
CHANGED
|
@@ -301,15 +301,18 @@ so a CLI pinned at an old version cannot quietly re-lay an old core.
|
|
|
301
301
|
| Command | Role |
|
|
302
302
|
| -------------------- | ------------------------------------------------------------------------------------- |
|
|
303
303
|
| `/cohorte-init-pipeline` | Detect stack → interview → generate the profile + agents. Run once per project. |
|
|
304
|
+
| `/cohorte-intake [paste]` | Triage anything that arrives (ticket, email, trace, thread) into a `/cohorte-patch` handoff or a brainstorm seed — staged to disk, carded in Ideas. |
|
|
304
305
|
| `/cohorte-brainstorm` | Interactive persona panel that pressure-tests a feature idea. |
|
|
305
306
|
| `/cohorte-spec` | Freeze the feature spec + contract into `specs/<id>.md` (UI features also get a standalone design brief at `specs/design/<id>.md`). Also applies review returns. |
|
|
306
307
|
| `/cohorte-patch [bug]` | Bug-fix entry: triage a bug and freeze a ~60-line patch spec (`specs/patch-<slug>.md`) whose regression test replaces the contract. Then the normal `/cohorte-build → review → ship`. |
|
|
307
308
|
| `/cohorte-build <id>` | Readiness gate on the frozen spec, then the lead authors the contract and dispatches one implementer per surface in parallel. |
|
|
308
|
-
| `/cohorte-review <id>` | Read-only review agents (one per touched surface, parallel) audit the diff vs the spec; out-of-scope findings go to the refactor backlog. |
|
|
309
|
+
| `/cohorte-review <id>` | Read-only review agents (one per touched surface, parallel) audit the diff vs the spec; out-of-scope findings go to the refactor backlog. `--pr <num>` reviews an **incoming** GitHub PR in a throwaway worktree and offers to post the report as a comment. |
|
|
309
310
|
| `/cohorte-fix <id>` | Apply a review report: remediation into the spec, re-dispatch only the surfaces with findings. |
|
|
311
|
+
| `/cohorte-fleet plan\|status\|sync` | Fly several features in parallel: overlap matrix + merge order, one worktree per feature, live status, post-merge rebase sweep. Coordination only — never headless execution. |
|
|
310
312
|
| `/cohorte-ship <id>` | Release agent commits, pushes, opens the PR; watches CI; proposes worktree teardown. |
|
|
311
313
|
| `/cohorte-audit [path]` | Prioritized refactor backlog for existing code. |
|
|
312
314
|
| `/cohorte-refactor <domain>` | Apply the backlog for one surface, TDD-first. |
|
|
315
|
+
| `/cohorte-retro [last n]` | Mine the accumulated review findings for patterns; ratified ones become §Conventions rules and the surface agents are re-rendered — the next build never produces the finding. |
|
|
313
316
|
| `/cohorte-align-ds` | Align the code UI kit to the design system (no-op if none configured). |
|
|
314
317
|
| `/cohorte-update-pipeline` | Refresh the installed core (global or bundled) to the latest published version. |
|
|
315
318
|
| `/cohorte-doctor` | Diagnose the installation (core, agents↔surfaces, hooks, gate, retrieval, worktrees). |
|
|
@@ -31,7 +31,11 @@ Idea (may be empty): **$ARGUMENTS**
|
|
|
31
31
|
|
|
32
32
|
If the idea is empty: when a board is configured and its **Ideas** column has cards, list them (with any
|
|
33
33
|
sub-bullet notes as seed context) and let the human pick one — otherwise ask **"What are we building?"**.
|
|
34
|
-
Either way, wait. If the idea is non-empty, restate it in one line and confirm you've got it
|
|
34
|
+
Either way, wait. If the idea is non-empty, restate it in one line and confirm you've got it — and
|
|
35
|
+
**if `specs/reports/intake-<idea>.md` exists, read it FIRST**: it is `/cohorte-intake`'s staged seed
|
|
36
|
+
(who is asking with verbatim quotes, goals/non-goals, an open-questions list that is this panel's
|
|
37
|
+
agenda, prior art incl. any `_decisions.md` line the request contradicts). The panel argues against
|
|
38
|
+
the distillate, never against the bare slug.
|
|
35
39
|
|
|
36
40
|
## Run the panel
|
|
37
41
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
model: sonnet
|
|
3
|
+
description: Fly several features in parallel — overlap analysis and merge order, one isolated worktree per feature, a live status board, and the post-merge rebase sweep nobody remembers to do.
|
|
4
|
+
argument-hint: plan <id> <id> [...] | status | sync
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the **fleet controller**. Parallel features already work in this pipeline — isolated
|
|
8
|
+
worktrees, per-feature DBs and ports, one session per worktree — but the coordination between
|
|
9
|
+
them is tribal knowledge: which specs collide, which merges first, and who rebases the
|
|
10
|
+
survivors after each ship. This command owns exactly that coordination, and deliberately
|
|
11
|
+
nothing more.
|
|
12
|
+
|
|
13
|
+
**What this command will never do: spawn the work headless.** The obvious "cool" version —
|
|
14
|
+
fire a loop per worktree from here — is the 2.2.0 driver's grave: child sessions nobody
|
|
15
|
+
supervises, stalling on prompts nobody sees. Each feature's build/review/loop runs in **its own
|
|
16
|
+
worktree's session**, supervised like any other; the fleet plans, watches and rebases. State
|
|
17
|
+
lives in `specs/reports/fleet.json` (the main checkout's gitignored buffer — not to be confused
|
|
18
|
+
with the dashboard's multi-*project* fleet registry; this file is this repo's multi-*feature*
|
|
19
|
+
flight plan).
|
|
20
|
+
|
|
21
|
+
> Read `PIPELINE.md` §`pipeline-profile` first: `surfaces` (paths — the overlap analysis keys
|
|
22
|
+
> on them), `contract`, `vcs.default_branch`, and the `isolation` block. _Skip the re-read if
|
|
23
|
+
> already in context and unmodified since._ **`isolation.enabled: false` ⇒ stop at `plan`**:
|
|
24
|
+
> N features in one checkout is a merge-conflict generator with extra steps — name the fix
|
|
25
|
+
> (`/cohorte-init-pipeline` wires isolation) and go no further.
|
|
26
|
+
|
|
27
|
+
## `plan <id> <id> [...]` — collide, order, provision
|
|
28
|
+
|
|
29
|
+
1. **Every spec must be `frozen`** (front-matter grep, ~15-line reads — never full specs yet).
|
|
30
|
+
Anything else (`draft`, `in-review`, missing) ⇒ name it and stop; a fleet of half-frozen
|
|
31
|
+
specs is N problems flying in formation.
|
|
32
|
+
2. **Overlap analysis** — read each spec's §5 contract entries and §6 surface tasks, then build
|
|
33
|
+
the matrix *feature × surface*, and flag the two collision classes:
|
|
34
|
+
- **Contract dependency** — spec B's §5 references shapes spec A introduces ⇒ B ships
|
|
35
|
+
**after** A, and B's worktree must rebase once A merges (the `sync` mode's job). This is
|
|
36
|
+
an ordering, not a blocker.
|
|
37
|
+
- **Same-tree writes** — two specs whose §6 tasks land in the same `surfaces[].path` (worse:
|
|
38
|
+
the same module). This is where parallel merges bleed; propose either an order (lighter
|
|
39
|
+
feature first) or — when the overlap is one file both must edit — say plainly that these
|
|
40
|
+
two should not fly together, and let the human drop one from the fleet.
|
|
41
|
+
3. **Propose the merge order** from those edges (dependencies first, then ascending overlap),
|
|
42
|
+
show the matrix + order in a compact table, and get the human's go-ahead — the order is a
|
|
43
|
+
plan they will live with for days.
|
|
44
|
+
4. **Provision worktrees** — for each feature without one: `scripts/new-feature.sh <id>` (the
|
|
45
|
+
rendered isolation script: worktree + branch + DB + port slot). Relay each script's output
|
|
46
|
+
line; a script failure stops the plan for that feature, never silently.
|
|
47
|
+
5. **Write `specs/reports/fleet.json`** (overwrite): `{"ts":"<ISO>","order":[...ids in merge
|
|
48
|
+
order...],"features":{"<id>":{"worktree":"<path>","branch":"<branch>","dependsOn":[...]}}}`.
|
|
49
|
+
6. **Print the launch plan** — one line per feature, in order: the worktree path to open a
|
|
50
|
+
session in, and the first command to run there (`/cohorte-build <id>`, or "ask for the loop
|
|
51
|
+
workflow: `{feature: \"<id>\"}`" on a runtime that has it). The human launches them; the
|
|
52
|
+
fleet does not.
|
|
53
|
+
|
|
54
|
+
## `status` — one table, no archaeology
|
|
55
|
+
|
|
56
|
+
Read `fleet.json` (absent ⇒ say `plan` comes first, stop). For each feature, **mechanical reads
|
|
57
|
+
only, redirected — always from THAT feature's worktree**, never the main checkout (each worktree
|
|
58
|
+
carries its own copy of `specs/<id>.md` and `specs/reports/`, and that copy is the one its run
|
|
59
|
+
has been writing): `<worktree>/specs/<id>.md` front-matter `status` · the worktree's
|
|
60
|
+
`specs/reports/<id>.loop.json` (`phase`/`round`/`outcome`) and `<id>.verdict.json`
|
|
61
|
+
(`verdict`/`blocking`) when present · then ONE
|
|
62
|
+
`git -C <worktree> fetch --quiet origin <default_branch> || true` and
|
|
63
|
+
`git -C <worktree> rev-list --count origin/<default_branch>..HEAD` and `..origin/<default_branch>`
|
|
64
|
+
(ahead / **behind** — behind is the number that matters, and against the *remote* ref: the local
|
|
65
|
+
one goes stale the moment a PR merges on the host, which is precisely when status gets asked).
|
|
66
|
+
One row per feature, in merge order:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
<id> · <status> · loop: <phase> r<round> | <outcome> · blocking: <n> · ↑<ahead> ↓<behind> · next: <the one action>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`next` is the whole point of the mode: the single action per feature (a command to run in its
|
|
73
|
+
worktree, "waiting on <dep> to merge", "ready to ship — its turn in the order", or "rebase
|
|
74
|
+
needed — run sync"). A worktree registered in `fleet.json` but gone from
|
|
75
|
+
`git worktree list` is reported as such, never silently dropped.
|
|
76
|
+
|
|
77
|
+
## `sync` — the post-merge sweep
|
|
78
|
+
|
|
79
|
+
Run after every merge (the human says which feature shipped, or you detect it: spec
|
|
80
|
+
`status: shipped` + branch merged into `<default_branch>`):
|
|
81
|
+
|
|
82
|
+
1. Drop the shipped feature from `fleet.json` (rewrite, keep order of the rest) — and if its
|
|
83
|
+
worktree still exists, remind the teardown `/cohorte-ship` proposes:
|
|
84
|
+
`scripts/remove-feature.sh <id>` (`--drop-db` at the human's call). Never run it unasked.
|
|
85
|
+
2. For each surviving worktree, in merge order — `git -C <worktree> fetch origin
|
|
86
|
+
<default_branch>` **always**, then decide whether the rebase is THIS session's to run:
|
|
87
|
+
- **Tree dirty** (`git -C <worktree> status --porcelain` non-empty — the NORMAL mid-flight
|
|
88
|
+
state: feature work stays uncommitted until `/cohorte-ship`, so `git rebase` would refuse
|
|
89
|
+
with "unstaged changes" before any conflict even exists) **or a run in flight** (the
|
|
90
|
+
worktree's `loop.json` has no `outcome`): **do not touch it.** Report the row as
|
|
91
|
+
`rebase needed — run \`git rebase origin/<default_branch>\` from that worktree's own
|
|
92
|
+
session` (mutating a branch from outside its session, mid-run, is how work disappears).
|
|
93
|
+
- **Clean and idle**: rebase it, output redirected to `specs/reports/fleet-sync.txt`. A
|
|
94
|
+
conflict is reported verbatim and left for its owner (`git rebase --abort` restores) —
|
|
95
|
+
never resolved, never `--force`-anything from here.
|
|
96
|
+
3. **Say the consequence out loud, per rebased (or rebase-needed) worktree:** the rebase moves
|
|
97
|
+
every commit, so any `reviewed_base`/`reviewed_digest` in that spec then describes a tree
|
|
98
|
+
that no longer exists — `/cohorte-ship` will (rightly) refuse until a fresh
|
|
99
|
+
`/cohorte-review` re-stamps it. A clean rebase is not a re-verdict; the re-review is.
|
|
100
|
+
4. Reprint the `status` table.
|
|
101
|
+
|
|
102
|
+
In chat, every mode prints its table/plan and nothing else — the matrix evidence and rebase
|
|
103
|
+
logs live in `specs/reports/`. **Recommend a `/clear`** after `plan` (the flight plan is on
|
|
104
|
+
disk; the sessions doing the flying are elsewhere anyway).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Triage anything that arrives — a ticket, a client email, a stack trace, a Slack thread — into the pipeline's entry point: a patch handoff or a brainstorm seed.
|
|
3
|
+
argument-hint: [paste the raw material — ticket, email, trace, thread — or empty to be asked]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the **intake triager**. Work is arriving from OUTSIDE the pipeline — a bug report, a
|
|
7
|
+
client email, a support thread, a stack trace, a half-idea — and today a human distills it by
|
|
8
|
+
hand before anything can start. Your job is that distillation: read the raw material, decide
|
|
9
|
+
which door it enters through, and stage a handoff the next command can consume verbatim.
|
|
10
|
+
|
|
11
|
+
> Read `PIPELINE.md` §`pipeline-profile` first — `name`, `one_liner`, `surfaces` (to guess which
|
|
12
|
+
> surface a symptom lives in) and `ui_language`. _Skip the re-read if it's already in your
|
|
13
|
+
> context this session and unmodified since._ Read `specs/_decisions.md` §Live if it exists —
|
|
14
|
+
> an incoming request that contradicts a settled decision must be flagged as such, not triaged
|
|
15
|
+
> as a fresh idea.
|
|
16
|
+
|
|
17
|
+
## 1. Ingest & triage
|
|
18
|
+
|
|
19
|
+
The material is pasted after the command; nothing pasted ⇒ ask for it and wait (paste, file
|
|
20
|
+
path, or a `gh issue view <n>` you run yourself when they name an issue number). Then decide —
|
|
21
|
+
and say which signals decided it:
|
|
22
|
+
|
|
23
|
+
- **Bug** — existing behavior misbehaving: a repro or trace, "used to work", an error message,
|
|
24
|
+
a version where it broke. → §2.
|
|
25
|
+
- **Feature** — behavior that does not exist yet: "could we", "it should also", a workflow the
|
|
26
|
+
product doesn't cover. → §3.
|
|
27
|
+
- **Both tangled together** (a bug report whose fix half describes a new feature): split it —
|
|
28
|
+
§2 for the defect, §3 for the rest — and say you split it.
|
|
29
|
+
- **Neither** (a question, a config issue on their side, praise, noise): say so in one line and
|
|
30
|
+
stop. Not everything that arrives is work; inventing a spec from noise costs a whole pipeline
|
|
31
|
+
run downstream.
|
|
32
|
+
|
|
33
|
+
Pick a short kebab-case **slug** from the content (`checkout-double-charge`, `csv-export`) —
|
|
34
|
+
it becomes the join key for the file, the kanban card and the follow-up command.
|
|
35
|
+
|
|
36
|
+
## 2. Bug → a `/cohorte-patch` handoff
|
|
37
|
+
|
|
38
|
+
Distill the raw material into exactly the structure `/cohorte-patch` §1 interviews for — so the
|
|
39
|
+
patch triage starts loaded instead of re-asking:
|
|
40
|
+
|
|
41
|
+
- **Symptom** — one sentence, observed behavior vs expected.
|
|
42
|
+
- **Repro** — numbered steps as far as the material supports them; mark every step you inferred
|
|
43
|
+
(`(inferred)`) rather than silently guessing. A trace with no steps ⇒ the trace IS the repro
|
|
44
|
+
material, say so.
|
|
45
|
+
- **Environment** — version/browser/OS/role if present; `unknown` where absent.
|
|
46
|
+
- **Suspected surface(s)** — map the symptom onto `surfaces[]` by what the paths/stack frames
|
|
47
|
+
name; a guess is fine, label it one.
|
|
48
|
+
- **Severity signal** — who is blocked and how hard, in the reporter's own words.
|
|
49
|
+
|
|
50
|
+
**Stage it** to `specs/reports/intake-<slug>.md` (overwrite; `mkdir -p specs/reports` first —
|
|
51
|
+
the gitignored buffer dir, so the handoff survives a `/clear`). Then:
|
|
52
|
+
|
|
53
|
+
> **Kanban** (SCHEMA.md §Kanban): run
|
|
54
|
+
> `<core>/pipeline/scripts/kanban-move.sh auto patch-<slug> ideas --title "[patch] <one-line title>"`.
|
|
55
|
+
> The id is **`patch-<slug>`, prefix included, and the title prefix is `[patch]`** — that is the
|
|
56
|
+
> exact join key and the exact title `/cohorte-patch` §1 looks for, so its later move finds THIS
|
|
57
|
+
> card instead of creating a duplicate and stranding this one in Ideas (the failure SCHEMA
|
|
58
|
+
> §Kanban's "tag before you move" exists to prevent). `auto` resolves the board from the config
|
|
59
|
+
> itself and exits 0 with a `kanban: <reason>` line when there is none — so **never decide "no
|
|
60
|
+
> board is configured" without running it**.
|
|
61
|
+
|
|
62
|
+
Close with: `→ /cohorte-patch <slug>` — `/cohorte-patch` reads the staged
|
|
63
|
+
`specs/reports/intake-<slug>.md` when it exists, so the triage starts loaded. Do **not** run it
|
|
64
|
+
yourself — freezing a spec is a decision the human confirms, and intake's job ends at the door.
|
|
65
|
+
|
|
66
|
+
## 3. Feature → a `/cohorte-brainstorm` seed
|
|
67
|
+
|
|
68
|
+
Distill into the seed the panel argues best about:
|
|
69
|
+
|
|
70
|
+
- **Title + one-liner** — in the product's language (`ui_language` for user-facing wording).
|
|
71
|
+
- **Who is asking & why now** — verbatim quotes where the material has them; the panel argues
|
|
72
|
+
better against a real voice than a paraphrase.
|
|
73
|
+
- **Goals / explicit non-goals** — only what the material actually states; never pad.
|
|
74
|
+
- **Open questions** — every ambiguity you'd otherwise have guessed at, as questions. This list
|
|
75
|
+
is the seed's real value: it is the brainstorm's agenda.
|
|
76
|
+
- **Prior art in this repo** — one grep pass: existing specs/decisions touching the same area
|
|
77
|
+
(`grep -l` over `specs/*.md`, redirected — never a file read per name). Contradicts a
|
|
78
|
+
`_decisions.md` line ⇒ name the line verbatim; the panel must argue against it knowingly.
|
|
79
|
+
|
|
80
|
+
**Stage it** to `specs/reports/intake-<slug>.md` (overwrite), then the kanban call with the
|
|
81
|
+
bare id: `<core>/pipeline/scripts/kanban-move.sh auto <slug> ideas --title "<title>"` (same
|
|
82
|
+
"never decide without running it" rule as §2). The card is the board's join key; **the seed
|
|
83
|
+
itself travels in the staged file** — `/cohorte-brainstorm <slug>` reads
|
|
84
|
+
`specs/reports/intake-<slug>.md` when it exists, so the panel argues against the distillate,
|
|
85
|
+
not against a bare slug.
|
|
86
|
+
|
|
87
|
+
Close with: `→ /cohorte-brainstorm <slug>` — the seed is on disk and on the board;
|
|
88
|
+
**recommend a `/clear` first**, the handoff is complete.
|
|
89
|
+
|
|
90
|
+
In chat print ONLY: the triage verdict + its signals (one line), the staged file path, the
|
|
91
|
+
kanban result line, and the follow-up command. Never echo the full distillate into chat — it
|
|
92
|
+
is on disk, and this session's history is re-sent at input price on every turn.
|
|
@@ -28,7 +28,12 @@ Bug (may be empty): **$ARGUMENTS**
|
|
|
28
28
|
|
|
29
29
|
## 1. Get the bug
|
|
30
30
|
|
|
31
|
-
If `$ARGUMENTS` is non-empty, restate it in one line and confirm you've got it
|
|
31
|
+
If `$ARGUMENTS` is non-empty, restate it in one line and confirm you've got it — and if it is
|
|
32
|
+
(or names) a slug with a staged `specs/reports/intake-<slug>.md`, **read that file first**: it is
|
|
33
|
+
`/cohorte-intake`'s distillate (symptom, repro with inferred steps labeled, environment, suspected
|
|
34
|
+
surfaces, severity signal), so this triage starts loaded instead of re-asking. Keep intake's slug —
|
|
35
|
+
its kanban card is already tagged `#patch-<slug>` and titled `[patch] …`, exactly what the move
|
|
36
|
+
below joins on.
|
|
32
37
|
|
|
33
38
|
If it is empty: when a board is configured and its **Ideas** column has cards, list them (with any
|
|
34
39
|
sub-bullet notes as context) — **cards titled `[patch]` first**, since those are the ones a human
|
|
@@ -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
|
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>"}')
|
|
@@ -387,7 +398,7 @@ const staging = await agent(
|
|
|
387
398
|
// mark ALL surfaces failed on the dashboard), 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
|