cohorte 2.7.0 → 2.8.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 CHANGED
@@ -7,6 +7,77 @@ short, user-facing, most recent first. One `## <version> — <YYYY-MM-DD>` secti
7
7
  > They are history and are deliberately not rewritten — every command gained a `cohorte-` prefix
8
8
  > in 2.0.0.
9
9
 
10
+ ## 2.8.0 — 2026-08-22
11
+
12
+ - **`/cohorte-loop` is back — as a workflow, which is the whole point.** The 2.2.0 driver was
13
+ retired because it spawned headless child processes it could not supervise: it stalled on a
14
+ permission prompt it could not see, read a missing `build.json` as an empty one, and had a
15
+ session killed mid-write with nothing noticing. `core/workflows/loop.js` runs
16
+ build → review → [fix → review]* for one feature inside the Workflow runtime instead — the
17
+ runtime holds control flow, a dead agent resolves to `null` rather than to silence, and there
18
+ is no prompt to stall on.
19
+
20
+ What it will not do is as designed as what it does: it *verifies* `/cohorte-build`'s outputs
21
+ (frozen spec, fresh `readiness.json`, the lead-authored contract) as preconditions and aborts —
22
+ naming the gap — on anything that is a human's call: a `NOT-READY` spec, a surface the profile
23
+ doesn't own, a blocking finding on the contract file itself. A dead reviewer's zero findings
24
+ can never read as ship (`unreviewed` is checked before `blocking`, in that order on purpose);
25
+ identical blocking findings two rounds running abort as treading water instead of burning the
26
+ remaining rounds; `maxRounds` (default 5) is the last net, never the first. State is four files
27
+ in `specs/reports/` — re-invoking resumes, and every file older than the spec is treated as
28
+ absent. There is **no** `core/commands/cohorte-loop.md` and `validate-core.mjs` now fails if
29
+ one appears: without the Workflow runtime the loop refuses explicitly rather than degrading to
30
+ a lead re-reasoning the fan-out every round at session prices. It stamps `in-progress` /
31
+ `in-review` / `blocked` on the spec — the driver states SCHEMA.md kept alive since 2.2.0 have
32
+ a producer again.
33
+
34
+ To serve it, the review workflow now writes the same `specs/reports/<id>.verdict.json` the
35
+ conversational `/cohorte-review` §3 guarantees (blocking count, normalized `blocking_items`,
36
+ sha256 fingerprint, the degraded `aborted: "preflight"` form) — one machine contract, two
37
+ producers.
38
+
39
+ - **A deny behind an ask was reachable with one click.** `gate.py` scanned command segments in
40
+ order and returned on the first match, so `git commit -m x && node ace migration:fresh`
41
+ surfaced only the benign `git commit` confirm — and the human's single "yes" ran the
42
+ hard-denied migration behind it. Deny patterns are now matched across the whole chain before
43
+ any ask is offered. Global installs pick this up with `cohorte update`; bundled repos via
44
+ `/cohorte-update-pipeline`.
45
+
46
+ - **The SHIP stamp is now earned, not implied.** The conversational `/cohorte-review` ticked the
47
+ DoD and wrote the freshness stamp on every SHIP — including one carrying HIGH findings, which
48
+ `/cohorte-ship`'s gates then happily certified, while the workflow variant refused the same
49
+ state. Both paths now agree: tick + stamp only when nothing above LOW survived; surviving
50
+ HIGH/MEDIUM routes to `/cohorte-fix` (or an explicit park) first.
51
+
52
+ - **A dozen bugs the release audit surfaced**, the sharper ones being: `cohorte install` seeded
53
+ `cohorte.config.yaml` under `~/.claude` even when `CLAUDE_CONFIG_DIR` pointed elsewhere — a
54
+ config no reader ever probed (CI now asserts the seed lands where the readers look);
55
+ the refactor workflow's retry round discarded the items its first verify had already cleared,
56
+ so the backlog re-dispatched finished work — and invoked with a bare `"backend"` it silently
57
+ refactored *every* big domain; the dashboard trusted the absolute paths in a committed
58
+ `runtimes.json`, going all-red on any cloned or moved checkout; `preflight.sh` fed a UTC
59
+ timestamp to a local-time `touch -t`, future-dating the throwaway index west of UTC (and an
60
+ all-empty command list stamped a green preflight that had verified nothing);
61
+ `new-feature.sh` branched worktrees off the *local* default branch its own fetch never
62
+ updated; a CRLF kanban board failed with "column not found" on a column that exists; and a
63
+ dead mechanical-gates agent read as "0 failures" in the audit workflow. Plus a sweep of doc
64
+ drift (the first-feature walkthrough skipped build *and* review; "Node-less" installers that
65
+ require Node; stale counts of agents, scripts and board columns).
66
+
67
+ - **The shell installers are now the thin delegators they already were.** Since 2.2.0 both
68
+ scripts handed everything to `bin/cli.js` and then carried ~300 lines of unreachable legacy
69
+ copy code below the hand-off — dead text that `validate-core`'s "does the installer copy X"
70
+ checks were vacuously matching, which is worse than no check: it reads as coverage. The dead
71
+ code is gone; the checks now assert CI's install dry-run postconditions — tests against the
72
+ copy that actually runs. Along the way: `sh install.sh` from inside a checkout silently
73
+ *cloned the remote* instead of installing the local tree (`$0` arrives with no slash and the
74
+ self-locate case missed it); both installers now refuse a Node older than 18 up front instead
75
+ of half-installing before `fs.cpSync` crashes; and the review agent's `tools:` names both
76
+ retrieval providers (`mcp__serena`, `mcp__graphify`) — one fixed agent file, shared across
77
+ projects, only the wired provider is live in a session, and graphify projects' reviewers were
78
+ silently cut off from their own index.
79
+
80
+
10
81
  ## 2.7.0 — 2026-08-22
