cohorte 1.3.3 → 1.3.4

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 (39) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +4 -4
  3. package/bin/cli.js +22 -4
  4. package/core/agents/implementer.template.md +10 -5
  5. package/core/commands/cycle.md +15 -8
  6. package/core/commands/doctor.md +3 -1
  7. package/core/hooks/gate.py +21 -6
  8. package/core/templates/agent-handoff.md +7 -2
  9. package/core/templates/review-feedback.md +7 -4
  10. package/core/templates/spec.template.md +5 -2
  11. package/core/templates/steps/init-pipeline/04-write-render.md +2 -1
  12. package/core/workflows/audit.js +20 -3
  13. package/core/workflows/cycle.js +146 -40
  14. package/core/workflows/refactor.js +16 -5
  15. package/core/workflows/review.js +59 -7
  16. package/dashboard/README.md +22 -5
  17. package/dashboard/dist/assets/index-AFQnlfjO.css +1 -0
  18. package/dashboard/dist/assets/{index-BxgA_mz1.js → index-DLBzciIC.js} +12 -11
  19. package/dashboard/dist/index.html +2 -2
  20. package/dashboard/server/doctor.js +60 -19
  21. package/dashboard/server/fleet.js +19 -5
  22. package/dashboard/server/index.js +79 -7
  23. package/dashboard/server/metrics.js +15 -4
  24. package/dashboard/server/versions.js +28 -6
  25. package/dashboard/server/yaml.js +4 -1
  26. package/install.ps1 +4 -0
  27. package/install.sh +19 -1
  28. package/package.json +5 -2
  29. package/profile/SCHEMA.md +28 -9
  30. package/scripts/kanban-move.sh +34 -20
  31. package/scripts/new-feature.sh.template +3 -1
  32. package/scripts/preflight.sh +16 -3
  33. package/scripts/remove-feature.sh.template +2 -1
  34. package/scripts/telemetry-send.sh +15 -1
  35. package/scripts/test-dashboard.mjs +356 -0
  36. package/scripts/test-gate.mjs +273 -0
  37. package/scripts/test-workflows.mjs +443 -0
  38. package/scripts/validate-core.mjs +49 -0
  39. package/dashboard/dist/assets/index-Cj0SpgEY.css +0 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,96 @@
3
3
  Entries are shown by `/update-pipeline` ("What's new") after a core refresh. Keep them short,
4
4
  user-facing, most recent first. One `## <version> — <YYYY-MM-DD>` section per release.
5
5
 
