cohorte 1.3.2 → 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.
- package/CHANGELOG.md +119 -0
- package/README.md +4 -4
- package/bin/cli.js +22 -4
- package/core/agents/implementer.template.md +10 -5
- package/core/commands/cycle.md +15 -8
- package/core/commands/doctor.md +3 -1
- package/core/hooks/gate.py +57 -26
- 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 +8 -3
- package/core/workflows/audit.js +20 -3
- package/core/workflows/cycle.js +157 -41
- package/core/workflows/refactor.js +16 -5
- package/core/workflows/review.js +59 -7
- 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 +60 -19
- package/dashboard/server/fleet.js +19 -5
- package/dashboard/server/index.js +79 -7
- package/dashboard/server/metrics.js +15 -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 +28 -9
- package/scripts/kanban-move.sh +34 -20
- 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 +356 -0
- package/scripts/test-gate.mjs +273 -0
- package/scripts/test-workflows.mjs +443 -0
- package/scripts/validate-core.mjs +49 -0
- package/dashboard/dist/assets/index-Cj0SpgEY.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,125 @@
|
|
|
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
|
+
|
|
96
|
+
## 1.3.3 — 2026-07-30
|
|
97
|
+
|
|
98
|
+
> **Re-run `npx cohorte@latest update --global` (or `update`)** — the gate fixes only apply once
|
|
99
|
+
> the installed `hooks/gate.py` is refreshed.
|
|
100
|
+
|
|
101
|
+
- **The cycle workflow could exit SHIP-READY with open findings.** A round with only HIGH/MEDIUM
|
|
102
|
+
findings scored `SHIP`, broke the loop, ticked the DoD and stamped the freshness gate — making
|
|
103
|
+
`/ship` a straight shot over unfixed findings, against the workflow's own "zero open findings"
|
|
104
|
+
contract. The exit condition is now literally zero open findings + a smoke PASS.
|
|
105
|
+
- **Dead implementers went undetected in the cycle workflow.** `agent()` returns `null` when a
|
|
106
|
+
subagent dies, but the build fan-out wrapped every result in a truthy object before the check —
|
|
107
|
+
so the "implementer(s) died" question never fired and build telemetry always said `ok`.
|
|
108
|
+
- **`gate.py` gated worktree commands as if they ran on the default branch.** Branch and HEAD were
|
|
109
|
+
resolved in `CLAUDE_PROJECT_DIR` (the main checkout, usually on `main`) instead of where the
|
|
110
|
+
command actually runs — so in a feature worktree, every `ask_on_default_branch` pattern
|
|
111
|
+
prompted, and the preflight HEAD-moved check compared against the wrong checkout. Git state now
|
|
112
|
+
resolves at the hook payload's `cwd`.
|
|
113
|
+
- **The preflight phase gate hung headless runs.** The bypassPermissions "nobody can answer an
|
|
114
|
+
ask ⇒ deny" escalation only covered Bash patterns; a review/smoke Task dispatch with a stale
|
|
115
|
+
stamp still emitted an unanswerable `ask`. The phase gate now escalates the same way.
|
|
116
|
+
- **`/init-pipeline` bundled installs registered the gate with the dead `Bash`-only matcher** —
|
|
117
|
+
the exact bug 1.3.2 fixed in the installers lived on in the template — and never dropped an
|
|
118
|
+
existing registration, so a bundled repo later switched to global ran the gate twice per
|
|
119
|
+
command. The template now mandates `Bash|Task` and a reconcile.
|
|
120
|
+
- Smaller cycle-workflow fixes: smoke telemetry reports the real failure count (was always 0 —
|
|
121
|
+
it filtered on a `kind` value that doesn't exist); a run whose last round ends on a red
|
|
122
|
+
preflight now flags that the reported findings are from the previous round; a malformed
|
|
123
|
+
preflight stamp says "unreadable" instead of "not found".
|
|
124
|
+
|
|
6
125
|
## 1.3.2 — 2026-07-30
|
|
7
126
|
|
|
8
127
|
> **Re-run `npx cohorte@latest update --global` (or `update`).** This release repairs the gate
|
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 →
|
|
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
|
-
|
|
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 →
|
|
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=')) {
|
|
72
|
-
|
|
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), {
|
|
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
|
-
|
|
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.
|
package/core/commands/cycle.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
model: sonnet
|
|
3
|
-
description: Launch the full dev-cycle workflow (contract → build →
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
package/core/commands/doctor.md
CHANGED
|
@@ -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)
|
|
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.
|
package/core/hooks/gate.py
CHANGED
|
@@ -48,9 +48,12 @@ SPLIT = re.compile(r"&&|\|\||[;|\n]")
|
|
|
48
48
|
WS = re.compile(r"\s+")
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def project_root() -> str:
|
|
52
|
+
return os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
53
|
+
|
|
54
|
+
|
|
51
55
|
def load_config() -> dict:
|
|
52
|
-
|
|
53
|
-
path = os.path.join(root, ".claude", "gate-config.json")
|
|
56
|
+
path = os.path.join(project_root(), ".claude", "gate-config.json")
|
|
54
57
|
empty = {"deny": [], "ask": [], "ask_on_default_branch": [], "default_branch": "main",
|
|
55
58
|
"preflight": {}}
|
|
56
59
|
try:
|
|
@@ -76,13 +79,19 @@ def norm(s: str) -> str:
|
|
|
76
79
|
return WS.sub(" ", s.strip())
|
|
77
80
|
|
|
78
81
|
|
|
79
|
-
def
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
+
def session_cwd(payload: dict) -> str:
|
|
83
|
+
"""Where the gated command actually runs. A feature worktree is its own
|
|
84
|
+
checkout — resolving git state in CLAUDE_PROJECT_DIR (the main checkout)
|
|
85
|
+
would gate every worktree command as if it ran on the default branch."""
|
|
86
|
+
return payload.get("cwd") or project_root()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def current_branch(cwd: str):
|
|
90
|
+
"""The checked-out branch at `cwd`, or None (not a repo / detached / git absent)."""
|
|
82
91
|
try:
|
|
83
92
|
out = subprocess.run(
|
|
84
93
|
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
85
|
-
cwd=
|
|
94
|
+
cwd=cwd, capture_output=True, text=True, timeout=3,
|
|
86
95
|
)
|
|
87
96
|
if out.returncode == 0:
|
|
88
97
|
return out.stdout.strip() or None
|
|
@@ -91,19 +100,33 @@ def current_branch():
|
|
|
91
100
|
return None
|
|
92
101
|
|
|
93
102
|
|
|
94
|
-
def
|
|
95
|
-
"""HEAD sha
|
|
96
|
-
|
|
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()
|
|
97
109
|
try:
|
|
98
110
|
out = subprocess.run(
|
|
99
111
|
["git", "rev-parse", "HEAD"],
|
|
100
|
-
cwd=
|
|
112
|
+
cwd=cwd, capture_output=True, text=True, timeout=3,
|
|
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,
|
|
101
122
|
)
|
|
102
123
|
if out.returncode == 0:
|
|
103
|
-
|
|
124
|
+
for line in out.stdout.splitlines():
|
|
125
|
+
if line.startswith("HEAD "):
|
|
126
|
+
heads.add(line.split(" ", 1)[1].strip())
|
|
104
127
|
except Exception:
|
|
105
128
|
pass
|
|
106
|
-
return
|
|
129
|
+
return heads
|
|
107
130
|
|
|
108
131
|
|
|
109
132
|
def check_preflight(payload: dict, cfg: dict) -> int:
|
|
@@ -116,29 +139,37 @@ def check_preflight(payload: dict, cfg: dict) -> int:
|
|
|
116
139
|
if subagent not in agents:
|
|
117
140
|
return 0
|
|
118
141
|
|
|
119
|
-
|
|
120
|
-
stamp = os.path.join(root, ".claude", "preflight.ok")
|
|
142
|
+
stamp = os.path.join(project_root(), ".claude", "preflight.ok")
|
|
121
143
|
why = None
|
|
122
144
|
try:
|
|
123
145
|
with open(stamp, "r", encoding="utf-8") as fh:
|
|
124
|
-
|
|
125
|
-
age_min = (time.time() - float(epoch_s)) / 60
|
|
126
|
-
max_age = float(pf.get("max_age_minutes", 30) or 30)
|
|
127
|
-
if age_min > max_age:
|
|
128
|
-
why = f"the preflight stamp is {age_min:.0f} min old (max {max_age:.0f})"
|
|
129
|
-
else:
|
|
130
|
-
head = current_head()
|
|
131
|
-
if head and sha not in ("", "none") and head != sha:
|
|
132
|
-
why = "HEAD moved since the preflight ran"
|
|
146
|
+
raw = fh.read().strip()
|
|
133
147
|
except Exception:
|
|
148
|
+
raw = None
|
|
134
149
|
why = "no preflight stamp found"
|
|
150
|
+
if raw is not None:
|
|
151
|
+
try:
|
|
152
|
+
epoch_s, _, sha = raw.partition(" ")
|
|
153
|
+
age_min = (time.time() - float(epoch_s)) / 60
|
|
154
|
+
max_age = float(pf.get("max_age_minutes", 30) or 30)
|
|
155
|
+
if age_min > max_age:
|
|
156
|
+
why = f"the preflight stamp is {age_min:.0f} min old (max {max_age:.0f})"
|
|
157
|
+
else:
|
|
158
|
+
heads = known_heads(session_cwd(payload))
|
|
159
|
+
if heads and sha not in ("", "none") and sha not in heads:
|
|
160
|
+
why = "HEAD moved since the preflight ran"
|
|
161
|
+
except Exception:
|
|
162
|
+
why = "the preflight stamp is unreadable (expected `<epoch> <sha>`)"
|
|
135
163
|
if why is None:
|
|
136
164
|
return 0
|
|
165
|
+
# Same rule as the Bash gate: unattended runs have nobody to answer an "ask".
|
|
166
|
+
unattended = payload.get("permission_mode") == "bypassPermissions"
|
|
137
167
|
return decide(
|
|
138
|
-
"ask",
|
|
168
|
+
"deny" if unattended else "ask",
|
|
139
169
|
f"Phase gate: dispatching `{subagent}` but {why}. Run the deterministic pre-flight first "
|
|
140
170
|
f"(pipeline/scripts/preflight.sh — typecheck + lint + tests) so agents never review red code; "
|
|
141
|
-
f"or confirm to dispatch anyway (PIPELINE.md gate.preflight)."
|
|
171
|
+
f"or confirm to dispatch anyway (PIPELINE.md gate.preflight)."
|
|
172
|
+
+ (" (denied outright: unattended run, nobody to confirm)" if unattended else ""),
|
|
142
173
|
)
|
|
143
174
|
|
|
144
175
|
|
|
@@ -173,7 +204,7 @@ def main() -> int:
|
|
|
173
204
|
# be conservative and gate. Resolve the branch once, lazily.
|
|
174
205
|
on_default = False
|
|
175
206
|
if branch_gated:
|
|
176
|
-
branch = current_branch()
|
|
207
|
+
branch = current_branch(session_cwd(payload))
|
|
177
208
|
on_default = branch is None or branch == default
|
|
178
209
|
|
|
179
210
|
for raw in SPLIT.split(command):
|
|
@@ -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>}}`
|
|
@@ -30,8 +31,12 @@
|
|
|
30
31
|
retrieval provider's MCP tools when wired (e.g. `mcp__serena`). Never allowlist anything matching
|
|
31
32
|
a `gate.ask`/`gate.deny` pattern. Mention the human can widen it later with
|
|
32
33
|
`/fewer-permission-prompts`) + the hooks, **conditioned on the install mode:**
|
|
33
|
-
- **bundled:** register the PreToolUse
|
|
34
|
-
|
|
34
|
+
- **bundled:** register the PreToolUse hook `.claude/hooks/gate.py` with matcher `Bash|Task`
|
|
35
|
+
(Task is required — the preflight phase gate keys off Task dispatches; a `Bash`-only matcher
|
|
36
|
+
leaves it dead) and the PostToolUse formatter (detected formatter). Before adding, drop any
|
|
37
|
+
existing PreToolUse entry whose command ends in `gate.py` (here AND in `~/.claude/settings.json`
|
|
38
|
+
if one points at this repo's copy) — exactly one registration must survive, or every gated
|
|
39
|
+
command prompts twice.
|
|
35
40
|
- **global:** the PreToolUse gate hook is
|
|
36
41
|
already in `~/.claude/settings.json` and reads this repo's `gate-config.json` — do **not** re-register
|
|
37
42
|
it here (double-registration double-prompts). It no-ops where its config is absent, so one
|
package/core/workflows/audit.js
CHANGED
|
@@ -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:
|
|
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
|
}
|