cohorte 1.3.3 → 1.4.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 +133 -0
- package/README.md +4 -7
- package/bin/cli.js +49 -4
- package/core/agents/implementer.template.md +10 -5
- package/core/agents/profile-reader.md +22 -0
- package/core/commands/doctor.md +8 -4
- package/core/hooks/gate.py +21 -6
- package/core/templates/agent-handoff.md +7 -2
- package/core/templates/review-feedback.md +7 -4
- package/core/templates/spec.template.md +5 -2
- package/core/templates/steps/init-pipeline/04-write-render.md +2 -1
- package/core/workflows/audit.js +88 -7
- package/core/workflows/refactor.js +85 -9
- package/core/workflows/review.js +132 -11
- package/dashboard/README.md +22 -5
- package/dashboard/dist/assets/index-AFQnlfjO.css +1 -0
- package/dashboard/dist/assets/{index-BxgA_mz1.js → index-DLBzciIC.js} +12 -11
- package/dashboard/dist/index.html +2 -2
- package/dashboard/server/doctor.js +68 -20
- package/dashboard/server/fleet.js +19 -5
- package/dashboard/server/index.js +79 -7
- package/dashboard/server/metrics.js +16 -4
- package/dashboard/server/versions.js +28 -6
- package/dashboard/server/yaml.js +4 -1
- package/install.ps1 +4 -0
- package/install.sh +19 -1
- package/package.json +5 -2
- package/profile/SCHEMA.md +23 -26
- package/scripts/kanban-move.sh +34 -20
- package/scripts/metrics/collect.mjs +495 -0
- package/scripts/metrics/prices.json +39 -0
- package/scripts/new-feature.sh.template +3 -1
- package/scripts/preflight.sh +16 -3
- package/scripts/remove-feature.sh.template +2 -1
- package/scripts/telemetry-send.sh +15 -1
- package/scripts/test-dashboard.mjs +362 -0
- package/scripts/test-gate.mjs +273 -0
- package/scripts/test-metrics.mjs +135 -0
- package/scripts/test-workflows.mjs +321 -0
- package/scripts/validate-core.mjs +51 -1
- package/core/commands/cycle.md +0 -54
- package/core/workflows/cycle.js +0 -407
- package/dashboard/dist/assets/index-Cj0SpgEY.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,139 @@
|
|
|
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.4.0 — 2026-08-01
|
|
7
|
+
|
|
8
|
+
> **Re-run `npx cohorte@latest update --global` (or `update`)** — the workflow fixes only apply
|
|
9
|
+
> once the installed core is refreshed. Both the workflow scripts and the `profile-reader` agent
|
|
10
|
+
> are replaced by the update.
|
|
11
|
+
|
|
12
|
+
- **BREAKING — `/cycle` and `cycle.js` are removed.** The full-cycle workflow is gone: the command
|
|
13
|
+
file, the script, its tests and its documentation. The conversational path it wrapped is
|
|
14
|
+
unchanged and remains the way to run a feature — `/build` → `/smoke` → `/review` → `/fix` →
|
|
15
|
+
`/ship`, with `/clear` safe between each. `review.js`, `audit.js` and `refactor.js` are
|
|
16
|
+
untouched. Nothing else in the pipeline depended on it; a `/cycle` in an old habit will simply
|
|
17
|
+
report an unknown command. Metrics files and dashboards that already carry `phase: "cycle"`
|
|
18
|
+
lines keep rendering them.
|
|
19
|
+
- **A workflow could dispatch zero agents and still report a verdict.** Phase 0's `profile-reader`
|
|
20
|
+
(haiku) intermittently returned the profile as a JSON *string* nested under a wrapper field
|
|
21
|
+
(`{"output": "{\"surfaces\": …}"}`) instead of at the top level. The schema was
|
|
22
|
+
`{type: 'object', additionalProperties: true}` — no declared properties, no required keys — so
|
|
23
|
+
the wrapper validated cleanly and every field then read as `undefined`: `surfaces` fell back to
|
|
24
|
+
`[]`, `parallel([])` dispatched **nothing**, and because every later guard compares against
|
|
25
|
+
`surfaces`, an empty list made them all vacuously pass. The run finished with a verdict, no code
|
|
26
|
+
written, and no complaint — indistinguishable from a clean run with an empty diff. Fixed in three
|
|
27
|
+
places: `profile-reader.md` now states that the profile's keys go at the top level of the
|
|
28
|
+
structured-output tool (with the wrong shapes shown), the schema declares what it expects, and a
|
|
29
|
+
profile with no surfaces **aborts loudly** instead of proceeding. All three workflows.
|
|
30
|
+
- **`args` given as a JSON string became the feature id.** A caller that JSON-encoded its arguments
|
|
31
|
+
got that whole blob used as the id — which is how a report was written to
|
|
32
|
+
`specs/reports/{"feature": "x"}.md` — and the other options (`maxRounds`, `smoke`) silently read
|
|
33
|
+
as `undefined` on the same run, so a run could skip smoke without saying so. `args` is now parsed
|
|
34
|
+
back into an object (a bare slug is still valid shorthand), and a feature id that is not a slug
|
|
35
|
+
throws with an actionable message **before** anything touches the filesystem — so no junk file
|
|
36
|
+
can be written, and a path-shaped id is rejected.
|
|
37
|
+
- **`/doctor` warned about a file cohorte itself had written.** `/audit` writes
|
|
38
|
+
`specs/refactor-backlog.md` by design; the spec scanner globbed `specs/*.md` and flagged it for
|
|
39
|
+
having no valid front-matter `status`. It fired in every project that had ever run `/audit`. Both
|
|
40
|
+
the conversational `/doctor` and the dashboard port now exclude it.
|
|
41
|
+
- **New: `cohorte metrics` — real cost and runtime per command.** Reconstructs tokens, USD,
|
|
42
|
+
wall/active time and subagent counts from Claude Code's own transcripts, so it needs nothing
|
|
43
|
+
enabled and works retroactively on runs that already happened. It is worktree-aware (a feature
|
|
44
|
+
built across worktrees adds up instead of being dropped), attributes subagent spend back to the
|
|
45
|
+
command that spawned it, and de-duplicates the repeated `usage` blocks a single API response
|
|
46
|
+
writes across several transcript lines — summing those naively inflates tokens ~1.8×.
|
|
47
|
+
`--json`, `--runs`, `--days=N`, `--since=ISO`. Prices live in `scripts/metrics/prices.json`.
|
|
48
|
+
|
|
49
|
+
## 1.3.4 — 2026-07-31
|
|
50
|
+
|
|
51
|
+
> **Re-run `npx cohorte@latest update --global` (or `update`)** — the workflow and script
|
|
52
|
+
> fixes only apply once the installed core is refreshed.
|
|
53
|
+
|
|
54
|
+
- **The dashboard server now has tests.** `dashboard/server/*.js` is shipped runtime code — a
|
|
55
|
+
hand-rolled YAML parser every `/doctor` check derives from, the metrics aggregator, the JS port
|
|
56
|
+
of `/doctor`, the board parser, the fleet registry, and the HTTP guards — with no coverage at
|
|
57
|
+
all. `scripts/test-dashboard.mjs` (83 assertions, in CI) pins each module, including every
|
|
58
|
+
`/doctor` check both green and deliberately broken.
|
|
59
|
+
- **The gate hook now has tests.** `hooks/gate.py` is the one component that can block a command,
|
|
60
|
+
and CI only ever checked that it *parsed* — every one of its shipped regressions reached users
|
|
61
|
+
first. `scripts/test-gate.mjs` drives its real stdin→stdout contract (42 assertions: deny/ask
|
|
62
|
+
tiers, chained-command splitting, branch-conditional gating at the payload's cwd, the
|
|
63
|
+
`bypassPermissions` ask⇒deny escalation, config robustness, the preflight phase gate, worktree
|
|
64
|
+
HEAD matching) and runs in CI. No new defect was found in the gate itself — the behaviour is
|
|
65
|
+
now pinned.
|
|
66
|
+
- **A crashed reviewer scored as a clean surface.** `agent()` returns `null` when a subagent dies,
|
|
67
|
+
and a dead reviewer returns zero findings — byte-identical to a surface with nothing wrong. Both
|
|
68
|
+
`review.js` and `cycle.js` read that as `SHIP`: the review workflow answered "`/ship`" when
|
|
69
|
+
*every* reviewer had crashed, and the cycle workflow exited **SHIP-READY**, ticked the DoD and
|
|
70
|
+
stamped the freshness gate over code nobody had read. Unreviewed surfaces are now named in
|
|
71
|
+
`unreviewedSurfaces` + `questions`, can never score `SHIP`, and the cycle re-reviews instead of
|
|
72
|
+
dispatching an empty fix round. `scripts/test-workflows.mjs` (new, run in CI) pins this.
|
|
73
|
+
- **Swept the whole "dead agent read as success" family across all four workflows** — the same
|
|
74
|
+
root cause as the two above, found at eight more call sites by auditing every `agent()` result
|
|
75
|
+
in `cycle.js` · `review.js` · `audit.js` · `refactor.js`. The worst: a dead **diff-staging**
|
|
76
|
+
agent in `review.js` returned `verdict: SHIP` ("no diff — nothing to review") for a feature
|
|
77
|
+
nobody had looked at; and a dead **close/staging** agent let both `/cycle` and the review
|
|
78
|
+
workflow report `SHIP-READY` + a report path + "ship is a straight shot" when the report, the
|
|
79
|
+
DoD ticks, the freshness stamp and the metrics had never been written (and `/ship`'s freshness
|
|
80
|
+
gate skips silently when those fields are absent, so the human would have shipped on it). Also:
|
|
81
|
+
a dead auditor made a domain look **clean** instead of unaudited; a dead backlog writer/reader
|
|
82
|
+
and a dead item-ticker were each reported as success. Every one of these now distinguishes
|
|
83
|
+
"died" from "succeeded with nothing to say", and `scripts/test-workflows.mjs` pins all of them.
|
|
84
|
+
- **A dead contract agent was reported as a successful re-authoring.** Same failure shape as the
|
|
85
|
+
crashed reviewer: when the lead-equivalent agent that re-authors spec §5 + the contract file
|
|
86
|
+
died, `/cycle` still pushed a `contractChanges` entry and handed **every** consuming surface a
|
|
87
|
+
CRITICAL "the contract was RE-AUTHORED — re-read it and realign" item, pointing at a file
|
|
88
|
+
nobody had touched. It now reports the contract as UNCHANGED and ripples nothing.
|
|
89
|
+
- **A red preflight nobody owns burned every remaining round.** When no surface path appeared in
|
|
90
|
+
the failure tail and no implementer had survived the build, the fix round dispatched *zero*
|
|
91
|
+
agents, the next round found the same red gates, and the loop spun to the cap doing literally
|
|
92
|
+
nothing before reporting a stale verdict. It now stops immediately with the failure tail.
|
|
93
|
+
- **Findings belonging to no surface were dropped silently.** A finding whose file sits outside
|
|
94
|
+
every surface tree — reachable when the diff-staging agent names a key the profile lacks —
|
|
95
|
+
stayed in the open set (so the loop could never exit clean) while nobody was ever dispatched to
|
|
96
|
+
fix it. They are now named, with their `file:line`, in `questions`.
|
|
97
|
+
- **`/cycle` build telemetry hid dead implementers**: results were mapped over the *survivors*, so
|
|
98
|
+
two of three surfaces reported `ok,ok` and the dead one vanished from the funnel entirely.
|
|
99
|
+
- **The review workflow sent HIGH findings straight to `/ship`.** A `SHIP` verdict can legitimately
|
|
100
|
+
carry HIGH/MEDIUM findings (only CRITICAL and security force a fix), but the conversational
|
|
101
|
+
`/review` routes any surviving HIGH to `/fix` — the workflow said `/ship`. It now only recommends
|
|
102
|
+
shipping when nothing above LOW survived, and stamps the freshness gate on that same condition.
|
|
103
|
+
- **Smoke is now opt-in in the cycle workflow** — `/cycle <id> smoke` (or `args.smoke: true`).
|
|
104
|
+
Booting the app every round is expensive and a library project has nothing to smoke. Without it
|
|
105
|
+
the run reports `smoke: "SKIPPED"`, leaves the runtime-flow DoD boxes unticked, and says so.
|
|
106
|
+
- **The dashboard reset could move the shared global core.** Nothing stopped a project path of
|
|
107
|
+
`~` — reset would then rename `~/.claude` into a backup dir and break every repo on the machine,
|
|
108
|
+
while the UI promised the global core is never touched. It now refuses that path outright.
|
|
109
|
+
- **The cycle workflow polluted the metrics with phantom surfaces.** It wrote `rounds` / `verdict` /
|
|
110
|
+
`smoke` inside the metrics line's `surfaces` map, so the dashboard rendered them as three surface
|
|
111
|
+
rows and scored `rounds: "1"` as a failing surface. Run-level facts now sit outside `surfaces`,
|
|
112
|
+
and the dashboard knows the `cycle` phase.
|
|
113
|
+
- **Telemetry from bundled installs reported no core version.** `telemetry-send.sh` read `VERSION`
|
|
114
|
+
only from the *global* core; it now resolves the core that ships it. Payloads are also hardened —
|
|
115
|
+
a quote or newline in the results string used to produce JSON the collector dropped.
|
|
116
|
+
- **Every kanban card move added a blank line.** Ten moves of one card padded a board with fifteen
|
|
117
|
+
of them, and every phase command moves cards. Runs of blank lines are now collapsed; a board is
|
|
118
|
+
byte-stable across moves. Sub-notes of a duplicate card are no longer duplicated either.
|
|
119
|
+
- **Python bytecode could reach the published package and users' `.claude`.** `.npmignore` is inert
|
|
120
|
+
under an explicit `files` allowlist, so its `__pycache__/` rule never fired, and all three
|
|
121
|
+
installers copied the directory verbatim. Excluded at both ends, asserted in CI.
|
|
122
|
+
- **Every non-design surface agent rendered with a blank first TDD step.** `<SURFACE_TDD_STEP1>`
|
|
123
|
+
sat as numbered item 1 of the TDD list but is filled only for `uses_design` surfaces, so every
|
|
124
|
+
other agent got an empty "1." above its real first step. It is now a lead-in paragraph.
|
|
125
|
+
- Doc/template corrections found by reading the whole core against the code: the spec template's
|
|
126
|
+
`## 6+. Surface tasks` could collide with the `§8`/`§9` sections the pipeline references by
|
|
127
|
+
number; `/init-pipeline` step 04 forgot `smoke.md` in its "leave the fixed agents as-is" list;
|
|
128
|
+
the getting-started page placed `implementer.template.md` in `agents/` (it ships in `pipeline/`);
|
|
129
|
+
the dashboard docs never documented the CSRF/DNS-rebinding guard; `dashboard/README.md` was
|
|
130
|
+
missing `metrics.js`, `/api/metrics` and `/audit`; the two reference-only templates
|
|
131
|
+
(`agent-handoff.md`, `review-feedback.md`) are unreferenced copies of shapes that live in the
|
|
132
|
+
agents — `review-feedback.md` had drifted and is re-synced, and both now say so.
|
|
133
|
+
- Smaller: the dashboard no longer stalls for 13 s per project when npm is unreachable (failed
|
|
134
|
+
lookups are cached and de-duplicated); `--port=` rejects a non-numeric value instead of listening
|
|
135
|
+
on a random port; a missing hashed asset 404s instead of being served `index.html`; a project
|
|
136
|
+
card is now keyboard-activatable; long headless logs are trimmed instead of growing unbounded;
|
|
137
|
+
`install.sh --help` exists; the preflight stamp is written once per distinct directory.
|
|
138
|
+
|
|
6
139
|
## 1.3.3 — 2026-07-30
|
|
7
140
|
|
|
8
141
|
> **Re-run `npx cohorte@latest update --global` (or `update`)** — the gate fixes only apply once
|
package/README.md
CHANGED
|
@@ -217,7 +217,6 @@ 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). |
|
|
221
220
|
| `/smoke <id>` | Run the feature for real: infra up, contract endpoints, UI flows, design conformance. |
|
|
222
221
|
| `/review <id>` | Read-only review agents (one per touched surface, parallel) audit the diff vs the spec. |
|
|
223
222
|
| `/fix <id>` | Apply a review/smoke report: remediation into the spec, re-dispatch only the surfaces with findings. |
|
|
@@ -278,13 +277,12 @@ Rules that make it safe:
|
|
|
278
277
|
|
|
279
278
|
### Workflows — deterministic multi-agent runs (opt-in)
|
|
280
279
|
|
|
281
|
-
|
|
280
|
+
Four phases also ship as **workflow scripts** for the Claude Code Workflow runtime — the same
|
|
282
281
|
fan-out the commands orchestrate, but driven by a deterministic script instead of the lead reasoning
|
|
283
282
|
it out turn by turn:
|
|
284
283
|
|
|
285
284
|
| Script | What it runs |
|
|
286
285
|
| ----------------------- | ----------------------------------------------------------------------------------------- |
|
|
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. |
|
|
288
286
|
| `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
287
|
| `workflows/audit.js` | One auditor per domain (every surface + shared) concurrently, prioritized `specs/refactor-backlog.md`. |
|
|
290
288
|
| `workflows/refactor.js` | Big domains only: `shared` first and alone, then parallel surface implementers, per-domain verify + one retry. |
|
|
@@ -297,10 +295,9 @@ The essentials:
|
|
|
297
295
|
- **Prerequisite: Claude Code ≥ 2.1.154** with workflows enabled. `/doctor` (check 8) tells you
|
|
298
296
|
which path your session will take and why.
|
|
299
297
|
- **No input mid-run — questions at the edges.** A workflow runs to completion without asking
|
|
300
|
-
anything
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
command gate still fires inside workflow subagents; in unattended runs its confirms become denies.
|
|
298
|
+
anything: whatever would have been a mid-run question lands in the result at the end. The
|
|
299
|
+
destructive-command gate still fires inside workflow subagents; in unattended runs its confirms
|
|
300
|
+
become denies.
|
|
304
301
|
- Phase 0 of every script is the `profile-reader` agent (haiku) — it reads `PIPELINE.md` and hands
|
|
305
302
|
the script the profile as JSON, since workflow scripts have no filesystem access. Mechanical
|
|
306
303
|
phases run on haiku; judgment phases use the same pinned agents as the commands.
|
package/bin/cli.js
CHANGED
|
@@ -35,6 +35,7 @@ Usage:
|
|
|
35
35
|
cohorte install [target] [--global]
|
|
36
36
|
cohorte update [target] [--global]
|
|
37
37
|
cohorte dashboard [target] [--port=N] [--host=ADDR] [--open]
|
|
38
|
+
cohorte metrics [target] [--days=N] [--since=ISO] [--runs] [--json]
|
|
38
39
|
cohorte version
|
|
39
40
|
|
|
40
41
|
Commands:
|
|
@@ -49,6 +50,11 @@ Commands:
|
|
|
49
50
|
by default (loopback only — its actions execute code). --host=ADDR
|
|
50
51
|
to expose (e.g. --host=0.0.0.0, prints a security warning). --open
|
|
51
52
|
to launch the browser.
|
|
53
|
+
metrics Cost and runtime per command, reconstructed from Claude Code's own
|
|
54
|
+
transcripts (tokens, USD, wall/active time, subagent count). Reads
|
|
55
|
+
~/.claude/projects — nothing to enable, and it covers runs that
|
|
56
|
+
already happened. Worktree-aware, so a feature adds up. --json for
|
|
57
|
+
the raw rollup, --runs to include every individual invocation.
|
|
52
58
|
version Print the installed CLI version.`);
|
|
53
59
|
process.exit(code);
|
|
54
60
|
}
|
|
@@ -64,12 +70,30 @@ let port = parseInt(process.env.COHORTE_DASHBOARD_PORT, 10) || 4317;
|
|
|
64
70
|
let host = process.env.COHORTE_DASHBOARD_HOST || '127.0.0.1';
|
|
65
71
|
let openBrowser = false;
|
|
66
72
|
|
|
73
|
+
// Flags that belong to `metrics` and are forwarded verbatim to the collector. They are
|
|
74
|
+
// listed here rather than parsed, so the collector stays the single source of truth for
|
|
75
|
+
// its own options — but the generic "unknown flag" guard below must not reject them.
|
|
76
|
+
const metricsFlags = [];
|
|
77
|
+
const isMetricsFlag = (a) =>
|
|
78
|
+
a === '--json' || a === '--runs' || a.startsWith('--days=') || a.startsWith('--since=');
|
|
79
|
+
|
|
67
80
|
for (const a of args) {
|
|
68
|
-
if (a === 'install' || a === 'update' || a === 'dashboard') mode = a;
|
|
81
|
+
if (a === 'install' || a === 'update' || a === 'dashboard' || a === 'metrics') mode = a;
|
|
82
|
+
else if (isMetricsFlag(a)) metricsFlags.push(a);
|
|
69
83
|
else if (a === 'version' || a === '--version' || a === '-v') { console.log(VERSION); process.exit(0); }
|
|
70
84
|
else if (a === '--global' || a === '-g') scope = 'global';
|
|
71
|
-
else if (a.startsWith('--port=')) {
|
|
72
|
-
|
|
85
|
+
else if (a.startsWith('--port=')) {
|
|
86
|
+
// A bad value used to land as NaN, which http.listen() silently treats as
|
|
87
|
+
// "any free port" — the banner then printed `localhost:NaN` and nothing worked.
|
|
88
|
+
port = parseInt(a.slice(7), 10);
|
|
89
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
90
|
+
console.error(`error: --port must be an integer 1-65535 (got "${a.slice(7)}")`);
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|
|
93
|
+
} else if (a.startsWith('--host=')) {
|
|
94
|
+
host = a.slice(7).trim();
|
|
95
|
+
if (!host) { console.error('error: --host= needs an address (e.g. --host=0.0.0.0)'); process.exit(2); }
|
|
96
|
+
}
|
|
73
97
|
else if (a === '--open') { openBrowser = true; }
|
|
74
98
|
else if (a === 'help' || a === '--help' || a === '-h') usage(0);
|
|
75
99
|
else if (a.startsWith('-')) { console.error(`error: unknown flag: ${a}`); usage(2); }
|
|
@@ -86,6 +110,19 @@ if (mode === 'dashboard') {
|
|
|
86
110
|
return;
|
|
87
111
|
}
|
|
88
112
|
|
|
113
|
+
// --- metrics: cost + runtime per command ------------------------------------
|
|
114
|
+
// The collector is ESM and this CLI is CommonJS, so it runs as a child process rather
|
|
115
|
+
// than being required. stdio is inherited so --json stays pipeable.
|
|
116
|
+
if (mode === 'metrics') {
|
|
117
|
+
const script = path.join(pkgRoot, 'scripts', 'metrics', 'collect.mjs');
|
|
118
|
+
if (!fs.existsSync(script)) {
|
|
119
|
+
console.error(`error: metrics collector not found at ${script}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const r = spawnSync(process.execPath, [script, target, ...metricsFlags], { stdio: 'inherit' });
|
|
123
|
+
process.exit(r.status == null ? 1 : r.status);
|
|
124
|
+
}
|
|
125
|
+
|
|
89
126
|
// --- paths -------------------------------------------------------------------
|
|
90
127
|
const globalDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
91
128
|
const dest = scope === 'global' ? globalDir : path.join(target, '.claude');
|
|
@@ -103,8 +140,16 @@ function copyCore() {
|
|
|
103
140
|
// Workflow runtime resolves from .claude/workflows (bundled) or ~/.claude/workflows
|
|
104
141
|
// (global) — same copy rule in both modes, like commands.
|
|
105
142
|
for (const d of ['commands', 'hooks', 'templates', 'workflows']) {
|
|
106
|
-
fs.cpSync(path.join(src, 'core', d), path.join(dest, d), {
|
|
143
|
+
fs.cpSync(path.join(src, 'core', d), path.join(dest, d), {
|
|
144
|
+
recursive: true,
|
|
145
|
+
force: true,
|
|
146
|
+
// Never carry a Python bytecode cache into a user's .claude. It appears in a
|
|
147
|
+
// source checkout the moment anyone compiles or imports gate.py (CI does), it
|
|
148
|
+
// is machine- and interpreter-specific, and copy-over never deletes it later.
|
|
149
|
+
filter: s => !s.split(/[\\/]/).includes('__pycache__') && !s.endsWith('.pyc'),
|
|
150
|
+
});
|
|
107
151
|
}
|
|
152
|
+
fs.rmSync(path.join(dest, 'hooks', '__pycache__'), { recursive: true, force: true });
|
|
108
153
|
// 0.1.19 renamed questionnaire-domain-brief.md → research-brief.md; drop the stale copy.
|
|
109
154
|
fs.rmSync(path.join(dest, 'templates', 'questionnaire-domain-brief.md'), { force: true });
|
|
110
155
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
@@ -21,6 +21,28 @@ nothing only when something is wrong.
|
|
|
21
21
|
3. Return **only** the JSON object — no fences, no commentary, no markdown. Your final message is
|
|
22
22
|
parsed by a script.
|
|
23
23
|
|
|
24
|
+
## Returning through a structured-output tool
|
|
25
|
+
|
|
26
|
+
When you are given a tool to return structured output, the profile's own keys go at the **top
|
|
27
|
+
level** of that tool's input — `surfaces`, `commands`, `vcs`, `contract`, and the rest, as real
|
|
28
|
+
JSON values.
|
|
29
|
+
|
|
30
|
+
Do **not** serialize the profile to a string and nest it under a wrapper field. These are wrong:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{"output": "{\"surfaces\": [ … ]}"} ← the profile as text under a wrapper
|
|
34
|
+
{"profile": {"surfaces": [ … ]}} ← the profile nested one level down
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This is right:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{"surfaces": [ … ], "commands": { … }, "vcs": { … }}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A wrapped return costs the caller everything: the script reads `surfaces` as absent, builds
|
|
44
|
+
nothing, and reports success anyway.
|
|
45
|
+
|
|
24
46
|
## Failure shape
|
|
25
47
|
|
|
26
48
|
If `PIPELINE.md` is missing, or the `yaml pipeline-profile` fence is absent or unparseable, return
|
package/core/commands/doctor.md
CHANGED
|
@@ -37,14 +37,16 @@ fix only with the human's go-ahead (or hand them the command).
|
|
|
37
37
|
(`review.md`, `release.md`, `smoke.md`, `profile-reader.md` — repo or `~/.claude/agents/`) must
|
|
38
38
|
each carry their `model:` line too (sonnet/haiku/sonnet/haiku). **Command pins:** every mechanical command file
|
|
39
39
|
(`build`, `review`, `fix`, `smoke`, `ship`, `audit`, `refactor`, `doctor`, `align-ds`,
|
|
40
|
-
`update-pipeline
|
|
40
|
+
`update-pipeline` — in `.claude/commands/` or `~/.claude/commands/`) carries `model: sonnet` in
|
|
41
41
|
its frontmatter — ⚠️ if missing (the lead's orchestration turn then bills at the session model,
|
|
42
42
|
e.g. Opus/Fable). `brainstorm`, `spec`, and `init-pipeline` are intentionally unpinned
|
|
43
43
|
(interactive — they inherit the session model).
|
|
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)
|
|
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.
|
|
@@ -71,7 +73,7 @@ fix only with the human's go-ahead (or hand them the command).
|
|
|
71
73
|
take and why:
|
|
72
74
|
- **Claude Code version** ≥ 2.1.154 (`claude --version 2>/dev/null | head -1`) — older or no CLI
|
|
73
75
|
on PATH ⇒ conversational only.
|
|
74
|
-
- **Scripts present:** `<core>/workflows/review.js` + `audit.js` + `refactor.js`
|
|
76
|
+
- **Scripts present:** `<core>/workflows/review.js` + `audit.js` + `refactor.js` —
|
|
75
77
|
missing on a current core ⇒ half-done install, re-run install/update.
|
|
76
78
|
- **Phase-0 agent present:** `<agents dir>/profile-reader.md` (repo `.claude/agents/` bundled or
|
|
77
79
|
`~/.claude/agents/` global) — the workflows abort without it.
|
|
@@ -82,7 +84,9 @@ fix only with the human's go-ahead (or hand them the command).
|
|
|
82
84
|
End the check with ONE summary line, e.g.
|
|
83
85
|
`workflows: available (opt-in — ask to "run the review workflow")` or
|
|
84
86
|
`workflows: unavailable (<first failing prerequisite>) — conversational commands (the default)`.
|
|
85
|
-
9. **Specs & metrics.** Every `specs/*.md` front-matter `status` is a valid stage
|
|
87
|
+
9. **Specs & metrics.** Every `specs/*.md` front-matter `status` is a valid stage — excluding
|
|
88
|
+
`_`-prefixed files and `specs/refactor-backlog.md`, which `/audit` writes as a backlog, not a
|
|
89
|
+
spec, and which has no front-matter to check; `shipped` specs
|
|
86
90
|
with a live worktree flagged (see 6). `.claude/pipeline-metrics.jsonl` and `specs/reports/` (the
|
|
87
91
|
`/review`·`/smoke` report buffer that lets a `/fix` survive a `/clear`) are gitignored. Metrics
|
|
88
92
|
belong to the **main checkout** — a `pipeline-metrics.jsonl` inside a live feature worktree is a
|
package/core/hooks/gate.py
CHANGED
|
@@ -100,18 +100,33 @@ def current_branch(cwd: str):
|
|
|
100
100
|
return None
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
def
|
|
104
|
-
"""HEAD sha
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
144
|
-
if
|
|
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> ·
|
|
1
|
+
# HANDOFF — <surface> · <feature_id>
|
|
2
2
|
|
|
3
|
-
<!--
|
|
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
|
-
<!--
|
|
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
|
|
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
|
|
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>}}`
|
package/core/workflows/audit.js
CHANGED
|
@@ -21,9 +21,67 @@ export const meta = {
|
|
|
21
21
|
],
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// The Workflow runtime hands `args` to a script verbatim, so a caller that passes a
|
|
25
|
+
// JSON-ENCODED STRING instead of a real object gets that string back here. The old
|
|
26
|
+
// `typeof args === 'string' ? args.trim()` then took the whole blob as the value — which
|
|
27
|
+
// is how a report landed on disk named `specs/reports/{"feature": "x"}.md`, and how
|
|
28
|
+
// maxRounds/smoke were silently dropped on the same run. Parse it back into the object
|
|
29
|
+
// it was meant to be; a bare slug stays valid shorthand.
|
|
30
|
+
const ARGS = (() => {
|
|
31
|
+
if (typeof args === 'string') {
|
|
32
|
+
const t = args.trim()
|
|
33
|
+
if (t.startsWith('{')) {
|
|
34
|
+
try { const o = JSON.parse(t); if (o && typeof o === 'object' && !Array.isArray(o)) return o } catch {}
|
|
35
|
+
}
|
|
36
|
+
return { feature: t, target: t }
|
|
37
|
+
}
|
|
38
|
+
return args && typeof args === 'object' ? args : {}
|
|
39
|
+
})()
|
|
40
|
+
const target = ARGS.target || ''
|
|
25
41
|
|
|
26
|
-
|
|
42
|
+
// The profile-reader returns through a StructuredOutput tool call, and a haiku agent
|
|
43
|
+
// intermittently nests the whole profile as a JSON *string* under a single wrapper field
|
|
44
|
+
// ({"output": "{\"surfaces\": …}"}) instead of putting the profile's keys at the top level.
|
|
45
|
+
// The schema here used to be {type:'object', additionalProperties:true} — no declared
|
|
46
|
+
// properties, no required keys — so that wrapper validated cleanly and every field then read
|
|
47
|
+
// as undefined: `surfaces` fell back to [], parallel([]) dispatched zero agents, the
|
|
48
|
+
// dead-agent guard had no surfaces to find missing, and the run reported a verdict having
|
|
49
|
+
// done nothing. On the surface it is indistinguishable from a clean run with an empty diff.
|
|
50
|
+
// Declaring the shape gives the tool layer something to validate and the agent something to
|
|
51
|
+
// aim at; unwrapProfile() salvages a wrapped return that still gets through; and the
|
|
52
|
+
// zero-surface abort below makes the silent-success path impossible either way.
|
|
53
|
+
// See also the structured-output section of core/agents/profile-reader.md.
|
|
54
|
+
const PROFILE = {
|
|
55
|
+
type: 'object', additionalProperties: true,
|
|
56
|
+
properties: {
|
|
57
|
+
error: { type: 'string', description: 'set ONLY when PIPELINE.md is missing or unparseable' },
|
|
58
|
+
surfaces: {
|
|
59
|
+
type: 'array',
|
|
60
|
+
description: "one entry per surface, at the TOP LEVEL of this object — never a JSON string",
|
|
61
|
+
items: {
|
|
62
|
+
type: 'object', required: ['key'], additionalProperties: true,
|
|
63
|
+
properties: { key: { type: 'string' }, path: { type: 'string' }, agent: { type: 'string' } },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Salvage a profile handed back as JSON text rather than as an object — either the whole
|
|
70
|
+
// return, or nested under a single wrapper field. Anything already shaped like a profile
|
|
71
|
+
// (has `surfaces`, or is the documented `{error}` failure shape) passes through untouched.
|
|
72
|
+
const unwrapProfile = p => {
|
|
73
|
+
if (typeof p === 'string') { try { return JSON.parse(p) } catch { return null } }
|
|
74
|
+
if (!p || typeof p !== 'object') return null
|
|
75
|
+
if (Array.isArray(p.surfaces) || p.error) return p
|
|
76
|
+
for (const v of Object.values(p)) {
|
|
77
|
+
if (typeof v !== 'string') continue
|
|
78
|
+
try {
|
|
79
|
+
const inner = JSON.parse(v)
|
|
80
|
+
if (inner && typeof inner === 'object' && !Array.isArray(inner)) return inner
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
return p
|
|
84
|
+
}
|
|
27
85
|
|
|
28
86
|
const GATES = {
|
|
29
87
|
type: 'object', required: ['failures'], additionalProperties: false,
|
|
@@ -64,15 +122,21 @@ const BACKLOG = {
|
|
|
64
122
|
|
|
65
123
|
// ── Phase 0 — profile ────────────────────────────────────────────────────────
|
|
66
124
|
phase('Profile')
|
|
67
|
-
const profile = await agent(
|
|
125
|
+
const profile = unwrapProfile(await agent(
|
|
68
126
|
'Return this project\'s PIPELINE.md `yaml pipeline-profile` block as JSON, per your instructions.',
|
|
69
127
|
{ agentType: 'profile-reader', label: 'profile', schema: PROFILE, effort: 'low' },
|
|
70
|
-
)
|
|
128
|
+
))
|
|
71
129
|
if (!profile || profile.error) {
|
|
72
130
|
return { error: `profile unreadable: ${(profile && profile.error) || 'profile-reader returned nothing'}` }
|
|
73
131
|
}
|
|
74
132
|
const cmds = profile.commands || {}
|
|
75
133
|
const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
|
|
134
|
+
// A profile with no surfaces cannot do this workflow's work, and every later
|
|
135
|
+
// guard compares against `surfaces` — an empty list makes them all vacuously
|
|
136
|
+
// pass. Fail loudly here instead of finishing with nothing done.
|
|
137
|
+
if (!surfaces.length) {
|
|
138
|
+
return { error: 'profile has no surfaces — nothing would be audited. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /doctor' }
|
|
139
|
+
}
|
|
76
140
|
const quiet = (q, full) => (q && !String(q).startsWith('<') ? q : full ? `${full} 2>&1 | tail -40` : '')
|
|
77
141
|
const scope = target || 'the whole repo'
|
|
78
142
|
|
|
@@ -114,11 +178,20 @@ const audited = await parallel(domains.map(d => () => agent(
|
|
|
114
178
|
{ agentType: 'review', label: `audit:${d.key}`, schema: BACKLOG },
|
|
115
179
|
).then(r => r && { key: d.key, items: r.items, overflow: r.overflow || 0 })))
|
|
116
180
|
const perDomain = audited.filter(Boolean)
|
|
181
|
+
// A dead auditor returns null, and a domain with no result is indistinguishable
|
|
182
|
+
// from a domain with nothing to report — the backlog would simply omit it and the
|
|
183
|
+
// human would read that as "clean". Name them instead.
|
|
184
|
+
const deadDomains = domains.filter(d => !perDomain.some(p => p.key === d.key)).map(d => d.key)
|
|
185
|
+
if (deadDomains.length) log(`Auditor died on: ${deadDomains.join(', ')} — those domains are NOT audited`)
|
|
117
186
|
|
|
118
187
|
// ── Phase 3 — merge + write the backlog ──────────────────────────────────────
|
|
119
188
|
phase('Backlog')
|
|
120
189
|
const SEV = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
|
121
190
|
const body = ['# Refactor backlog', '', `> Generated by the cohorte-audit workflow (scope: ${scope}).`]
|
|
191
|
+
if (deadDomains.length) {
|
|
192
|
+
body.push('', `> ⚠ NOT audited (the auditor died): ${deadDomains.join(', ')} — absence of items below`,
|
|
193
|
+
'> for those domains means "not looked at", not "clean". Re-run the audit for them.')
|
|
194
|
+
}
|
|
122
195
|
let total = 0
|
|
123
196
|
for (const d of perDomain) {
|
|
124
197
|
const items = [...d.items].sort((a, b) => SEV[a.severity] - SEV[b.severity])
|
|
@@ -127,18 +200,26 @@ for (const d of perDomain) {
|
|
|
127
200
|
for (const it of items) body.push(`- [ ] ${it.severity} · ${it.file}:${it.line} · ${it.kind} · ${it.fix}`)
|
|
128
201
|
if (d.overflow) body.push(`- [ ] (+${d.overflow} more beyond the cap — re-audit ${d.key} after this pass)`)
|
|
129
202
|
}
|
|
130
|
-
await agent(
|
|
203
|
+
const written = await agent(
|
|
131
204
|
`Write EXACTLY this content to specs/refactor-backlog.md (overwrite), then return the single word done:\n<<<BACKLOG\n${body.join('\n')}\nBACKLOG`,
|
|
132
205
|
{ model: 'haiku', label: 'write-backlog', effort: 'low' },
|
|
133
206
|
)
|
|
207
|
+
// Returning `backlog: <path>` when the writer died points /refactor at a file
|
|
208
|
+
// that does not exist (or, worse, at the PREVIOUS run's stale backlog).
|
|
209
|
+
const backlogOk = written != null && /done/i.test(String(written))
|
|
134
210
|
|
|
135
211
|
return {
|
|
136
|
-
backlog: 'specs/refactor-backlog.md',
|
|
212
|
+
backlog: backlogOk ? 'specs/refactor-backlog.md' : '(NOT written — the backlog writer died)',
|
|
213
|
+
notAudited: deadDomains, // absence of findings here means "not looked at"
|
|
137
214
|
mechanicalFailures: mech.length,
|
|
138
215
|
domains: Object.fromEntries(perDomain.map(d => [d.key, d.items.length + (d.overflow || 0)])),
|
|
139
216
|
total,
|
|
140
217
|
top: perDomain.flatMap(d => d.items.map(it => ({ ...it, domain: d.key })))
|
|
141
218
|
.sort((a, b) => SEV[a.severity] - SEV[b.severity]).slice(0, 10)
|
|
142
219
|
.map(it => `[${it.severity}] ${it.domain} · ${it.file}:${it.line} — ${it.fix}`),
|
|
143
|
-
next:
|
|
220
|
+
next: !backlogOk
|
|
221
|
+
? 'the backlog was NEVER written (writer died) — the counts above are real but nothing is on disk; re-run the audit'
|
|
222
|
+
: deadDomains.length
|
|
223
|
+
? `re-audit ${deadDomains.join(', ')} (auditor died — not covered), then /refactor <domain>`
|
|
224
|
+
: 'refactor a domain with /refactor <domain> (or the refactor workflow for big domains)',
|
|
144
225
|
}
|