6
+ ## 1.3.4 — 2026-07-31
7
+
8
+ > **Re-run `npx cohorte@latest update --global` (or `update`)** — the workflow and script
9
+ > fixes only apply once the installed core is refreshed.
10
+
11
+ - **The dashboard server now has tests.** `dashboard/server/*.js` is shipped runtime code — a
12
+ hand-rolled YAML parser every `/doctor` check derives from, the metrics aggregator, the JS port
13
+ of `/doctor`, the board parser, the fleet registry, and the HTTP guards — with no coverage at
14
+ all. `scripts/test-dashboard.mjs` (83 assertions, in CI) pins each module, including every
15
+ `/doctor` check both green and deliberately broken.
16
+ - **The gate hook now has tests.** `hooks/gate.py` is the one component that can block a command,
17
+ and CI only ever checked that it *parsed* — every one of its shipped regressions reached users
18
+ first. `scripts/test-gate.mjs` drives its real stdin→stdout contract (42 assertions: deny/ask
19
+ tiers, chained-command splitting, branch-conditional gating at the payload's cwd, the
20
+ `bypassPermissions` ask⇒deny escalation, config robustness, the preflight phase gate, worktree
21
+ HEAD matching) and runs in CI. No new defect was found in the gate itself — the behaviour is
22
+ now pinned.
23
+ - **A crashed reviewer scored as a clean surface.** `agent()` returns `null` when a subagent dies,
24
+ and a dead reviewer returns zero findings — byte-identical to a surface with nothing wrong. Both
25
+ `review.js` and `cycle.js` read that as `SHIP`: the review workflow answered "`/ship`" when
26
+ *every* reviewer had crashed, and the cycle workflow exited **SHIP-READY**, ticked the DoD and
27
+ stamped the freshness gate over code nobody had read. Unreviewed surfaces are now named in
28
+ `unreviewedSurfaces` + `questions`, can never score `SHIP`, and the cycle re-reviews instead of
29
+ dispatching an empty fix round. `scripts/test-workflows.mjs` (new, run in CI) pins this.
30
+ - **Swept the whole "dead agent read as success" family across all four workflows** — the same
31
+ root cause as the two above, found at eight more call sites by auditing every `agent()` result
32
+ in `cycle.js` · `review.js` · `audit.js` · `refactor.js`. The worst: a dead **diff-staging**
33
+ agent in `review.js` returned `verdict: SHIP` ("no diff — nothing to review") for a feature
34
+ nobody had looked at; and a dead **close/staging** agent let both `/cycle` and the review
35
+ workflow report `SHIP-READY` + a report path + "ship is a straight shot" when the report, the
36
+ DoD ticks, the freshness stamp and the metrics had never been written (and `/ship`'s freshness
37
+ gate skips silently when those fields are absent, so the human would have shipped on it). Also:
38
+ a dead auditor made a domain look **clean** instead of unaudited; a dead backlog writer/reader
39
+ and a dead item-ticker were each reported as success. Every one of these now distinguishes
40
+ "died" from "succeeded with nothing to say", and `scripts/test-workflows.mjs` pins all of them.
41
+ - **A dead contract agent was reported as a successful re-authoring.** Same failure shape as the
42
+ crashed reviewer: when the lead-equivalent agent that re-authors spec §5 + the contract file
43
+ died, `/cycle` still pushed a `contractChanges` entry and handed **every** consuming surface a
44
+ CRITICAL "the contract was RE-AUTHORED — re-read it and realign" item, pointing at a file
45
+ nobody had touched. It now reports the contract as UNCHANGED and ripples nothing.
46
+ - **A red preflight nobody owns burned every remaining round.** When no surface path appeared in
47
+ the failure tail and no implementer had survived the build, the fix round dispatched *zero*
48
+ agents, the next round found the same red gates, and the loop spun to the cap doing literally
49
+ nothing before reporting a stale verdict. It now stops immediately with the failure tail.
50
+ - **Findings belonging to no surface were dropped silently.** A finding whose file sits outside
51
+ every surface tree — reachable when the diff-staging agent names a key the profile lacks —
52
+ stayed in the open set (so the loop could never exit clean) while nobody was ever dispatched to
53
+ fix it. They are now named, with their `file:line`, in `questions`.
54
+ - **`/cycle` build telemetry hid dead implementers**: results were mapped over the *survivors*, so
55
+ two of three surfaces reported `ok,ok` and the dead one vanished from the funnel entirely.
56
+ - **The review workflow sent HIGH findings straight to `/ship`.** A `SHIP` verdict can legitimately
57
+ carry HIGH/MEDIUM findings (only CRITICAL and security force a fix), but the conversational
58
+ `/review` routes any surviving HIGH to `/fix` — the workflow said `/ship`. It now only recommends
59
+ shipping when nothing above LOW survived, and stamps the freshness gate on that same condition.
60
+ - **Smoke is now opt-in in the cycle workflow** — `/cycle <id> smoke` (or `args.smoke: true`).
61
+ Booting the app every round is expensive and a library project has nothing to smoke. Without it
62
+ the run reports `smoke: "SKIPPED"`, leaves the runtime-flow DoD boxes unticked, and says so.
63
+ - **The dashboard reset could move the shared global core.** Nothing stopped a project path of
64
+ `~` — reset would then rename `~/.claude` into a backup dir and break every repo on the machine,
65
+ while the UI promised the global core is never touched. It now refuses that path outright.
66
+ - **The cycle workflow polluted the metrics with phantom surfaces.** It wrote `rounds` / `verdict` /
67
+ `smoke` inside the metrics line's `surfaces` map, so the dashboard rendered them as three surface
68
+ rows and scored `rounds: "1"` as a failing surface. Run-level facts now sit outside `surfaces`,
69
+ and the dashboard knows the `cycle` phase.
70
+ - **Telemetry from bundled installs reported no core version.** `telemetry-send.sh` read `VERSION`
71
+ only from the *global* core; it now resolves the core that ships it. Payloads are also hardened —
72
+ a quote or newline in the results string used to produce JSON the collector dropped.
73
+ - **Every kanban card move added a blank line.** Ten moves of one card padded a board with fifteen
74
+ of them, and every phase command moves cards. Runs of blank lines are now collapsed; a board is
75
+ byte-stable across moves. Sub-notes of a duplicate card are no longer duplicated either.
76
+ - **Python bytecode could reach the published package and users' `.claude`.** `.npmignore` is inert
77
+ under an explicit `files` allowlist, so its `__pycache__/` rule never fired, and all three
78
+ installers copied the directory verbatim. Excluded at both ends, asserted in CI.
79
+ - **Every non-design surface agent rendered with a blank first TDD step.** `<SURFACE_TDD_STEP1>`
80
+ sat as numbered item 1 of the TDD list but is filled only for `uses_design` surfaces, so every
81
+ other agent got an empty "1." above its real first step. It is now a lead-in paragraph.
82
+ - Doc/template corrections found by reading the whole core against the code: the spec template's
83
+ `## 6+. Surface tasks` could collide with the `§8`/`§9` sections the pipeline references by
84
+ number; `/init-pipeline` step 04 forgot `smoke.md` in its "leave the fixed agents as-is" list;
85
+ the getting-started page placed `implementer.template.md` in `agents/` (it ships in `pipeline/`);
86
+ the dashboard docs never documented the CSRF/DNS-rebinding guard; `dashboard/README.md` was
87
+ missing `metrics.js`, `/api/metrics` and `/audit`; the two reference-only templates
88
+ (`agent-handoff.md`, `review-feedback.md`) are unreferenced copies of shapes that live in the
89
+ agents — `review-feedback.md` had drifted and is re-synced, and both now say so.
90
+ - Smaller: the dashboard no longer stalls for 13 s per project when npm is unreachable (failed
91
+ lookups are cached and de-duplicated); `--port=` rejects a non-numeric value instead of listening
92
+ on a random port; a missing hashed asset 404s instead of being served `index.html`; a project
93
+ card is now keyboard-activatable; long headless logs are trimmed instead of growing unbounded;
94
+ `install.sh --help` exists; the preflight stamp is written once per distinct directory.
95
+
6
96
  ## 1.3.3 — 2026-07-30