11
82
 
12
83
  - **The metrics panel billed Sonnet 5 fifty percent over.** `prices.json` carried $3/$15 because
package/README.md CHANGED
@@ -229,8 +229,8 @@ those actions.
229
229
  ✅/⚠️/❌ checklist (each failure with its fix), the **Surfaces ↔ agents** map from `PIPELINE.md`,
230
230
  and one board: a **Kanban** if the project has a linked Obsidian board (columns + cards from the
231
231
  vault, with clickable PR links + live open/merged/closed status and a ship-date-sorted Shipped
232
- column, via `gh`), otherwise a **Specs board** from `specs/*.md` (by `draft · frozen · in-review ·
233
- shipped`). The Kanban supersedes the Specs board when both would apply.
232
+ column, via `gh`), otherwise a **Specs board** from `specs/*.md` (by `draft · frozen · in-progress ·
233
+ in-review · blocked · shipped`). The Kanban supersedes the Specs board when both would apply.
234
234
  - **Actions** (stream their output live) — **Update / Install core** (the shared global core, or a
235
235
  repo's bundled core); **Init-pipeline / Update-pipeline / Audit**, which run those Claude Code
236
236
  commands **headless** (`claude -p`, autonomous — Init skips the interactive interview, so review
@@ -276,7 +276,7 @@ will do:
276
276
 
277
277
  ```sh
278
278
  npm i -g cohorte
279
- francois ext install TheBidouilleAgency/cohorte
279
+ francois ext install TheBidouilleAgency/francois-plugin-cohorte
280
280
  ```
281
281
 
282
282
  ## Releasing (maintainers)
@@ -365,15 +365,17 @@ Rules that make it safe:
365
365
 
366
366
  ### Workflows — deterministic multi-agent runs (opt-in)
367
367
 
368
- Four phases also ship as **workflow scripts** for the Claude Code Workflow runtime — the same
369
- fan-out the commands orchestrate, but driven by a deterministic script instead of the lead reasoning
370
- it out turn by turn:
368
+ Four **workflow scripts** ship for the Claude Code Workflow runtime — the same fan-out the
369
+ commands orchestrate, but driven by a deterministic script instead of the lead reasoning it out
370
+ turn by turn. Three are opt-in variants of their same-named commands; the fourth, `loop.js`,
371
+ exists **only** as a workflow:
371
372
 
372
373
  | Script | What it runs |
373
374
  | ----------------------- | ----------------------------------------------------------------------------------------- |
374
375
  | `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. |
375
376
  | `workflows/audit.js` | One auditor per domain (every surface + shared) concurrently, prioritized `specs/refactor-backlog.md`. |
376
377
  | `workflows/refactor.js` | Big domains only: `shared` first and alone, then parallel surface implementers, per-domain verify + one retry. |
378
+ | `workflows/loop.js` | `/cohorte-loop` — build → review → [fix → review]* for one feature, unattended and resumable; exits on zero blocking findings, treading water, maxRounds, or anything that needs a human (contract change, unreviewed surface). No conversational fallback, on purpose. |
377
379
 
378
380
  The essentials:
379
381
 
@@ -413,15 +415,15 @@ See `profile/SCHEMA.md` for every field in `PIPELINE.md` and how the pipeline us
413
415
 
414
416
  ```
415
417
  package.json # npm package (cohorte) — semver source of truth
416
- bin/cli.js # the npm CLI: install / update / dashboard / version (cross-platform, no deps)
417
- install.sh # script installer (fresh + --update) for no-Node environments
418
+ bin/cli.js # the npm CLI: install / update / dashboard / specs / doctor / metrics / version (cross-platform, no deps)
419
+ install.sh # script installer (fresh + --update) for npm-less setups — still needs Node
418
420
  install.ps1 # same installer for Windows PowerShell (fresh + -Update)
419
421
  core/ # copied verbatim into ~/.claude (global) or <project>/.claude (bundled)
420
422
  agents/ # implementer.template.md (rendered per surface) + review / release / profile-reader
421
423
  commands/ # init-pipeline + the pipeline commands + /cohorte-update-pipeline
422
424
  hooks/ # gate.py (destructive-command gate; branch-aware; preflight phase gate)
423
425
  templates/ # handoff / brainstorm-return / design-brief / review-feedback / pr-body / spec
424
- workflows/ # opt-in Workflow-runtime scripts: review.js / audit.js / refactor.js
426
+ workflows/ # Workflow-runtime scripts: review.js / audit.js / refactor.js (opt-in variants) + loop.js (workflow-only)
425
427
  profile/
426
428
  PIPELINE.template.md # the profile skeleton /cohorte-init-pipeline fills
427
429
  SCHEMA.md # field reference
package/bin/cli.js CHANGED
@@ -562,7 +562,20 @@ async function seedConfig() {
562
562
  // being told: `~/.claude` for Claude Code (unchanged — existing files stay authoritative),
563
563
  // `~/.cohorte` for every other runtime. The shipped scripts probe both, in that order, so
564
564
  // a human who drives one repo from two agents still has a single board and a single consent.
565
- const cfg = adapter.expandHome(adapter.configPath(runtime));
565
+ //
566
+ // CLAUDE_CONFIG_DIR moves the whole `~/.claude` tree (globalDir already follows it), but
567
+ // adapter.configPath() speaks in literal `~/.claude/…` — expanding through HOME alone
568
+ // seeded a file at a path no reader probes (kanban-move.sh and the dashboard follow
569
+ // CLAUDE_CONFIG_DIR), missed a filled config there (re-seeding disabled defaults beside
570
+ // it — the exact two-file fork this function's comments forbid), and printed a banner
571
+ // path that did not exist. Re-root the claude-shaped path onto globalDir so the seed,
572
+ // the existing-file check, the banner and every reader agree on one file.
573
+ const homeClaude = path.join(os.homedir(), '.claude');
574
+ const rerootClaude = (p) =>
575
+ p === homeClaude || p.startsWith(homeClaude + path.sep)
576
+ ? path.join(globalDir, p.slice(homeClaude.length + 1) || '.')
577
+ : p;
578
+ const cfg = rerootClaude(adapter.expandHome(adapter.configPath(runtime)));
566
579
  fs.mkdirSync(path.dirname(cfg), { recursive: true });
567
580
  // Pre-rename names, newest first — read as a fallback so upgrades don't lose the config.
568
581
  const legacy = ['thebidouille.config.yaml']
@@ -571,7 +584,7 @@ async function seedConfig() {
571
584
  // A second runtime must not fork the config: two files means two boards and two consent
572
585
  // records, and the human edits whichever one they happen to open. The scripts read
573
586
  // `~/.cohorte` first, so seeding it here would SHADOW a filled `~/.claude` copy.
574
- const claudeCfg = adapter.expandHome(adapter.configPath({ id: 'claude' }));
587
+ const claudeCfg = rerootClaude(adapter.expandHome(adapter.configPath({ id: 'claude' })));
575
588
  if (cfg !== claudeCfg && fs.existsSync(claudeCfg)) {
576
589
  console.log(` · reusing your existing ${claudeCfg} (the scripts read it as a fallback)`);
577
590
  return;
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: review
3
3
  description: Read-only reviewer. Compares the implementation against the frozen spec, then audits code quality, security, and (if the profile declares it) mobile-first. Emits the REVIEW REPORT. Dispatched by /cohorte-review — one per touched surface on multi-surface diffs. Cannot modify anything.
4
- tools: Read, Grep, Glob, mcp__serena
4
+ tools: Read, Grep, Glob, mcp__serena, mcp__graphify
5
5
  model: sonnet
6
6
  ---
7
7
 
@@ -40,7 +40,9 @@ be precise and self-contained.
40
40
  a default.
41
41
  - If `retrieval.provider` in `PIPELINE.md` is not `none`, its MCP tools are in your toolset —
42
42
  prefer them over Grep/Glob + whole-file Reads: locate code by symbol, read only the definitions
43
- you need. Fall back to Grep/Read when they are unavailable or come up empty.
43
+ you need. Fall back to Grep/Read when they are unavailable or come up empty. (Your `tools:`
44
+ list names every provider this agent file is shared across — only the one the project actually
45
+ wired is live in your session; the others simply are not there.)
44
46
 
45
47
  ## What you check, in order
46
48
 
@@ -143,7 +143,9 @@ _Only if `contract.enabled`._ From §5 of the spec, write/update the feature's c
143
143
  `<contract.path>/$ARGUMENTS.<contract.ext>` in the profile's `mechanism` (e.g. Zod v4 schemas + inferred
144
144
  types for `shared-types-zod`). Export it from `contract.index` if set. This is the ONLY file the agents
145
145
  share; they import it read-only and must not edit it. If `contract.enabled` is false, the spec prose is
146
- the sync channel — say so and skip. **Postcondition (if `contract.enabled`):**
146
+ the sync channel — say so and skip, but still run `date +%s` on its own (same as the patch skip
147
+ above: §4's wall-clock start comes from here, and a build with no start epoch writes a metrics
148
+ line with no duration). **Postcondition (if `contract.enabled`):**
147
149
  `test -f <contract.path>/$ARGUMENTS.<contract.ext> && date +%s` — the contract file must exist before
148
150
  you dispatch §3, or the stateless agents have nothing to build against (the epoch output is §4's
149
151
  wall-clock start — no separate timing call).
@@ -85,7 +85,7 @@ fix only with the human's go-ahead (or hand them the command).
85
85
  check: CLI resolvable from PATH, `.mcp.json` entry present in PATH-proof launcher form,
86
86
  `.serena/` gitignored, server actually connects.
87
87
  5. **Design** (if `design.enabled`). `snapshot_dir` exists and is committed; `ui_kit_path` +
88
- `tokens_path` exist; if `provider: claude-design`, `DesignSync` responds (`list_projects`) and
88
+ `tokens_path` exist; if `provider: claude-design`, `DesignSync` responds (`list_files` on the `design_system_project`) and
89
89
  `design_system_project` is reachable. Recall: spec `design_files` are full
90
90
  `…/design/p/<projectId>?file=<file>` links that carry their own project + page; `design_project` is
91
91
  only a legacy fallback for old bare-filename specs (default `none`).
@@ -114,8 +114,10 @@ fix only with the human's go-ahead (or hand them the command).
114
114
  check; do NOT raise it to match a newer feature's floor, or every install between the two
115
115
  versions reads as broken while its workflows run fine. When `design.inline` is on, report the
116
116
  design floor (≥ 2.1.234) as its own line under check 8b — separate prerequisite, separate verdict.
117
- - **Scripts present:** `<core>/workflows/review.js` + `audit.js` + `refactor.js` —
118
- missing on a current core ⇒ half-done install, re-run install/update.
117
+ - **Scripts present:** `<core>/workflows/review.js` + `audit.js` + `refactor.js` + `loop.js`
118
+ missing on a current core ⇒ half-done install, re-run install/update. (`/cohorte-loop` is
119
+ **workflow-only** — no command file exists on purpose; without this runtime it refuses
120
+ rather than degrading to a conversational loop.)
119
121
  - **Phase-0 agent present:** `<agents>/profile-reader.md` — the workflows abort without it.
120
122
  - **Workflows enabled in this session** — the `Workflow` tool is in your own toolset right now;
121
123
  absent ⇒ disabled for this session (a setting or an old client), conversational path.
@@ -31,8 +31,8 @@ that change the *contract*; `/cohorte-fix` is for everything else.
31
31
  re-author the contract file yourself now (lead-only, per `/cohorte-build` §2) — agents never edit it. If
32
32
  the contract change ripples into surfaces *without* findings, fall back to full `/cohorte-build` instead
33
33
  and say so.
34
- - **Note the epoch** (`date +%s`) in the first Bash call you make here — §3's metrics line and usage
35
- ping both carry `seconds`, and there is no separate timing call.
34
+ - **Note the epoch** (`date +%s`) in the first Bash call you make here — §3's metrics line carries
35
+ `seconds`, and there is no separate timing call.
36
36
 
37
37
  ## 2. Scope the re-dispatch — only surfaces with findings
38
38
 
@@ -42,10 +42,11 @@ that change the *contract*; `/cohorte-fix` is for everything else.
42
42
  to the most relevant surface — say which.
43
43
  - Re-dispatch **ONLY the surfaces owning ≥1 item**, in parallel, in a **single message** — the exact
44
44
  dispatch template from `/cohorte-build` §3 (one byte-stable template for builds and fix loops; you do NOT
45
- paste a diff — the agent computes its own, scoped to its tree). Fill the template's final variable
46
- slot with that surface's open `- [ ]` item lines **verbatim**, so the agent needs no spec re-read to
45
+ paste a diff — the agent computes its own, scoped to its tree). Fill the template's **Remediation
46
+ slot** with that surface's open `- [ ]` item lines **verbatim**, so the agent needs no spec re-read to
47
47
  find its work; fill the design slot with `none` when a `uses_design` surface's open items are all
48
- non-visual (no DesignSync re-fetch for a type fix). Surfaces without findings are NOT re-dispatched
48
+ non-visual (no DesignSync re-fetch for a type fix), and the readiness-gaps slot with `none` (a fix
49
+ loop has no fresh §1.6 verdict). Surfaces without findings are NOT re-dispatched —
49
50
  that is the point.
50
51
 
51
52
  ## 3. Integrate & check off what's fixed
@@ -136,9 +136,11 @@ between the pipeline and any automated driver, which parses no prose:
136
136
 
137
137
  - **`blocking` = CRITICAL findings + `security` findings, deduplicated** (a finding that is both
138
138
  counts once). That is exactly the agent's existing verdict rule restated as a number, so
139
- `blocking == 0` ⟺ `verdict == SHIP`. HIGH/MEDIUM/LOW quality findings are **not** blocking
140
- they follow the `deferred:<id>` backlog route below, and must never cost a driver an iteration.
141
- - **`blocking_items`** one normalized string per blocking finding, `<surface>|<file>|<problem>`:
139
+ `blocking == 0` ⟺ `verdict == SHIP`. HIGH/MEDIUM/LOW quality findings are **not** blocking and
140
+ must never cost a driver an iteration a *human* routes them in the verdict branch below
141
+ (a `/cohorte-fix` pass, or a deliberate park in the backlog); a driver ignores them.
142
+ - **`blocking_items`** — one normalized string per **distinct** blocking finding (deduplicated —
143
+ two findings sharing surface, file and problem head are one identity), `<surface>|<file>|<problem>`:
142
144
  the file path **without the `:line`** (a fix that inserts lines shifts every line below it — a
143
145
  line-bearing identity would change every pass and the drift detection would never fire), and the
144
146
  **problem**, not the fix, cut to its first 8 words, lowercased, every run of non-alphanumerics
@@ -181,25 +183,33 @@ In chat print ONLY: the verdict, the severity-count table, a one-line digest of
181
183
  finding, and `Full report: specs/reports/$ARGUMENTS.md` — never echo the findings body into chat (it
182
184
  would sit in this session's history, re-sent every turn). Then:
183
185
 
184
- - **SHIP** → only reachable with `unreviewed` empty (the roll call above forbids it otherwise). A SHIP
185
- verdict *is* the pipeline's statement that the feature meets its Definition of
186
- Done, so **tick the DoD**: in `specs/$ARGUMENTS.md` §`Acceptance criteria / DoD`, flip each `- [ ]`
187
- `- [x]` for the criteria the pipeline has actually verified — spec conformance + `ui_language`
188
- copy (this review), tests · lint · typecheck (a green `/cohorte-build`), mobile-first as far as the code
189
- shows it (this review). **Leave `- [ ]` (and say which) any item no stage actually verified** —
190
- nothing in the pipeline *runs* the feature, so any criterion that needs the app up (runtime flows,
191
- a visual check against the design) stays open unless the human says they exercised it by hand and
192
- it held. Ticking is the lead's job
193
- (the reviewer is read-only). **Then stamp the freshness gate** so `/cohorte-ship` can refuse to ship code
194
- edited after this verdict: compute `BASE=$(git merge-base <default_branch> HEAD)` and write into the
195
- spec front-matter `reviewed_base: $BASE` plus
196
- `reviewed_digest: $(git diff $BASE -- . ':(exclude)specs/' | sha256sum | cut -c1-16)` — the fingerprint
197
- of exactly the source you just reviewed (specs excluded, so DoD ticks + the ship status flip don't
198
- trip it). Then tell the human they can `/cohorte-ship` **recommend a `/clear` first**, the handoff is
199
- fully on disk. **SHIP with leftover LOW findings** (or LOW+MEDIUM at the human's call) does NOT
200
- force a fix cycle for nits: park them through §3.5's exact route (the backlog, under their surface's
201
- domain heading, tagged `deferred:$ARGUMENTS` never as open `## Remediation` items, which would
202
- re-trigger the fix loop), keep the SHIP verdict and the freshness stamp, and let the human ship.
186
+ - **SHIP** → only reachable with `unreviewed` empty (the roll call above forbids it otherwise).
187
+ **Split on what survived — the DoD tick and the freshness stamp are earned by a clean bill, not
188
+ by the verdict alone** (the workflow variant enforces the same rule; certifying surviving
189
+ HIGH/MEDIUM findings for `/cohorte-ship` would ship known defects):
190
+ - **Nothing above LOW survived** the clean bill. **Tick the DoD**: in `specs/$ARGUMENTS.md`
191
+ §`Acceptance criteria / DoD`, flip each `- [ ]` `- [x]` for the criteria the pipeline has
192
+ actually verified spec conformance + `ui_language` copy (this review), tests · lint ·
193
+ typecheck (a green `/cohorte-build`), mobile-first as far as the code shows it (this review).
194
+ **Leave `- [ ]` (and say which) any item no stage actually verified** — nothing in the pipeline
195
+ *runs* the feature, so any criterion that needs the app up (runtime flows, a visual check
196
+ against the design) stays open unless the human says they exercised it by hand and it held.
197
+ Ticking is the lead's job (the reviewer is read-only). **Then stamp the freshness gate** so
198
+ `/cohorte-ship` can refuse to ship code edited after this verdict: compute
199
+ `BASE=$(git merge-base <default_branch> HEAD)` and write into the spec front-matter
200
+ `reviewed_base: $BASE` plus `reviewed_digest: $(git diff $BASE -- . ':(exclude)specs/' | sha256sum | cut -c1-16)`
201
+ (`shasum -a 256` then the first 16 hex chars where there is no `sha256sum` — macOS) the
202
+ fingerprint of exactly the source you just reviewed (specs excluded, so DoD ticks + the ship
203
+ status flip don't trip it). Park the leftover LOWs through §3.5's exact route (the backlog,
204
+ under their surface's domain heading, tagged `deferred:$ARGUMENTS` never as open
205
+ `## Remediation` items, which would re-trigger the fix loop). Then tell the human they can
206
+ `/cohorte-ship` — **recommend a `/clear` first**, the handoff is fully on disk.
207
+ - **HIGH or MEDIUM findings survived** — the verdict stays SHIP (they are not blocking), but
208
+ **no DoD tick and no freshness stamp**. Route them: `/cohorte-fix $ARGUMENTS` for what should
209
+ be fixed now; MEDIUMs the human *explicitly* chooses to live with go through §3.5's park
210
+ instead — and once everything left is LOW, apply the clean-bill branch above (tick, stamp,
211
+ ship). The stamp is the pipeline certifying what ships; it is never written over known
212
+ HIGH/MEDIUM defects.
203
213
  - **REVISE / BLOCK**, or any CRITICAL/HIGH/security finding → tell the human to run
204
214
  **`/cohorte-fix $ARGUMENTS`** — it appends the report to the spec's `## Remediation` and re-dispatches ONLY
205
215
  the surfaces with findings. The full path (`/cohorte-spec` Mode B then `/cohorte-build`) remains for findings that
@@ -23,7 +23,8 @@ You are the **lead**. Ship feature **$ARGUMENTS**. This is the outward-facing ga
23
23
  verdict was REVISE/BLOCK, stop and say so.
24
24
  - **Freshness gate** — the reviewed code must be exactly what ships. If the spec front-matter carries
25
25
  `reviewed_base` + `reviewed_digest`, recompute
26
- `git diff <reviewed_base> -- . ':(exclude)specs/' | sha256sum | cut -c1-16` and compare to
26
+ `git diff <reviewed_base> -- . ':(exclude)specs/' | sha256sum | cut -c1-16` (`shasum -a 256`
27
+ then the first 16 hex chars where there is no `sha256sum` — macOS) and compare to
27
28
  `reviewed_digest`. **Match** ⇒ source unchanged since the SHIP verdict, proceed. **Mismatch** ⇒ source
28
29
  (or the contract) was edited after review — the verdict is **stale**: stop and tell the human to re-run
29
30
  `/cohorte-review $ARGUMENTS` before shipping. Missing fields (spec predates the gate) ⇒ skip, don't block.
@@ -91,7 +91,7 @@ Detect the mode from the pasted content:
91
91
  `specs/reports/<id>.md`, where `/cohorte-review` stages its last report. Identify `feature_id` from its
92
92
  header; open `specs/<id>.md`.
93
93
  2. Append each finding to the spec's **`## Remediation`**, one per line:
94
- `- [ ] <severity> · <file:line> · <spec-violation|quality|security> · <concrete fix>`
94
+ `- [ ] <severity> · <file:line> · <spec-violation|quality|security|complexity> · <concrete fix>`
95
95
  (Keep prior items; add the new round under a dated/numbered subheading.)
96
96
  3. If a finding implies the **contract** must change, update §5 and flag it so the lead re-authors the
97
97
  contract file.
@@ -454,13 +454,19 @@ def check_bash(payload: dict, cfg: dict) -> int:
454
454
  branch_cache[directory] = branch is None or branch == default
455
455
  return branch_cache[directory]
456
456
 
457
- for raw in SPLIT.split(command):
458
- seg = norm(raw)
459
- if not seg:
460
- continue
457
+ segments = [s for s in (norm(raw) for raw in SPLIT.split(command)) if s]
458
+
459
+ # Deny wins over ask across the WHOLE chain, not just in segment order. Scanning
460
+ # segment-by-segment returned on the first match, so `git commit -m x && node ace
461
+ # migration:fresh` surfaced only the benign `git commit` ask — and the human's one
462
+ # confirmation ran the hard-denied command behind it. Scan every segment for deny
463
+ # patterns first; only a fully deny-free chain gets an ask.
464
+ for seg in segments:
461
465
  for pat in deny:
462
466
  if norm(pat) in seg:
463
467
  return decide("deny", f"`{pat}` is forbidden by the project's PIPELINE.md gate.")
468
+
469
+ for seg in segments:
464
470
  for pat in ask:
465
471
  if norm(pat) in seg:
466
472
  return decide(ask_decision,
@@ -157,8 +157,15 @@ const gates = await agent(
157
157
  'the file and return each failure as file/line/kind/one-line summary, capped at 40 (set overflow for the rest).',
158
158
  { model: 'haiku', label: 'gates', schema: GATES, effort: 'low' },
159
159
  )
160
+ // A dead gates agent is NOT "zero mechanical failures" — silence would read as the
161
+ // cleanest possible inventory from a phase that never ran (SCHEMA.md §Dead agents).
162
+ // The domain auditors still run (they read the staged gates file, which then simply
163
+ // isn't there), but the run must say the mechanical sweep is uncovered.
164
+ const gatesCovered = gates != null
160
165
  const mech = (gates && gates.failures) || []
161
- log(`Mechanical failures: ${mech.length}${gates && gates.overflow ? ` (+${gates.overflow} overflow)` : ''}`)
166
+ log(gatesCovered
167
+ ? `Mechanical failures: ${mech.length}${gates.overflow ? ` (+${gates.overflow} overflow)` : ''}`
168
+ : 'the gates agent died — mechanical checks NOT covered this run')
162
169
 
163
170
  // ── Phase 2 — one auditor per domain, concurrent ─────────────────────────────
164
171
  // Domains = every surface + `shared` (contract package + anything outside the
@@ -195,6 +202,10 @@ if (deadDomains.length) {
195
202
  body.push('', `> ⚠ NOT audited (the auditor died): ${deadDomains.join(', ')} — absence of items below`,
196
203
  '> for those domains means "not looked at", not "clean". Re-run the audit for them.')
197
204
  }
205
+ if (!gatesCovered) {
206
+ body.push('', '> ⚠ Mechanical gates NOT run (the gates agent died) — lint/typecheck/test failures',
207
+ '> are uncounted below. Re-run the audit for the mechanical sweep.')
208
+ }
198
209
  let total = 0
199
210
  for (const d of perDomain) {
200
211
  const items = [...d.items].sort((a, b) => SEV[a.severity] - SEV[b.severity])
@@ -214,7 +225,8 @@ const backlogOk = written != null && /done/i.test(String(written))
214
225
  return {
215
226
  backlog: backlogOk ? 'specs/refactor-backlog.md' : '(NOT written — the backlog writer died)',
216
227
  notAudited: deadDomains, // absence of findings here means "not looked at"
217
- mechanicalFailures: mech.length,
228
+ mechanicalFailures: gatesCovered ? mech.length : null, // null = gates agent died, NOT zero
229
+ gatesCovered,
218
230
  domains: Object.fromEntries(perDomain.map(d => [d.key, d.items.length + (d.overflow || 0)])),
219
231
  total,
220
232
  top: perDomain.flatMap(d => d.items.map(it => ({ ...it, domain: d.key })))