7
97
 
8
98
  > **Re-run `npx cohorte@latest update --global` (or `update`)** — the gate fixes only apply once
package/README.md CHANGED
@@ -217,7 +217,7 @@ it in `.claude/pipeline/VERSION` and bundled repos in their committed `pipeline.
217
217
  | `/brainstorm` | Interactive persona panel that pressure-tests a feature idea. |
218
218
  | `/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. |
219
219
  | `/build <id>` | Lead authors the contract, then dispatches one implementer per surface in parallel. |
220
- | `/cycle <id>` | Launch the full dev-cycle **workflow** on a frozen spec: build → smoke ∥ review → fix until zero findings; deferred questions in the output. Needs workflows enabled (falls back to the conversational path). |
220
+ | `/cycle <id>` | Launch the full dev-cycle **workflow** on a frozen spec: build → review → fix until zero findings (add `smoke` to run the app each round); deferred questions in the output. Needs workflows enabled (falls back to the conversational path). |
221
221
  | `/smoke <id>` | Run the feature for real: infra up, contract endpoints, UI flows, design conformance. |
222
222
  | `/review <id>` | Read-only review agents (one per touched surface, parallel) audit the diff vs the spec. |
223
223
  | `/fix <id>` | Apply a review/smoke report: remediation into the spec, re-dispatch only the surfaces with findings. |
@@ -278,13 +278,13 @@ Rules that make it safe:
278
278
 
279
279
  ### Workflows — deterministic multi-agent runs (opt-in)
280
280
 
281
- Three phases also ship as **workflow scripts** for the Claude Code Workflow runtime — the same
281
+ Four phases also ship as **workflow scripts** for the Claude Code Workflow runtime — the same
282
282
  fan-out the commands orchestrate, but driven by a deterministic script instead of the lead reasoning
283
283
  it out turn by turn:
284
284
 
285
285
  | Script | What it runs |
286
286
  | ----------------------- | ----------------------------------------------------------------------------------------- |
287
- | `workflows/cycle.js` | **The full dev cycle on a frozen spec**: contract → parallel build → smoke ∥ review(+cross-check) → fix, looping until zero findings + PASS (contract changes handled in-loop by a lead-equivalent agent). Human decisions come back in a `questions` array at the END — empty when `/brainstorm`+`/spec` did their job. Exits SHIP-ready (DoD ticked, freshness stamped) so `/ship` is a straight shot. |
287
+ | `workflows/cycle.js` | **The full dev cycle on a frozen spec**: contract → parallel build → review(+cross-check) → fix (∥ smoke each round if opted in via `args.smoke`), looping until zero findings (contract changes handled in-loop by a lead-equivalent agent). Human decisions come back in a `questions` array at the END — empty when `/brainstorm`+`/spec` did their job. Exits SHIP-ready (DoD ticked, freshness stamped) so `/ship` is a straight shot. |
288
288
  | `workflows/review.js` | Preflight gate (aborts while red — zero agents), one reviewer per touched surface, adversarial cross-check of CRITICAL/security findings, merged verdict only. |
289
289
  | `workflows/audit.js` | One auditor per domain (every surface + shared) concurrently, prioritized `specs/refactor-backlog.md`. |
290
290
  | `workflows/refactor.js` | Big domains only: `shared` first and alone, then parallel surface implementers, per-domain verify + one retry. |
@@ -338,7 +338,7 @@ core/ # copied verbatim into ~/.claude (global) or <project>/.
338
338
  commands/ # init-pipeline + the pipeline commands + /update-pipeline
339
339
  hooks/ # gate.py (destructive-command gate; branch-aware; preflight phase gate)
340
340
  templates/ # handoff / brainstorm-return / design-brief / review-feedback / pr-body / spec
341
- workflows/ # opt-in Workflow-runtime scripts: review.js / audit.js / refactor.js
341
+ workflows/ # opt-in Workflow-runtime scripts: cycle.js / review.js / audit.js / refactor.js
342
342
  profile/
343
343
  PIPELINE.template.md # the profile skeleton /init-pipeline fills
344
344
  SCHEMA.md # field reference
package/bin/cli.js CHANGED
@@ -68,8 +68,18 @@ for (const a of args) {
68
68
  if (a === 'install' || a === 'update' || a === 'dashboard') mode = a;
69
69
  else if (a === 'version' || a === '--version' || a === '-v') { console.log(VERSION); process.exit(0); }
70
70
  else if (a === '--global' || a === '-g') scope = 'global';
71
- else if (a.startsWith('--port=')) { port = parseInt(a.slice(7), 10); }
72
- else if (a.startsWith('--host=')) { host = a.slice(7); }
71
+ else if (a.startsWith('--port=')) {
72
+ // A bad value used to land as NaN, which http.listen() silently treats as
73
+ // "any free port" — the banner then printed `localhost:NaN` and nothing worked.
74
+ port = parseInt(a.slice(7), 10);
75
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
76
+ console.error(`error: --port must be an integer 1-65535 (got "${a.slice(7)}")`);
77
+ process.exit(2);
78
+ }
79
+ } else if (a.startsWith('--host=')) {
80
+ host = a.slice(7).trim();
81
+ if (!host) { console.error('error: --host= needs an address (e.g. --host=0.0.0.0)'); process.exit(2); }
82
+ }
73
83
  else if (a === '--open') { openBrowser = true; }
74
84
  else if (a === 'help' || a === '--help' || a === '-h') usage(0);
75
85
  else if (a.startsWith('-')) { console.error(`error: unknown flag: ${a}`); usage(2); }
@@ -99,12 +109,20 @@ fs.mkdirSync(dest, { recursive: true });
99
109
 
100
110
  // --- helpers (mirror install.sh) --------------------------------------------
101
111
  function copyCore() {
102
- // `workflows` = the deterministic orchestration scripts (review/audit/refactor) the
112
+ // `workflows` = the deterministic orchestration scripts (cycle/review/audit/refactor) the
103
113
  // Workflow runtime resolves from .claude/workflows (bundled) or ~/.claude/workflows
104
114
  // (global) — same copy rule in both modes, like commands.
105
115
  for (const d of ['commands', 'hooks', 'templates', 'workflows']) {
106
- fs.cpSync(path.join(src, 'core', d), path.join(dest, d), { recursive: true, force: true });
116
+ fs.cpSync(path.join(src, 'core', d), path.join(dest, d), {
117
+ recursive: true,
118
+ force: true,
119
+ // Never carry a Python bytecode cache into a user's .claude. It appears in a
120
+ // source checkout the moment anyone compiles or imports gate.py (CI does), it
121
+ // is machine- and interpreter-specific, and copy-over never deletes it later.
122
+ filter: s => !s.split(/[\\/]/).includes('__pycache__') && !s.endsWith('.pyc'),
123
+ });
107
124
  }
125
+ fs.rmSync(path.join(dest, 'hooks', '__pycache__'), { recursive: true, force: true });
108
126
  // 0.1.19 renamed questionnaire-domain-brief.md → research-brief.md; drop the stale copy.
109
127
  fs.rmSync(path.join(dest, 'templates', 'questionnaire-domain-brief.md'), { force: true });
110
128
  const pipelineDir = path.join(dest, 'pipeline');
@@ -61,12 +61,17 @@ tools are unavailable or come up empty.
61
61
 
62
62
  ## How you work — strict TDD (red → green → refactor)
63
63
 
64
- 1. <SURFACE_TDD_STEP1>
65
- 2. **Write the failing test(s) first** from the frozen contract. Cover exactly what your baked
64
+ <!-- <SURFACE_TDD_STEP1> is a LEAD-IN paragraph, not a numbered item: it is filled only for a
65
+ `uses_design` surface (the design-pull step) and renders as nothing otherwise. As a numbered
66
+ item it left every non-design agent with a blank "1." above the real first step. -->
67
+
68
+ <SURFACE_TDD_STEP1>
69
+
70
+ 1. **Write the failing test(s) first** from the frozen contract. Cover exactly what your baked
66
71
  Testing rules (§Your conventions) prescribe. Run the test command and watch it fail (red).
67
- 3. Implement until green, following your baked conventions.
68
- 4. Refactor to the conventions. Keep tests green.
69
- 5. **Lint + format before handoff:** run your surface's lint and fix every issue. If the project
72
+ 2. Implement until green, following your baked conventions.
73
+ 3. Refactor to the conventions. Keep tests green.
74
+ 4. **Lint + format before handoff:** run your surface's lint and fix every issue. If the project
70
75
  registers a PostToolUse format hook (see `.claude/settings.json`), your files are already
71
76
  formatted on every write — skip `format_cmd`; otherwise run it too. Code you hand off must be
72
77
  lint-clean and formatted.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  model: sonnet
3
- description: Launch the full dev-cycle workflow (contract → build → smoke ∥ review → fix, until zero findings) for a frozen spec; relay its verdict + deferred questions.
4
- argument-hint: <feature_id> [max_rounds]
3
+ description: Launch the full dev-cycle workflow (contract → build → review → fix, until zero findings; add `smoke` to also run the app each round) for a frozen spec; relay its verdict + deferred questions.
4
+ argument-hint: <feature_id> [max_rounds] [smoke]
5
5
  ---
6
6
 
7
7
  You are the **lead**. Launch the full dev-cycle **workflow** for feature **$ARGUMENTS** — the
@@ -14,8 +14,11 @@ start it and relay its result. Do NOT run the phases yourself here — that's th
14
14
 
15
15
  ## 1. Resolve & check (fail fast, before spending anything)
16
16
 
17
- - Parse `$ARGUMENTS`: the first token is `<feature_id>`, an optional second numeric token is
18
- `<max_rounds>` (the workflow defaults to 5).
17
+ - Parse `$ARGUMENTS`: the first token is `<feature_id>`, an optional numeric token is
18
+ `<max_rounds>` (the workflow defaults to 5), and an optional literal token `smoke` opts the
19
+ smoke agent INTO the verify rounds (default: review-only — smoke is expensive, it boots the
20
+ app every round; skipping it is the human's explicit risk, `/smoke <feature_id>` stays
21
+ available standalone before `/ship`).
19
22
  - Resolve the script: `.claude/workflows/cycle.js` if it exists, else `~/.claude/workflows/cycle.js`
20
23
  (`test -f`). **Missing both** ⇒ the core predates 1.3.0 or is half-copied: tell the human to run
21
24
  `/update-pipeline`, and stop.
@@ -29,10 +32,12 @@ start it and relay its result. Do NOT run the phases yourself here — that's th
29
32
  ## 2. Launch
30
33
 
31
34
  Call the `Workflow` tool: `scriptPath: <resolved cycle.js path>`,
32
- `args: {"feature": "<feature_id>", "maxRounds": <max_rounds, omit if not given>}`.
33
- It runs in the background tell the human it's off and what it will do (build, then smoke ∥ review
34
- fix rounds until zero findings + PASS; no questions mid-run), and that `/workflows` shows live
35
- progress. Then END YOUR TURN never poll, never sleep; the completion notification re-wakes you.
35
+ `args: {"feature": "<feature_id>", "maxRounds": <max_rounds, omit if not given>, "smoke": <true
36
+ only if the smoke token was given omit otherwise>}`.
37
+ It runs in the background tell the human it's off and what it will do (build, then review → fix
38
+ rounds until zero findings, with smoke alongside each round if opted in; no questions mid-run), and
39
+ that `/workflows` shows live progress. Then END YOUR TURN — never poll, never sleep; the completion
40
+ notification re-wakes you.
36
41
 
37
42
  ## 3. Relay the result (when the task notification arrives)
38
43
 
@@ -43,6 +48,8 @@ context:
43
48
  - `outcome` · rounds used · review verdict · smoke result.
44
49
  - `contractChanges` if any — flag them explicitly: the loop re-authored the frozen contract
45
50
  lead-style; the human should eyeball those hunks in the diff.
51
+ - `unreviewedSurfaces` if non-empty — **say it plainly**: a reviewer died, so those surfaces
52
+ carry NO verdict. The run's verdict covers the others only; the code there was never read.
46
53
  - **The `questions` array, verbatim** — this is the human's inbox from the run (empty when the spec
47
54
  pre-answered everything). Each one is a decision to make, usually by sharpening the spec.
48
55
  - The `next` line: **SHIP-READY** ⇒ `/ship <feature_id>` (DoD ticked + freshness stamped — ship is a
@@ -44,7 +44,9 @@ fix only with the human's go-ahead (or hand them the command).
44
44
  3. **Hooks & gate.** `.claude/gate-config.json` exists and mirrors the profile's `gate` block
45
45
  (regenerate if drifted). The PreToolUse gate hook is registered **once** for the install mode
46
46
  (bundled: repo `settings.json`; global: `~/.claude/settings.json` — flag double registration,
47
- it double-prompts). Hook files exist at the registered paths.
47
+ it double-prompts) **with a matcher covering both `Bash` and `Task`** — a `Bash`-only matcher
48
+ leaves the preflight phase gate dead (the 1.3.0–1.3.1 regression). Hook files exist at the
49
+ registered paths.
48
50
  4. **Retrieval** (if `retrieval.provider` ≠ `none`). Run the SCHEMA.md §Code retrieval health
49
51
  check: CLI resolvable from PATH, `.mcp.json` entry present in PATH-proof launcher form,
50
52
  `.serena/` gitignored, server actually connects.
@@ -100,18 +100,33 @@ def current_branch(cwd: str):
100
100
  return None
101
101
 
102
102
 
103
- def current_head(cwd: str):
104
- """HEAD sha at `cwd`, or None."""
103
+ def known_heads(cwd: str):
104
+ """Every HEAD sha this repo currently has checked out: `cwd`'s plus each
105
+ linked worktree's. The preflight may legitimately have run in a feature
106
+ worktree while the Task dispatch fires from the main checkout (or vice
107
+ versa) — comparing against a single HEAD flags those runs as stale."""
108
+ heads = set()
105
109
  try:
106
110
  out = subprocess.run(
107
111
  ["git", "rev-parse", "HEAD"],
108
112
  cwd=cwd, capture_output=True, text=True, timeout=3,
109
113
  )
114
+ if out.returncode == 0 and out.stdout.strip():
115
+ heads.add(out.stdout.strip())
116
+ except Exception:
117
+ pass
118
+ try:
119
+ out = subprocess.run(
120
+ ["git", "worktree", "list", "--porcelain"],
121
+ cwd=cwd, capture_output=True, text=True, timeout=3,
122
+ )
110
123
  if out.returncode == 0:
111
- return out.stdout.strip() or None
124
+ for line in out.stdout.splitlines():
125
+ if line.startswith("HEAD "):
126
+ heads.add(line.split(" ", 1)[1].strip())
112
127
  except Exception:
113
128
  pass
114
- return None
129
+ return heads
115
130
 
116
131
 
117
132
  def check_preflight(payload: dict, cfg: dict) -> int:
@@ -140,8 +155,8 @@ def check_preflight(payload: dict, cfg: dict) -> int:
140
155
  if age_min > max_age:
141
156
  why = f"the preflight stamp is {age_min:.0f} min old (max {max_age:.0f})"
142
157
  else:
143
- head = current_head(session_cwd(payload))
144
- if head and sha not in ("", "none") and head != sha:
158
+ heads = known_heads(session_cwd(payload))
159
+ if heads and sha not in ("", "none") and sha not in heads:
145
160
  why = "HEAD moved since the preflight ran"
146
161
  except Exception:
147
162
  why = "the preflight stamp is unreadable (expected `<epoch> <sha>`)"
@@ -1,6 +1,11 @@
1
- # HANDOFF — <surface> · `<feature_id>`
1
+ # HANDOFF — <surface> · <feature_id>
2
2
 
3
- <!-- Keep it tight: the lead only acts on mismatches, test failures, remediation ticks, and TODOs.
3
+ <!-- REFERENCE COPY not read at runtime. The AUTHORITATIVE shape is inlined in
4
+ core/agents/implementer.template.md (§"Your return"), because a rendered agent must
5
+ not need a second file read to know its own output format. This file exists for humans
6
+ reading the core; keep it byte-aligned with the template, or delete both together.
7
+
8
+ Keep it tight: the lead only acts on mismatches, test failures, remediation ticks, and TODOs.
4
9
  Never list files one by one — the lead has `git diff --stat`. Never paste code excerpts —
5
10
  a file:line reference is enough, the code is on disk. One line per item. -->
6
11
 
@@ -1,15 +1,18 @@
1
1
  # REVIEW REPORT
2
2
 
3
- <!-- Emitted-report shape. Everything in an HTML comment is guidance do NOT emit it.
3
+ <!-- REFERENCE COPY not read at runtime. The AUTHORITATIVE shape is inlined in
4
+ core/agents/review.md (§"Your return"), because an agent must not need a second
5
+ file read to know its own output format. This file exists for humans reading the
6
+ core; keep it byte-aligned with the agent, or delete both together.
7
+
8
+ Everything in an HTML comment is guidance — do NOT emit it.
4
9
  Verdict rules live in the review agent's instructions (SHIP = no CRITICAL/security;
5
10
  REVISE = ≥1 CRITICAL; BLOCK = security vulnerability).
6
11
  CAPPED: max 20 findings, ONE line each (severity · file:line · type · concrete fix),
7
12
  ZERO code excerpts — file:line is enough, the source is on disk. Overflow ⇒ keep all
8
13
  CRITICAL/HIGH/security, fill by severity, close with one `+<n> more …` line. -->
9
14
 
10
- feature_id: <feature_id>
11
- Feature branch: <feature_branch_prefix><feature_id>
12
- Commit SHA: <first 12 chars>
15
+ feature_id: <feature_id> · scope: <surface.key>
13
16
 
14
17
  | Severity | Count |
15
18
  | -------- | ----- |
@@ -49,9 +49,12 @@ design_files: [] # design page links — full URLs of the form https://claude.ai
49
49
 
50
50
  - `<slug>...Request` / `<slug>...Response` / `<slug>Base` … (sketch the schemas here)
51
51
 
52
- ## 6+. Surface tasks
52
+ ## 6. Surface tasks
53
53
 
54
- > One subsection per surface in `PIPELINE.md` §surfaces (e.g. backend, frontend), each TDD.
54
+ > One `###` subsection per surface in `PIPELINE.md` §surfaces (e.g. backend, frontend), each TDD.
55
+ > Surfaces are subsections of §6, never top-level sections — §8 and §9 are referenced by number
56
+ > across the whole pipeline (`/smoke` drives "§8 flows", `/review` checks "§9 acceptance"), so a
57
+ > third surface must not become a §8 of its own.
55
58
 
56
59
  ### <surface.key>
57
60
 
@@ -14,7 +14,8 @@
14
14
  PIPELINE.md you just wrote), and the surface-specific blocks
15
15
  (`<SURFACE_EXTRA_NEVER>`, `<SURFACE_DESIGN_INPUT>`, `<SURFACE_TDD_STEP1>` — fill design-related ones
16
16
  only when `uses_design`).
17
- Leave `review.md` + `release.md` + `profile-reader.md` as-is (generic).
17
+ Leave the fixed agents as-is (generic, shipped by the installer): `review.md`, `release.md`,
18
+ `smoke.md`, `profile-reader.md`.
18
19
  4. **Generate `.claude/gate-config.json`** from the `gate` block — copy all five keys verbatim:
19
20
  `{"deny": [...], "ask": [...], "ask_on_default_branch": [...], "default_branch": "<vcs.default_branch>",
20
21
  "preflight": {"enabled": <gate.preflight.enabled>, "agents": [...], "max_age_minutes": <n>}}`
@@ -114,11 +114,20 @@ const audited = await parallel(domains.map(d => () => agent(
114
114
  { agentType: 'review', label: `audit:${d.key}`, schema: BACKLOG },
115
115
  ).then(r => r && { key: d.key, items: r.items, overflow: r.overflow || 0 })))
116
116
  const perDomain = audited.filter(Boolean)
117
+ // A dead auditor returns null, and a domain with no result is indistinguishable
118
+ // from a domain with nothing to report — the backlog would simply omit it and the
119
+ // human would read that as "clean". Name them instead.
120
+ const deadDomains = domains.filter(d => !perDomain.some(p => p.key === d.key)).map(d => d.key)
121
+ if (deadDomains.length) log(`Auditor died on: ${deadDomains.join(', ')} — those domains are NOT audited`)
117
122
 
118
123
  // ── Phase 3 — merge + write the backlog ──────────────────────────────────────
119
124
  phase('Backlog')
120
125
  const SEV = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
121
126
  const body = ['# Refactor backlog', '', `> Generated by the cohorte-audit workflow (scope: ${scope}).`]
127
+ if (deadDomains.length) {
128
+ body.push('', `> ⚠ NOT audited (the auditor died): ${deadDomains.join(', ')} — absence of items below`,
129
+ '> for those domains means "not looked at", not "clean". Re-run the audit for them.')
130
+ }
122
131
  let total = 0
123
132
  for (const d of perDomain) {
124
133
  const items = [...d.items].sort((a, b) => SEV[a.severity] - SEV[b.severity])
@@ -127,18 +136,26 @@ for (const d of perDomain) {
127
136
  for (const it of items) body.push(`- [ ] ${it.severity} · ${it.file}:${it.line} · ${it.kind} · ${it.fix}`)
128
137
  if (d.overflow) body.push(`- [ ] (+${d.overflow} more beyond the cap — re-audit ${d.key} after this pass)`)
129
138
  }
130
- await agent(
139
+ const written = await agent(
131
140
  `Write EXACTLY this content to specs/refactor-backlog.md (overwrite), then return the single word done:\n<<<BACKLOG\n${body.join('\n')}\nBACKLOG`,
132
141
  { model: 'haiku', label: 'write-backlog', effort: 'low' },
133
142
  )
143
+ // Returning `backlog: <path>` when the writer died points /refactor at a file
144
+ // that does not exist (or, worse, at the PREVIOUS run's stale backlog).
145
+ const backlogOk = written != null && /done/i.test(String(written))
134
146
 
135
147
  return {
136
- backlog: 'specs/refactor-backlog.md',
148
+ backlog: backlogOk ? 'specs/refactor-backlog.md' : '(NOT written — the backlog writer died)',
149
+ notAudited: deadDomains, // absence of findings here means "not looked at"
137
150
  mechanicalFailures: mech.length,
138
151
  domains: Object.fromEntries(perDomain.map(d => [d.key, d.items.length + (d.overflow || 0)])),
139
152
  total,
140
153
  top: perDomain.flatMap(d => d.items.map(it => ({ ...it, domain: d.key })))
141
154
  .sort((a, b) => SEV[a.severity] - SEV[b.severity]).slice(0, 10)
142
155
  .map(it => `[${it.severity}] ${it.domain} · ${it.file}:${it.line} — ${it.fix}`),
143
- next: 'refactor a domain with /refactor <domain> (or the refactor workflow for big domains)',
156
+ next: !backlogOk
157
+ ? 'the backlog was NEVER written (writer died) — the counts above are real but nothing is on disk; re-run the audit'
158
+ : deadDomains.length
159
+ ? `re-audit ${deadDomains.join(', ')} (auditor died — not covered), then /refactor <domain>`
160
+ : 'refactor a domain with /refactor <domain> (or the refactor workflow for big domains)',
144
161
  }