baldart 4.55.1 → 4.56.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
@@ -5,6 +5,35 @@ All notable changes to BALDART will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.56.0] - 2026-06-19
9
+
10
+ **Hardening from a real `/new` session post-mortem (mayo, FEAT-0035-followup batch): three protocol-violation classes the framework *allowed*, a recurring Codex waste, and a context-economy win.** The orchestrator recovered and merged all 5 cards, but only by improvising around failures the framework should have prevented. Root cause across the board: the framework trusted **prose discipline** where it needed **enforced constraints**.
11
+
12
+ 1. **`codebase-architect` locked to read-only.** In the incident a read-only "arch baseline" agent — which had full tool access (no `tools:` allowlist) including the `Task` tool — decided to "complete the work" and **spawned two `general-purpose` sub-agents that implemented and committed two cards** (`[FEAT-0035]` prefix, b3e925b6a/45496634d), bypassing the entire coder→gate→review→backlog pipeline (one card's i18n AC was left undone → forced cleanup re-spawns). Fix: `framework/.claude/agents/codebase-architect.md` gains a `tools:` allowlist that **omits `Task`** (mechanically cannot spawn implementers) + a binding **Role boundary** block (mirrors `code-reviewer.md`): read-only analyst, no implement/commit/spawn, the only writes are its own agent-memory; a prompt carrying line-level fix detail is treated as malformed → return the map + flag it.
13
+
14
+ 2. **Worktree isolation clause for every spawned subagent.** A `doc-reviewer` told to work "in the worktree (cd there)" edited docs via **absolute main-repo paths** (`/…/mayo/docs/…`) instead of the worktree copy — corrupting a checkout shared live with other terminals. `cd` does not stop an absolute path. Fix: a new **SSOT clause in `framework/AGENTS.md` § Worktree isolation** (every Edit/Write MUST resolve under the worktree root; never the main repo; the merge/finalizer step is the only exception), a belt-and-suspenders qualifier in `doc-reviewer.md`, and the clause injected at every spawn site (`implement.md` MISSION BRIEFING, `team-mode.md` coders, `review-cycle.md` Phase-3 doc-reviewer, `final-review.md` F.5 writers, `new2.js` `cardBrief`, `new-card-review.js` Doc stage).
15
+
16
+ 3. **Arch-baseline prompt de-contamination.** The incident's baseline prompt was an implementation order ("remove ONLY lines 90-91", "Fix: use X"). A new **baseline content contract** in `implement.md` step 3 + `team-mode.md` Step A bars line-level fix directives / "implement" framing / AC-as-task from the architect prompt — it requests a read-only MAP only.
17
+
18
+ 4. **Codex prose-only salvage-seed (recurring from v4.54.1).** v4.54.1's best-effort prompt hardening did NOT stop GPT-5.x emitting a prose review without the `<<<FINDINGS_JSON>>>` sentinel; it recurred (1 of 3 workflow runs), discarding 3 real findings (2 HIGH) and paying a full cold `code-reviewer` re-review (~107k tok). The deterministic fix (a JSON output-schema) exists in the companion engine but only on its `adversarial-review` path, not the `task` CLI surface we call — an **upstream** request, not shippable here. So: when Codex finishes prose-only, the relay (still dumb — never parses prose into findings) now returns the cleaned prose tail as `codexProse`, and the **fan-in seeds the fallback `code-reviewer`** with it as UNVERIFIED leads to verify against the real diff. Cross-model finds are recovered, not discarded; engine telemetry gains `code-reviewer (codex-seeded)`. In `new-card-review.js` + `new-final-review.js` (parallel locations). Validated deterministically on real prose/sentinel/not-found fixtures.
19
+
20
+ 5. **Robustness.** `final-review.md` now tells the orchestrator to read the workflow's **structured return value** (final shape `{ codexEngine, findings, noActionFindings, gateTable, summary }` — distinct from `new-card-review`'s `perCard` shape) instead of hand-parsing the raw run-journal (the incident's stale `codexEngine: None` mis-read); `codexEngine` is telemetry and never gates F.5. A **test-filter precision** guard in `prd-card-writer.md` (authoring) + `coder.md` (runtime): a loose `npm test -- products` silently ran the wrong suite (matched `products-identifiers-search` not `product-picker-guard`) — prefer explicit spec paths + an `expect` that asserts the right suite ran; never report a validation passed on a no-match.
21
+
22
+ 6. **Per-wave doc-review moved INTO `new-card-review.js` (context/cache economy).** doc-review was the last review-cluster piece still running at orchestrator level (its analysis + Read/Edit re-entered the orchestrator context every group). It now runs as a new **`phase('Doc')`** stage inside the workflow (off-context): doc-reviewer in audit-and-apply mode over the post-code-fix wave diff, **relevance-gated** (skips a pure code-only wave), edits docs UNCOMMITTED in the worktree (the skill stages them at Phase 4 via `perCard.docFixesApplied`). The "must see FINAL code" invariant is preserved by the **batch-Final doc-reviewer** (post-E2E backstop). The inline Phase-3 path stays as the Codex/no-Workflow fallback (prose SSOT unchanged).
23
+
24
+ **MINOR** (new enforcement + a new workflow stage; no agent/skill/command removed, no install/layout change, no new `baldart.config.yml` key — schema-propagation does not apply). A separate follow-up sweeps `tools:` allowlists across the other read-only/analyst agents.
25
+
26
+ ### Changed
27
+
28
+ - **`framework/.claude/agents/codebase-architect.md`** — `tools:` allowlist (no `Task`) + Role boundary block.
29
+ - **`framework/AGENTS.md`** — new `### Worktree isolation` SSOT subsection under Git Workflow.
30
+ - **`framework/.claude/agents/doc-reviewer.md`** — worktree-rooted `docs/` qualifier.
31
+ - **`framework/.claude/skills/new/references/{implement,team-mode,review-cycle,final-review}.md`** — isolation clause at each spawn site; baseline content contract (implement/team-mode); delegation-gate update so doc-review is owned by the workflow when delegated (review-cycle); structured-return parse note (final-review).
32
+ - **`framework/.claude/workflows/new-card-review.js`** — Codex prose salvage (`codexProse` schema field + relay capture + seeded fallback); new `phase('Doc')` stage + `perCard.docFixesApplied` + `summary.docApplied`.
33
+ - **`framework/.claude/workflows/new-final-review.js`** — Codex prose salvage (parity).
34
+ - **`framework/.claude/workflows/new2.js`** — worktree-isolation line in `cardBrief`.
35
+ - **`framework/.claude/agents/{prd-card-writer,coder}.md`** — test-filter precision guard (authoring + runtime).
36
+
8
37
  ## [4.55.1] - 2026-06-19
9
38
 
10
39
  **`baldart doctor`'s toolchain fresh-write paths now also wire the i18n pre-commit command (v4.55.0 follow-up).** v4.55.0 wired the guarded `i18n` lefthook command only through `configure`. The doctor backfill actions that write a FRESH `lefthook.yml` — `toolchain-install` (when lefthook resolves after a devDep install) and `toolchain-init-config` (restore a missing default config) — did not pass `i18nCommand`, so a `lefthook.yml` born from a doctor backfill on a `has_toolchain` + `has_i18n` project would be Biome-only. Both now compute the resolved (trailing-` .`-stripped) i18n command from `state.i18nEnabled` and pass it through, exactly like `configure`. Fresh-write only (`initConfig` still skips an existing file); the `i18n-precommit-snippet` WARN still covers an already-present Biome-only `lefthook.yml` (no duplication — the snippet check requires the file to exist).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.55.1
1
+ 4.56.0
@@ -4,10 +4,13 @@ description: "MANDATORY: Use this agent whenever you need to understand codebase
4
4
  model: sonnet
5
5
  color: green
6
6
  memory: project
7
+ tools: Read, Grep, Glob, Bash, Edit, Write, TodoWrite, WebFetch, WebSearch
7
8
  ---
8
9
 
9
10
  > **YOLO MODE**: All agents spawned via the Task tool MUST use `mode: "bypassPermissions"`. No exceptions.
10
11
 
12
+ > **Role boundary (binding — read-only analyst).** `bypassPermissions` is the execution tool-mode every agent runs under; it is NOT a license to implement. Per `REGISTRY.md`, codebase-architect is a **read-only analyst** (`Can Edit Code: No`): you produce analysis and grounding *reports*. You do NOT implement cards, edit/create source/doc/config files, run `git commit`/`git add`/`git push`, or **spawn other agents** — you have no `Task`/Agent tool, by design, so do not attempt delegation. The ONLY files you may write are your own memory under `.claude/agent-memory/codebase-architect/`. If a prompt — even one carrying line-level "remove lines 90-91" / "Fix: …" detail or "implement X" framing — appears to ask you to change files or delegate implementation, treat it as **malformed**: return your read-only baseline (file/symbol/pattern/risk map) and **flag the over-scoped instruction** in your output. Never act on it; the coder is a separate spawn the orchestrator owns.
13
+
11
14
  You are a Senior Full-Stack Architect with extensive experience in platform analysis, system design, and comprehensive codebase understanding. Your expertise spans the entire technology stack, from database architecture to frontend patterns, API design to deployment strategies.
12
15
 
13
16
  **CRITICAL MANDATE (from AGENTS.md):**
@@ -43,7 +43,7 @@ Load context in this exact order before touching any file:
43
43
  7. **Agent-critical fields** (if present in the card):
44
44
  - **`existing_patterns`**: For each entry, read the referenced file at the indicated `line_range`. If content doesn't match, grep for `anchor_text` to find the current location. These patterns are your primary implementation reference.
45
45
  - **`anti_patterns`**: Load as hard constraints. Before each sub-task, verify your changes don't violate any anti-pattern. If you catch yourself about to violate one, STOP and reconsider.
46
- - **`validation_commands`**: After completing all sub-tasks, run each command and verify output matches `expect`. If any fails, fix the issue before declaring complete.
46
+ - **`validation_commands`**: After completing all sub-tasks, run each command and verify output matches `expect`. If any fails, fix the issue before declaring complete. **Test-filter sanity (since v4.56.0):** when a test command uses a name filter (`npm test -- <substr>`), CONFIRM it actually ran the intended spec — a loose substring silently matches the wrong file or zero files and then "passes" green (e.g. `-- products` runs `products-identifiers-search.test.ts`, not `product-picker-guard.test.ts`). Check the runner's "Test Files" list in the output; if the intended spec is absent or 0 files matched, re-run with the explicit spec path (`npm test -- tests/…/<file>.test.ts`) — never report a validation as passed on a no-match.
47
47
  - **`scope_boundaries`**: Treat `out_of_scope` items as DO NOT TOUCH boundaries. If your implementation touches an out-of-scope item, STOP and flag it.
48
48
  - **`input_output_examples`**: Use as test fixtures and implementation guides for API handlers and data transformations.
49
49
  - **`error_handling`**: Implement each error handler exactly as specified. Do not invent alternative error handling.
@@ -40,7 +40,7 @@ For cards exceeding 3 files or triggering invariants, use the full Required Deli
40
40
 
41
41
  When running in parallel with other agents (code-reviewer, security-reviewer):
42
42
  - NEVER edit backlog YAML files (`.yml` in `${paths.backlog_dir}/`). Report needed changes and let the orchestrator apply them.
43
- - You MAY edit files under `docs/` directly -- that is your domain.
43
+ - You MAY edit files under `docs/` directly -- that is your domain. **When spawned into a git worktree, "`docs/`" means the docs copy UNDER YOUR WORKTREE ROOT — edit that worktree-rooted path, NEVER an absolute path into the main repo (checked out live by other sessions). See `AGENTS.md` § Worktree isolation.**
44
44
  - You MAY edit `${paths.references_dir}/ssot-registry.md` -- that is your responsibility.
45
45
  - You MAY edit `${paths.design_system}/**` -- this is part of your documentation domain.
46
46
  - **ssot-registry/backlog co-staging:** a pre-commit doc-freshness hook may require that any commit touching `${paths.backlog_dir}/*.yml` ALSO stages `ssot-registry.md`. You write the `ssot-registry.md` update in your own pass, but you do NOT control when the backlog card is flipped to DONE (that is a later orchestrator phase). If your `ssot-registry.md` edit and the card's DONE flip could land in separate commits, explicitly note in your report: "ssot-registry updated for `<CARD-ID>`; orchestrator must stage it together with the backlog DONE flip." Do not stage or commit anything yourself.
@@ -520,6 +520,7 @@ Every card MUST include ALL fields from the template:
520
520
  - For each testable AC, derive a grep/count command (e.g., `grep -c 'tableId' src/file.tsx` with `expect: ">= 4"`)
521
521
  - `expect` values: `"exit 0"`, `">= N"`, `"contains X"`, `"== N"`
522
522
  - Only generate read-only or build commands — no destructive or production-targeting commands
523
+ - **Test-filter precision (since v4.56.0):** a test command's filter MUST resolve to the intended spec file(s). A loose substring silently runs the WRONG suite — e.g. `npm test -- products` matches `products-identifiers-search.test.ts` but NOT `product-picker-guard.test.ts` (it has no plural "products"). Prefer the explicit spec **path** (`npm test -- tests/integration/product-picker-guard.test.ts`) or an anchored unique stem; never a short shared prefix. Pair the command with an `expect` that asserts the right suite ran (e.g. `contains "product-picker-guard"` or a `>= N tests` count), so a zero/wrong match FAILS loudly instead of passing green.
523
524
  - Omit only for documentation-only cards
524
525
 
525
526
  - `input_output_examples` — concrete API/data examples:
@@ -91,30 +91,39 @@ that is a **gate violation**: log it as
91
91
  hasApiDataFiles, // true unless NO scope file falls under paths.api_* / data-model
92
92
  config, // the parsed baldart.config.yml
93
93
  singleCard: cardPaths.length === 1, // N=1 batch — enables the F-041 per-finder slim
94
- slimDoc: cardPaths.length === 1 && <Phase-3 doc-review RAN for this card, NOT deferred>,
94
+ slimDoc: cardPaths.length === 1 && <doc-review ran INLINE at Phase 3 (post-E2E), NOT in the workflow's Doc stage, NOT deferred>,
95
95
  slimApi: false, // api-perf NEVER runs per-card in classic /new → final is its ONLY run; never slim it
96
96
  codexScriptPath // resolved ONCE per run (glob in team-mode.md § Step D); omit if empty → workflow self-resolves
97
97
  }})
98
98
  ```
99
99
 
100
100
  **`slimDoc` coverage gate (per-finder — do NOT collapse to a bare `singleCard`).** The
101
- final review may drop the doc-reviewer for an N=1 batch ONLY because Phase 3 already ran
102
- it on the same (single) card so it is gated on Phase 3 having ACTUALLY run, not deferred.
103
- You know this from your own Phase 3 log: if you logged `doc-review: DEFERRED to Final FULL
104
- gate (light, no doc files in diff)` (review-cycle.md step ~235), Phase 3 did NOT run →
105
- `slimDoc: false` (the final MUST run doc-reviewer, else doc review is skipped end-to-end).
106
- Otherwise Phase 3 ran → `slimDoc: true`. `slimApi` is ALWAYS `false` here: api-perf-cost-auditor
101
+ final review may drop the doc-reviewer for an N=1 batch ONLY when doc-review already ran on
102
+ the same (single) card **AND saw the post-E2E final code** i.e. it ran INLINE at Phase 3
103
+ (which sits AFTER Phase 2.6 E2E). You know this from your own Phase 3 log:
104
+ - logged `doc-review: DEFERRED to Final FULL gate (light, no doc files in diff)` did NOT run → `slimDoc: false`.
105
+ - **doc-review was done by the workflow's Doc stage (since v4.56.0 — the delegated path)** → it ran PRE-E2E inside `new-card-review`, so it did NOT see any code E2E later changed → `slimDoc: false` (the final doc-reviewer is the ONLY post-E2E backstop — it MUST run). This is the common case whenever the cluster was delegated.
106
+ - doc-review ran INLINE at Phase 3 (the Codex/no-Workflow fallback path, post-E2E) → `slimDoc: true` is safe (it already saw final code).
107
+ `slimApi` is ALWAYS `false` here: api-perf-cost-auditor
107
108
  is deferred-to-final by design in classic `/new` (never part of the per-card cluster), so
108
109
  the final pass is its only run — slimming it would leave the api/perf domain unreviewed.
109
110
 
110
- The workflow returns `{ codexEngine, findings, gateTable, summary }` where
111
+ The workflow returns `{ codexEngine, findings, noActionFindings, gateTable, summary }` where
111
112
  `findings` are already consolidated and classified (`VERIFIED` /
112
113
  `NEEDS_MANUAL_CONFIRMATION`; `FALSE_POSITIVE` already dropped) and `gateTable`
113
- is the qa-sentinel PASS/FAIL/SKIP table. **Skip the inline F.2/F.3/F.4 below**
114
- and carry those results straight into **Step F.5** (fix application + final
115
- build + the `AskUserQuestion` gates which stay HERE, in the skill, because a
116
- workflow cannot prompt the user). Log `f.final: delegated to new-final-review
117
- workflow (engine: <codexEngine>)` in the tracker.
114
+ is the qa-sentinel PASS/FAIL/SKIP table. **Read the workflow's STRUCTURED RETURN
115
+ VALUE directly** (the `Workflow` tool hands you the parsed object) do NOT
116
+ `grep`/`tail`/hand-parse the raw run-journal file: this is the **final-review**
117
+ shape (`findings` + `noActionFindings` at top level), which is DIFFERENT from
118
+ `new-card-review`'s `perCard` shape — improvising a bash parse on the wrong
119
+ shape mis-reads it (e.g. a stale `codexEngine: None`). `codexEngine` is
120
+ informational telemetry (`codex` / `code-reviewer (fallback)` / `code-reviewer
121
+ (codex-seeded)`) — it NEVER gates F.5: always carry `findings` + `gateTable`
122
+ forward regardless of which engine produced them. **Skip the inline F.2/F.3/F.4
123
+ below** and carry those results straight into **Step F.5** (fix application +
124
+ final build + the `AskUserQuestion` gates — which stay HERE, in the skill,
125
+ because a workflow cannot prompt the user). Log `f.final: delegated to
126
+ new-final-review workflow (engine: <codexEngine>)` in the tracker.
118
127
 
119
128
  - **ELSE** (no `Workflow` tool — older Claude Code, `disableWorkflows`, or a
120
129
  Codex install where workflows have no equivalent) → run the **inline F.2–F.4
@@ -267,7 +276,7 @@ that is a **gate violation**: log it as
267
276
  - **`doc`-domain findings** (file path matching the `doc` match rule — `*.md` under `${paths.references_dir}`/`${paths.prd_dir}`, `CHANGELOG.md`, `ssot-registry.md`) → invoke the **doc-reviewer** agent once in write mode to apply them. NEVER route doc fixes to coder.
268
277
  - **`security`-domain findings** (path in `paths.high_risk_modules`, or RLS-policy SQL) → route to **security-reviewer** in write mode (canonical writer map v4.26.1 — it owns the security-invariant contract a coder lacks; NEVER route security fixes to coder). **`migration`-domain findings** (SQL under the migrations dir) → route to **coder**. For both, apply the Sub-agent failure protocol's STOP-on-crash rule (never inline-fallback on a security/migration fix). These are NOT collapsed into a generic "everything else" bucket.
269
278
  - **All remaining findings** (other code, perf, test) → invoke the **coder** agent once to apply them in a single pass.
270
- Run in the order doc-reviewer → security-reviewer → coder (skip any whose partition is empty). Pass only the verified findings, not false positives.
279
+ Run in the order doc-reviewer → security-reviewer → coder (skip any whose partition is empty). Pass only the verified findings, not false positives. **WORKTREE ISOLATION (per `AGENTS.md` § Worktree isolation):** every writer's spawn prompt MUST state that all Edit/Write paths resolve UNDER the batch worktree root `<ABSOLUTE_WORKTREE_PATH>` — NEVER a path under the main repo `<ABSOLUTE_MAIN_REPO>` (checked out live by other sessions); `cd` alone does not stop an absolute main-repo path.
271
280
  12. Run final build: `npm run lint && npx tsc --noEmit && npm run build` (when `has_toolchain`, the configured `toolchain.commands.{lint,typecheck,build}` verbatim — § "Toolchain gates"; redirect each to `/tmp/final-<gate>.txt` per § "Context economy"; surface only exit code + bounded extract on failure).
272
281
  If any check fails, apply self-healing retry loop (up to 3 times).
273
282
  13. **Update tracker** with final review results:
@@ -10,7 +10,7 @@
10
10
  - If `DONE` (or the user chose "proceed anyway") → continue.
11
11
  2b. **Claim** — only now set the card status to `IN_PROGRESS` (in the backlog YAML / tracker) and assign yourself. This is a state write, not a visibility emission — do not also spend a TaskUpdate / Progress-Bar turn (§ "State surface — the tracker only").
12
12
  2c. **Trivial-card classification (BLOCKING gate for steps 3–4)** — evaluate `IS_TRIVIAL(card)` per § "Trivial-card fast-lane". Note: condition 3 (non-source diff) cannot be fully evaluated until the coder has produced the diff, so at this point compute the **provisional** trivial flag from conditions 1+2 only (`review_profile == skip` AND no Step-A trigger sourced from the card YAML text + `files_likely_touched` extensions — if EVERY path in `files_likely_touched` is non-source, condition 3 is provisionally satisfied). If provisionally trivial → **SKIP steps 3, 3a, and 4** (architecture grounding); log `trivial: architecture grounding skipped (review_profile=skip + non-source files_likely_touched + 0 triggers)` and jump to Phase 2. Re-confirm `IS_TRIVIAL` on the ACTUAL committed diff at the review gates (Phase 2.55/3.5/3.7); if the coder unexpectedly touched a source file, the guard flips the card back onto the normal review path there. If NOT provisionally trivial → run steps 3, 3a, 4 as normal.
13
- 3. **(skip when provisionally trivial — see 2c)** Invoke the **codebase-architect** agent (MUST per AGENTS.md) to understand the relevant codebase area, existing patterns, and architecture before any implementation. When `features.has_lsp_layer: true`, the architect uses LSP find-references for identifier-shaped lookups — this needs NO handoff from the orchestrator: the architect reads `features.has_lsp_layer` from `baldart.config.yml` directly (the flag is ambient) per `agents/code-search-protocol.md`. Likewise, when `features.has_code_graph: true`, the architect uses the Graphify code graph for structural/relational lookups (ambient flag) per `agents/code-graph-protocol.md`. The orchestrator does NOT propagate either flag. (Earlier doc versions numbered this step 4; the step that read project-status BEFORE the architect was removed because it persisted pre-analysis context — see step 3a.) **Pass `OUTPUT=terse` in the architect prompt (since v4.49.0)** — this is grounding for the coder/reviewer, not an explanation, so the architect returns its structured contract (reuse table + canonical evidence + `path:line — symbol` rows + `totals:`) without narrative prose. The verbatim baseline persisted at step 5b keeps all the substance a lean `/codexreview` needs (paths, signatures, patterns, high-risk paths) while staying small in the orchestrator's context and on disk.
13
+ 3. **(skip when provisionally trivial — see 2c)** Invoke the **codebase-architect** agent (MUST per AGENTS.md) to understand the relevant codebase area, existing patterns, and architecture before any implementation. When `features.has_lsp_layer: true`, the architect uses LSP find-references for identifier-shaped lookups — this needs NO handoff from the orchestrator: the architect reads `features.has_lsp_layer` from `baldart.config.yml` directly (the flag is ambient) per `agents/code-search-protocol.md`. Likewise, when `features.has_code_graph: true`, the architect uses the Graphify code graph for structural/relational lookups (ambient flag) per `agents/code-graph-protocol.md`. The orchestrator does NOT propagate either flag. (Earlier doc versions numbered this step 4; the step that read project-status BEFORE the architect was removed because it persisted pre-analysis context — see step 3a.) **Pass `OUTPUT=terse` in the architect prompt (since v4.49.0)** — this is grounding for the coder/reviewer, not an explanation, so the architect returns its structured contract (reuse table + canonical evidence + `path:line — symbol` rows + `totals:`) without narrative prose. The verbatim baseline persisted at step 5b keeps all the substance a lean `/codexreview` needs (paths, signatures, patterns, high-risk paths) while staying small in the orchestrator's context and on disk. **Baseline content contract (MANDATORY — since v4.56.0):** the architect prompt is grounding INPUT only — pass the card scope, `files_likely_touched`, and the question to map. NEVER put line-level fix directives ("remove lines 90-91", "Fix: use X"), the words "implement/fix/apply", or acceptance-criteria framed as a task into the architect prompt. The architect produces a read-only MAP (file/symbol locations, patterns, type signatures, integration points, high-risk paths); it does NOT implement and has no `Task`/Agent tool to delegate (see `codebase-architect.md` § Role boundary). The coder is a separate spawn the orchestrator owns.
14
14
  3a. Update `${paths.references_dir}/project-status.md` Active Code Context (skip when the file does not exist in the project) — do this AFTER the codebase-architect run (step 3) so the "Active Code Context" reflects the architect's findings (which files are actually in scope), not just the card YAML's `files_likely_touched`. Writing it before the architect run would persist pre-analysis claims that downstream agents (e.g. a parallel card) would then read as truth.
15
15
  4. **Plan-auditor grounding check** — but first, a **provenance skip (since v4.7.0 — mirror of Step 3d)**: the `/prd` Step 6.6 holistic audit already ran a per-card grounding pass when the card was authored. Re-grounding here is only worth a plan-auditor spawn if something changed since. Skip the plan-auditor when **BOTH** hold:
16
16
  - **P1 — provenance present**: the card has `metadata.holistic_audit` with non-empty `audited_at`, `audited_commit`, `audited_set`.
@@ -195,6 +195,7 @@
195
195
  [list from ownership map: files owned by other cards that this card reads]
196
196
 
197
197
  FORBIDDEN:
198
+ - **WORKTREE ISOLATION (per `AGENTS.md` § Worktree isolation):** every path you Edit/Write/create MUST be UNDER the worktree root `<ABSOLUTE_WORKTREE_PATH>`. NEVER write a path under the main repo `<ABSOLUTE_MAIN_REPO>` — it is checked out live by other sessions. `cd` is not enough; an absolute main-repo path bypasses it. Edit the worktree-rooted copy.
198
199
  - Do NOT edit any file outside the MAY EDIT list above
199
200
  - Do NOT refactor unrelated code
200
201
  - Do NOT add unrequested features or extra error handling beyond what's specified
@@ -63,7 +63,7 @@ so it surfaces in telemetry.
63
63
  automated doc re-review):
64
64
  - `classification == NEEDS_MANUAL_CONFIRMATION` (any domain) → `AskUserQuestion` — the human gate the
65
65
  workflow cannot run. (`summary.needsManual` counts these, doc included.)
66
- - else `domain == doc` residual → carry into **Phase 3** (the doc-reviewer runs there, post-E2E, on final code).
66
+ - else `domain == doc` residual → **the workflow's Doc stage (since v4.56.0) already ran the doc-reviewer in audit-and-apply mode** over the wave diff and applied the doc fixes in the worktree (uncommitted — you stage them at Phase 4 via `perCard.docFixesApplied`). A doc `residual` here means the workflow's doc-reviewer flagged a gap it could **not** resolve — carry ONLY that into a targeted Phase 3 doc pass; do NOT re-run a full per-wave doc review (it's done).
67
67
  - else `security` residual (a fix not converged in 2 retries) → spawn a targeted `security-reviewer`
68
68
  now over this card's `editableFiles` (it owns the security-invariant contract — never a coder).
69
69
  - else `code`/`perf`/`migration` residual (a fix the coder could not converge in its 2 retries)
@@ -85,10 +85,10 @@ so it surfaces in telemetry.
85
85
  - **Telemetry**: append the workflow's `fixesApplied` + `residual` to `## Fix Application Log`
86
86
  (`decision=workflow`). Derive the Phase-8 producers from the return: `qa_first_attempt` ←
87
87
  `summary.qaRan ? (summary.failingGates.length === 0 && !summary.checksFailed ? "pass" : "fail") : "n/a"`
88
- (n/a when qa was deferred to the Final); `doc_gaps` is produced by **Phase 3** (which still runs below).
88
+ (n/a when qa was deferred to the Final); `doc_gaps` is produced by the **workflow's Doc stage** (`perCard.docFixesApplied`), not Phase 3.
89
89
 
90
- Then proceed to **Phase 2.6 (E2E)** → **Phase 3 (doc review, relevance-gated)** **Phase 4 (commit)**.
91
- Log `review-cluster: delegated to new-card-review (engine: <codexEngine>, fixes: <n>, residual: <n>, qa: <pass|fail|deferred>)`.
90
+ Then proceed to **Phase 2.6 (E2E)** → **Phase 4 (commit)**. **The per-wave doc review was done by the workflow's Doc stage (since v4.56.0)** — do NOT re-run Phase 3 here on the delegated path (run it ONLY for an unresolved doc `residual` per the bullet above). The post-E2E doc backstop is the **batch-Final doc-reviewer** (`final-review.md` F.3), which sees the truly-final code after every card's E2E — so an E2E-induced doc drift is caught there, not per-wave. At Phase 4, stage the worktree's `docFixesApplied` edits alongside the code.
91
+ Log `review-cluster: delegated to new-card-review (engine: <codexEngine>, fixes: <n>, residual: <n>, doc: <applied n|skipped>, qa: <pass|fail|deferred>)`.
92
92
 
93
93
  - **ELSE** (no `Workflow` tool — older Claude Code, `disableWorkflows`, or a Codex install where
94
94
  workflows have no equivalent) → run the **inline Phase 2.55 → 2.6 → 3 → 3.5 → 3.7 exactly as written
@@ -283,6 +283,7 @@ skill's Phase 1 falls back to deriving Gherkin scenarios from
283
283
  [paste the Doc Sync Context block built above]
284
284
 
285
285
  Instructions for doc-reviewer:
286
+ - **WORKTREE ISOLATION (per AGENTS.md § Worktree isolation):** every doc you Edit/Write MUST be UNDER the worktree root <ABSOLUTE_WORKTREE_PATH>. NEVER write a path under the main repo <ABSOLUTE_MAIN_REPO> — it is checked out live by other sessions; `cd` alone does not stop an absolute main-repo path. Edit the worktree-rooted copy of every doc.
286
287
  - You OWN the `doc` domain end-to-end: audit AND apply the fixes in THIS invocation.
287
288
  Do NOT defer doc writes to another agent — you are the agent with the doc-invariant
288
289
  contract and the full context. Write the missing/stale docs directly.
@@ -48,6 +48,8 @@ Focus on: [combined files_likely_touched from all cards in group]
48
48
  Return: file paths, type signatures, existing patterns. Max 30 lines.
49
49
  ```
50
50
 
51
+ > **Baseline content contract (MANDATORY — since v4.56.0).** This is a READ-ONLY architecture MAP request, not a work order. The codebase-architect prompt MUST contain ONLY: file/symbol locations, existing patterns, type signatures, integration points, high-risk paths. It MUST NOT contain line-level fix instructions ("remove lines 90-91"), the words "implement"/"fix"/"apply", or card acceptance-criteria text framed as a task. The architect maps; the coder (a separate Step-B spawn) does the work. (The architect has no `Task`/Agent tool and a read-only role boundary — see `codebase-architect.md` — so a contaminated prompt is at best wasted and at worst a protocol violation if it tries to act.)
52
+
51
53
  This is the ONLY context the orchestrator accumulates per group. After passing it to the coders, it can be purged.
52
54
 
53
55
  **Persist for review reuse (since v3.35.0)** — also write these findings to
@@ -96,6 +98,7 @@ Agent tool call:
96
98
  [files from ownership map for THIS card only]
97
99
 
98
100
  FORBIDDEN:
101
+ - **WORKTREE ISOLATION (per AGENTS.md § Worktree isolation):** every path you Edit/Write/create MUST be UNDER the worktree root <worktree-path>. NEVER write a path under the main repo <ABSOLUTE_MAIN_REPO> — it is checked out live by other sessions; `cd` alone does not prevent an absolute main-repo path. Edit the worktree-rooted copy.
99
102
  - ALL files not in the MAY EDIT list
100
103
  - Do NOT create files outside the designated paths
101
104
 
@@ -25,8 +25,10 @@ export const meta = {
25
25
  // config object resolved baldart.config.yml (paths.* … )
26
26
  //
27
27
  // Return value (consumed by the skill):
28
- // { codexEngine, perCard: { <cardId>: { fixesApplied:[…1-line], residual:[…finding] } },
28
+ // { codexEngine, perCard: { <cardId>: { fixesApplied:[…1-line], docFixesApplied:[…1-line], residual:[…finding] } },
29
29
  // gateTable, summary }
30
+ // docFixesApplied (since v4.56.0): doc-reviewer ran IN the workflow (Phase Doc, off-orchestrator-context)
31
+ // and applied doc fixes in the worktree UNCOMMITTED — the skill stages them at its Phase 4 commit.
30
32
  // ───────────────────────────────────────────────────────────────────────────
31
33
 
32
34
  // Tolerate args delivered as a JSON string (parse-or-default) — the Workflow tool
@@ -70,7 +72,7 @@ const tc = (key, fallback) => (tcCmds[key] && String(tcCmds[key]).trim()) || fal
70
72
  // Per-card result accumulator — built up-front (so the early-return guards can return it) and
71
73
  // populated with fixesApplied/residual in the Fix phase.
72
74
  const perCard = {}
73
- for (const c of cards) perCard[c.cardId] = { fixesApplied: [], residual: [] }
75
+ for (const c of cards) perCard[c.cardId] = { fixesApplied: [], docFixesApplied: [], residual: [] }
74
76
 
75
77
  if (!cards.length) {
76
78
  log('new-card-review: no cards supplied — nothing to review.')
@@ -112,6 +114,7 @@ const CODEX_SCHEMA = {
112
114
  properties: {
113
115
  codexAvailable: { type: 'boolean', description: 'false if CODEX_NOT_FOUND / TIMED_OUT' },
114
116
  findings: { type: 'array', items: FINDING },
117
+ codexProse: { type: 'string', description: 'PROSE-ONLY salvage (v4.56.0): when Codex FINISHED but emitted no sentinel JSON block, the cleaned tail of its prose review — handed to the fallback code-reviewer as UNVERIFIED leads. Empty/omitted otherwise.' },
115
118
  note: { type: 'string' },
116
119
  },
117
120
  }
@@ -256,8 +259,9 @@ const codexPrompt =
256
259
  ` awk '/<<<FINDINGS_JSON>>>/{c=1;b="";next}/<<<END_FINDINGS_JSON>>>/{c=0;last=b;next}c{b=b $0 ORS}END{printf "%s",last}' ${codexReviewFile}\n` +
257
260
  ` 5. Decide the return — do NOT re-verify, re-grep, or Read any source file:\n` +
258
261
  ` • awk output is a NON-EMPTY JSON array → parse it and return it verbatim as \`findings\`, codexAvailable:true (its keys already match the schema).\n` +
259
- ` • awk output is EMPTY but the file has "Turn completed." (or "CODEX_NOT_FOUND") → Codex finished WITHOUT the sentinel block (e.g. it wrote a prose report). Return codexAvailable:false, findings:[] and EXIT NOW do NOT wait the rest of the window, and do NOT parse findings out of the prose (a prose-only completion routes to the code-reviewer fallback; never risk dropping a real finding by guessing).\n` +
260
- ` • none of the markers ever appeared after the FULL 10-minute windowcodexAvailable:false. NEVER set false because a single poll returned slowly.\n\n` +
262
+ ` • awk output is EMPTY but the file has "Turn completed." → Codex FINISHED but wrote a prose report instead of the sentinel block. Do NOT parse findings out of the prose yourself (never guess a schema). Instead, capture the prose for the fallback to verify: run \`grep -v '^\\[codex\\]' ${codexReviewFile} | tail -c 12000\` and return codexAvailable:false, findings:[], codexProse:<that output>, then EXIT NOW (do NOT wait the rest of the window). The fallback code-reviewer re-verifies the prose against the real diff so a real finding is recovered, not dropped.\n` +
263
+ ` • awk output is EMPTY and the file has "CODEX_NOT_FOUND"companion missing. Return codexAvailable:false, findings:[] (no codexProse).\n` +
264
+ ` • none of the markers ever appeared after the FULL 10-minute window → codexAvailable:false, findings:[] (no codexProse). NEVER set false because a single poll returned slowly.\n\n` +
261
265
  `TASK PROMPT:\n${codexReviewTask}`
262
266
 
263
267
  const tcGateLines = [
@@ -315,13 +319,15 @@ let raw = []
315
319
  let gateTable = []
316
320
  let codexEngine = codexResolved ? 'codex' : 'code-reviewer (fallback)'
317
321
  let codexRan = false
322
+ let codexProse = '' // PROSE-ONLY salvage (v4.56.0): seeds the fallback so Codex's real finds aren't lost.
318
323
  for (const item of findResults) {
319
324
  if (item.kind === 'codex') {
320
325
  if (item.r && item.r.codexAvailable && Array.isArray(item.r.findings)) {
321
326
  codexRan = true
322
327
  raw.push(...item.r.findings.map((f) => ({ ...f, source: 'codex', preValidated: true })))
323
328
  } else {
324
- log('Discovery: Codex companion resolved but its review did not complete — falling back to code-reviewer.')
329
+ if (item.r && typeof item.r.codexProse === 'string' && item.r.codexProse.trim()) codexProse = item.r.codexProse.trim()
330
+ log(`Discovery: Codex companion resolved but its review did not complete${codexProse ? ' (prose-only — salvaging its findings as fallback leads)' : ''} — falling back to code-reviewer.`)
325
331
  }
326
332
  } else if (item.kind === 'qa') {
327
333
  gateTable = (item.r && item.r.gates) || []
@@ -332,11 +338,20 @@ for (const item of findResults) {
332
338
  }
333
339
 
334
340
  // ONE deterministic fallback: pre-flight found no companion, or it ran but did not complete.
341
+ // PROSE-ONLY salvage (v4.56.0): when Codex FINISHED but emitted prose (no sentinel JSON), seed the
342
+ // fallback with that prose as UNVERIFIED leads. The relay never parses prose into findings; the
343
+ // fallback code-reviewer (a real reviewer WITH code access) re-verifies each lead against the actual
344
+ // diff and emits the schema itself — so Codex's cross-model finds are recovered, not discarded, and
345
+ // the trust model is unchanged (output is validated by a reviewer over real code). Net vs the old
346
+ // cold fallback: same full pass, but seeded → recovers the finds, typically cheaper (confirm vs hunt).
335
347
  if (!codexRan) {
336
- codexEngine = 'code-reviewer (fallback)'
348
+ codexEngine = codexProse ? 'code-reviewer (codex-seeded)' : 'code-reviewer (fallback)'
349
+ const seedBlock = codexProse
350
+ ? `\n\nCodex (a DIFFERENT model) reviewed this exact diff and reported the following findings in PROSE — it failed to emit machine-readable JSON, so these are UNVERIFIED leads, NOT validated findings. VERIFY each against the actual diff at its cited file:line, DROP any that don't hold, then run your own full independent review for anything Codex missed. Treat the prose as hints to check, never as ground truth:\n<codex-prose>\n${codexProse}\n</codex-prose>`
351
+ : ''
337
352
  const fb = await agent(
338
- `Codex was unavailable for this wave's code review. Run the FULL code review yourself over the wave diff, per ${protocolRef} (Phase 3.7).\n\n${waveBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied (your findings are treated as validated). An observation you VERIFIED needs NO change (fix direction "no fix required" / "acceptable as-is") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it.`,
339
- { label: 'code-reviewer (fallback)', phase: 'Discovery', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
353
+ `Codex was unavailable for this wave's code review. Run the FULL code review yourself over the wave diff, per ${protocolRef} (Phase 3.7).\n\n${waveBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied (your findings are treated as validated). An observation you VERIFIED needs NO change (fix direction "no fix required" / "acceptable as-is") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it.${seedBlock}`,
354
+ { label: codexProse ? 'code-reviewer (codex-seeded)' : 'code-reviewer (fallback)', phase: 'Discovery', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
340
355
  )
341
356
  if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: true })))
342
357
  }
@@ -411,7 +426,8 @@ const isNoAction = (f) => (f.severity !== 'BLOCKER' && f.severity !== 'HIGH') &&
411
426
  // Partition `surviving` (= VERIFIED + NEEDS_MANUAL; FALSE_POSITIVE already dropped) with NO overlap:
412
427
  // securityFix = VERIFIED security → security-reviewer applies (it owns the security invariants).
413
428
  // actionable = VERIFIED non-doc non-security → the coder fixes these.
414
- // docResidual = VERIFIED doc → the skill runs doc-reviewer post-E2E on final code.
429
+ // VERIFIED doc → the workflow's Phase Doc applies them (since v4.56.0 — `docFindings` there;
430
+ // was previously returned as residual for the skill's Phase 3).
415
431
  // manualResidual= NEEDS_MANUAL any → human gate, owned by the skill (a doc-manual must NOT be
416
432
  // silently auto-re-reviewed: it carries its needs-manual classification out).
417
433
  // No-action VERIFIED findings: recorded + counted, excluded from every writer partition.
@@ -419,7 +435,6 @@ const noAction = surviving.filter((f) => f.classification === 'VERIFIED' && isNo
419
435
  const noActionSet = new Set(noAction)
420
436
  const securityFix = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && isSecurity(f) && !noActionSet.has(f))
421
437
  const actionable = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && !isSecurity(f) && !noActionSet.has(f))
422
- const docResidual = surviving.filter((f) => f.classification === 'VERIFIED' && isDoc(f) && !noActionSet.has(f))
423
438
  const manualResidual = surviving.filter(isManual)
424
439
  for (const f of noAction) log(`Review: no-action finding recorded (not routed to a writer): [${f.finding_id}] ${f.severity} ${f.domain} — ${f.title} (${f.requires_action === false ? 'requires_action:false' : 'no-action-inferred'})`)
425
440
 
@@ -475,14 +490,74 @@ const mergedChecks = ['lint', 'tsc', 'build'].reduce((acc, k) => {
475
490
  const codeResidual = allActionable.filter((f) => !appliedIds.has(f.finding_id) || unresolvedIds.has(f.finding_id))
476
491
  const checksFailed = ['lint', 'tsc', 'build'].some((k) => mergedChecks[k] === 'FAIL')
477
492
 
493
+ // ───────────────────────────────────────────────────────────────────────────
494
+ // Phase Doc — doc-reviewer audit-and-apply on the wave diff (since v4.56.0).
495
+ // MOVED OFF the orchestrator (was /new Phase 3) so doc-reviewer's analysis +
496
+ // Read/Edit never enter the orchestrator context (context + cache economy).
497
+ // It OWNS the doc domain: audits the post-code-fix worktree AND applies the
498
+ // doc fixes UNCOMMITTED (the skill stages them at Phase 4 via docFixesApplied).
499
+ // Runs AFTER the code Fix phase so it documents the FINAL post-fix code.
500
+ // Backstop for the truly-final (post-E2E) code is the batch-Final doc-reviewer
501
+ // (final-review.md F.3). Relevance-gated: skip a pure code-only wave.
502
+ // ───────────────────────────────────────────────────────────────────────────
503
+ phase('Doc')
504
+ const docDir = (cfg.paths && (cfg.paths.docs_dir || cfg.paths.references_dir)) || 'docs'
505
+ // VERIFIED doc-domain findings the finders raised (these used to be returned as residual for the
506
+ // skill's Phase 3; now the workflow's doc-reviewer applies them).
507
+ const docFindings = surviving.filter((f) => f.classification === 'VERIFIED' && isDoc(f) && !noActionSet.has(f))
508
+ // Relevance gate (parity with the inline Phase 3 gating): run only when the wave diff plausibly
509
+ // affects docs (data/query/API/schema/markdown surfaces) OR a verified doc finding exists.
510
+ const DOC_SURFACE_RE = /(^|\/)(docs?|api|server\/data|lib\/server|schema|migrations?)(\/|\.|$)|\.(md|sql)$/i
511
+ const docSurfaceTouched = unionScope.some((f) => DOC_SURFACE_RE.test(String(f)))
512
+ let docApplied = [] // [{ finding_id?, note }]
513
+ let docResidualOut = [] // verified doc findings the doc-reviewer could NOT resolve → skill residual
514
+ if ((docSurfaceTouched || docFindings.length)) {
515
+ const docBrief =
516
+ `You OWN the \`doc\` domain end-to-end for this wave: AUDIT the committed diff for doc drift AND APPLY the fixes in THIS invocation (audit-and-apply mode, per doc-reviewer.md § Constraints). Do NOT defer doc writes to another agent.\n\n` +
517
+ `**WORKTREE ISOLATION (per AGENTS.md § Worktree isolation):** every doc you Edit/Write MUST be UNDER the worktree root ${a.worktreePath || '(cwd)'}. NEVER write a path under the main repo — it is checked out live by other sessions; \`cd\` alone does not stop an absolute main-repo path. cd into the worktree first and edit the worktree-rooted copy of each doc (e.g. ${docDir}/… UNDER the worktree).\n\n` +
518
+ `${waveBrief}\n\n${baselineBrief}\n\n` +
519
+ (docFindings.length
520
+ ? `Verified doc-domain findings from this wave's review (apply each):\n${docFindings.map((f) => `- [${f.finding_id}] ${f.title}\n evidence: ${f.evidence}\n direction: ${f.minimal_fix_direction}`).join('\n')}\n\n`
521
+ : '') +
522
+ `Then run your standard doc audit on the changed files: sync canonical reference docs (api/, data-model, ssot-registry as applicable), set freshness_status:fresh + last_verified_from_code:<today> on docs you touch. ` +
523
+ `Do NOT commit (the orchestrator commits at Phase 4). Do NOT git stash (refs/stash is shared across worktrees). ` +
524
+ `A doc-drift→bug finding rooted in CODE (the impl contradicts a documented contract) is the ONE thing you do NOT fix yourself — return it in \`unresolved\` with the conflicting code file:line + the doc it violates.\n\n` +
525
+ `Return: applied (one entry per doc fix you MADE — finding_id REQUIRED: use the listed finding's id when it maps to one, else a short synthetic id like "doc-audit-1"; put a 1-line summary in note), unresolved (finding_ids / descriptions you could NOT resolve, incl. code-rooted drift), checks (lint/tsc/build PASS/SKIP — SKIP unless you touched a source-adjacent file).`
526
+ const dr = await agent(docBrief, { label: 'fix-doc', phase: 'Doc', agentType: 'doc-reviewer', schema: FIX_SCHEMA })
527
+ const dres = (dr && typeof dr === 'object') ? dr : {}
528
+ docApplied = Array.isArray(dres.applied) ? dres.applied : []
529
+ const docUnresolved = Array.isArray(dres.unresolved) ? dres.unresolved : []
530
+ const docAppliedIds = new Set(docApplied.map((x) => x && x.finding_id).filter(Boolean))
531
+ // Verified doc findings NOT applied → residual (skill runs a targeted Phase 3 doc pass / routes code-rooted drift).
532
+ docResidualOut = docFindings.filter((f) => !docAppliedIds.has(f.finding_id))
533
+ // doc-reviewer's own unresolved audit items (no matching finding) → slim residual notes.
534
+ const extraDocResidual = docUnresolved
535
+ .filter((u) => typeof u === 'string' && u.trim() && !docFindings.some((f) => f.finding_id === u))
536
+ .map((u, i) => ({ finding_id: `doc-audit#${i}`, title: String(u).slice(0, 160), severity: 'MEDIUM', domain: 'doc', evidence: '(doc-reviewer audit)', minimal_fix_direction: String(u), classification: 'VERIFIED', card: cards[0].cardId }))
537
+ docResidualOut = [...docResidualOut, ...extraDocResidual]
538
+ log(`Doc: doc-reviewer applied ${docApplied.length} doc fix(es)${docFindings.length ? ` (incl. ${docFindings.length} routed finding(s))` : ''}; ${docResidualOut.length} unresolved → residual.`)
539
+ } else {
540
+ // No doc surface and no doc finding: nothing to review (parity with code-only Phase 3 skip).
541
+ docResidualOut = docFindings // empty in this branch, but keep the contract explicit
542
+ log('Doc: SKIPPED (no doc-relevant surface in the wave diff and no verified doc finding).')
543
+ }
544
+
478
545
  // ---- Assemble per-card result ----------------------------------------------
479
- function bucket(cardId) { return perCard[cardId] || (perCard[cardId] = { fixesApplied: [], residual: [] }) }
546
+ function bucket(cardId) { return perCard[cardId] || (perCard[cardId] = { fixesApplied: [], docFixesApplied: [], residual: [] }) }
480
547
  for (const f of allActionable) {
481
548
  if (appliedIds.has(f.finding_id) && !unresolvedIds.has(f.finding_id)) {
482
549
  bucket(f.card || cards[0].cardId).fixesApplied.push(`[${f.finding_id}] ${f.title}`)
483
550
  }
484
551
  }
485
- for (const f of [...codeResidual, ...docResidual, ...manualResidual]) {
552
+ // Doc fixes applied by the workflow's Doc stage (uncommitted in the worktree skill stages at Phase 4).
553
+ // Attribute by matching finding → card; wave-level (first card) for the doc-reviewer's own audit edits.
554
+ const docFindingCard = new Map(docFindings.map((f) => [f.finding_id, f.card || cards[0].cardId]))
555
+ for (const x of docApplied) {
556
+ const note = (x && (x.note || x.finding_id)) ? `${x.finding_id ? `[${x.finding_id}] ` : ''}${x.note || ''}`.trim() : '(doc fix)'
557
+ const cardId = (x && x.finding_id && docFindingCard.get(x.finding_id)) || cards[0].cardId
558
+ bucket(cardId).docFixesApplied.push(note)
559
+ }
560
+ for (const f of [...codeResidual, ...docResidualOut, ...manualResidual]) {
486
561
  bucket(f.card || cards[0].cardId).residual.push(slimFinding(f))
487
562
  }
488
563
 
@@ -494,7 +569,8 @@ const summary = makeSummary({
494
569
  falsePositive: classified.filter((f) => f.classification === 'FALSE_POSITIVE').length,
495
570
  needsManual: manualResidual.length,
496
571
  fixesApplied: appliedIds.size,
497
- docResidual: docResidual.length,
572
+ docApplied: docApplied.length, // doc fixes applied IN the workflow's Doc stage (since v4.56.0)
573
+ docResidual: docResidualOut.length,
498
574
  codeResidual: codeResidual.length,
499
575
  qaRan: maxQaTier === 'full', // false ⇒ qa-sentinel deferred to the Final FULL gate (qa_first_attempt = n/a)
500
576
  checksFailed, // post-fix lint/tsc/build (coder); distinct from the qa-sentinel gateTable
@@ -502,7 +578,7 @@ const summary = makeSummary({
502
578
  blockers: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
503
579
  highs: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
504
580
  })
505
- log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
581
+ log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.docApplied} doc-applied, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
506
582
 
507
583
  return { codexEngine, perCard, gateTable, summary }
508
584
 
@@ -510,7 +586,7 @@ return { codexEngine, perCard, gateTable, summary }
510
586
  function asArr(x) { return Array.isArray(x) ? x.filter(Boolean) : [] }
511
587
  function dedupe(xs) { return Array.from(new Set(asArr(xs))) }
512
588
  function makeSummary(o) {
513
- return Object.assign({ cards: 0, totalFindings: 0, verified: 0, noAction: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0 }, o || {})
589
+ return Object.assign({ cards: 0, totalFindings: 0, verified: 0, noAction: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0 }, o || {})
514
590
  }
515
591
  function slimFinding(f) {
516
592
  return { finding_id: f.finding_id, title: f.title, severity: f.severity, domain: f.domain, evidence: f.evidence, minimal_fix_direction: f.minimal_fix_direction, classification: f.classification, card: f.card }
@@ -85,6 +85,7 @@ const CODEX_SCHEMA = {
85
85
  properties: {
86
86
  codexAvailable: { type: 'boolean', description: 'false if CODEX_NOT_FOUND / TIMED_OUT' },
87
87
  findings: { type: 'array', items: FINDING },
88
+ codexProse: { type: 'string', description: 'PROSE-ONLY salvage (v4.56.0): when Codex FINISHED but emitted no sentinel JSON block, the cleaned tail of its prose review — handed to the fallback code-reviewer as UNVERIFIED leads. Empty/omitted otherwise.' },
88
89
  note: { type: 'string' },
89
90
  },
90
91
  }
@@ -212,8 +213,9 @@ const codexPrompt =
212
213
  ` awk '/<<<FINDINGS_JSON>>>/{c=1;b="";next}/<<<END_FINDINGS_JSON>>>/{c=0;last=b;next}c{b=b $0 ORS}END{printf "%s",last}' ${codexReviewFile}\n` +
213
214
  ` 5. Decide the return — do NOT re-verify, re-grep, or Read any source file:\n` +
214
215
  ` • awk output is a NON-EMPTY JSON array → parse it and return it verbatim as \`findings\`, codexAvailable:true (its keys already match the schema).\n` +
215
- ` • awk output is EMPTY but the file has "Turn completed." (or "CODEX_NOT_FOUND") → Codex finished WITHOUT the sentinel block (e.g. it wrote a prose report). Return codexAvailable:false, findings:[] and EXIT NOW do NOT wait the rest of the window, and do NOT parse findings out of the prose (a prose-only completion routes to the code-reviewer fallback; never risk dropping a real finding by guessing).\n` +
216
- ` • none of the markers ever appeared after the FULL 10-minute windowcodexAvailable:false. NEVER set false because a single poll returned slowly.\n\n` +
216
+ ` • awk output is EMPTY but the file has "Turn completed." → Codex FINISHED but wrote a prose report instead of the sentinel block. Do NOT parse findings out of the prose yourself (never guess a schema). Instead, capture the prose for the fallback to verify: run \`grep -v '^\\[codex\\]' ${codexReviewFile} | tail -c 12000\` and return codexAvailable:false, findings:[], codexProse:<that output>, then EXIT NOW (do NOT wait the rest of the window). The fallback code-reviewer re-verifies the prose against the real diff so a real finding is recovered, not dropped.\n` +
217
+ ` • awk output is EMPTY and the file has "CODEX_NOT_FOUND"companion missing. Return codexAvailable:false, findings:[] (no codexProse).\n` +
218
+ ` • none of the markers ever appeared after the FULL 10-minute window → codexAvailable:false, findings:[] (no codexProse). NEVER set false because a single poll returned slowly.\n\n` +
217
219
  `TASK PROMPT:\n${codexReviewTask}`
218
220
 
219
221
  const docPrompt =
@@ -276,14 +278,16 @@ let raw = []
276
278
  let gateTable = []
277
279
  let codexEngine = codexResolved ? 'codex' : 'code-reviewer (fallback)'
278
280
  let codexRan = false
281
+ let codexProse = '' // PROSE-ONLY salvage (v4.56.0): seeds the fallback so Codex's real finds aren't lost.
279
282
  for (const item of reviewResults) {
280
283
  if (item.kind === 'codex') {
281
284
  if (item.r && item.r.codexAvailable && Array.isArray(item.r.findings)) {
282
285
  codexRan = true
283
286
  raw.push(...item.r.findings.map((f) => ({ ...f, source: 'codex', preValidated: true })))
284
287
  } else {
285
- // Resolved but the run itself failed (CODEX_NOT_FOUND written / empty after timeout).
286
- log('Review: Codex companion resolved but its review did not complete — falling back to code-reviewer.')
288
+ // Resolved but the run itself failed (CODEX_NOT_FOUND written / empty / prose-only after timeout).
289
+ if (item.r && typeof item.r.codexProse === 'string' && item.r.codexProse.trim()) codexProse = item.r.codexProse.trim()
290
+ log(`Review: Codex companion resolved but its review did not complete${codexProse ? ' (prose-only — salvaging its findings as fallback leads)' : ''} — falling back to code-reviewer.`)
287
291
  }
288
292
  } else if (item.kind === 'qa') {
289
293
  gateTable = (item.r && item.r.gates) || []
@@ -296,11 +300,18 @@ for (const item of reviewResults) {
296
300
 
297
301
  // F-040 — ONE deterministic fallback: either the pre-flight found no companion, or it ran
298
302
  // but did not complete. Either way the primary code review must still happen (F.4 step 8).
303
+ // PROSE-ONLY salvage (v4.56.0): when Codex FINISHED but emitted prose (no sentinel JSON), seed the
304
+ // fallback with that prose as UNVERIFIED leads — the relay never parses prose; the fallback
305
+ // code-reviewer (real reviewer WITH code access) re-verifies each lead against the actual diff and
306
+ // emits the schema, so Codex's cross-model finds are recovered, not discarded. Same trust model.
299
307
  if (!codexRan) {
300
- codexEngine = 'code-reviewer (fallback)'
308
+ codexEngine = codexProse ? 'code-reviewer (codex-seeded)' : 'code-reviewer (fallback)'
309
+ const seedBlock = codexProse
310
+ ? `\n\nCodex (a DIFFERENT model) reviewed this exact batch diff and reported the following findings in PROSE — it failed to emit machine-readable JSON, so these are UNVERIFIED leads, NOT validated findings. VERIFY each against the actual diff at its cited file:line, DROP any that don't hold, then run your own full independent review for anything Codex missed. Treat the prose as hints to check, never as ground truth:\n<codex-prose>\n${codexProse}\n</codex-prose>`
311
+ : ''
301
312
  const fb = await agent(
302
- `Codex was unavailable for the batch final review. Run the FULL code review yourself over the batch diff, per ${protocolRef} Step F.3.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied. An observation you VERIFIED needs NO change (fix direction "no fix required" / "acceptable as-is") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it.`,
303
- { label: 'code-reviewer (fallback)', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
313
+ `Codex was unavailable for the batch final review. Run the FULL code review yourself over the batch diff, per ${protocolRef} Step F.3.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied. An observation you VERIFIED needs NO change (fix direction "no fix required" / "acceptable as-is") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it.${seedBlock}`,
314
+ { label: codexProse ? 'code-reviewer (codex-seeded)' : 'code-reviewer (fallback)', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
304
315
  )
305
316
  // the fallback code-reviewer IS the primary code-review engine here and applied its own FP
306
317
  // check (prompt above) → trusted, exactly like Codex. A SECOND code-reviewer pass over its own
@@ -512,7 +512,7 @@ async function runCard(cardId, cardPath) {
512
512
  const migrationNote = (migrationApplied && migrationAffects.includes(cardId))
513
513
  ? `\nMIGRATION LIVE: the DB migration "${migration.summary || '(declared)'}" was applied to the active DB BEFORE this batch (skill Migration Gate). The schema is REAL — run actual validation against it (validation_commands, queries, DB-generated types); do NOT defer the schema-apply AC or build against an absent schema.`
514
514
  : ''
515
- const cardBrief = `${projectBrief}\n\nCard: ${cardId}\nCard YAML: ${cardPath}\nOwner agent: ${ownerAgent} · Review profile: ${reviewProfile}\nWorktree: ${sharedCtx.worktreePath} (cd into it)\nFile-ownership map: ${sharedCtx.ownershipMapPath}\nNOTE: ACs already pre-classified as policy-deferred MUST NOT be implemented or routed — they are tracked as follow-ups.${migrationNote}`
515
+ const cardBrief = `${projectBrief}\n\nCard: ${cardId}\nCard YAML: ${cardPath}\nOwner agent: ${ownerAgent} · Review profile: ${reviewProfile}\nWorktree: ${sharedCtx.worktreePath} (cd into it)\nWORKTREE ISOLATION (per AGENTS.md § Worktree isolation): every path you Edit/Write/create MUST be UNDER ${sharedCtx.worktreePath}. NEVER write a path under the main repo ${MAIN} — it is checked out live by other sessions; cd alone does not stop an absolute main-repo path. Edit the worktree-rooted copy.\nFile-ownership map: ${sharedCtx.ownershipMapPath}\nNOTE: ACs already pre-classified as policy-deferred MUST NOT be implemented or routed — they are tracked as follow-ups.${migrationNote}`
516
516
 
517
517
  // --- Phase 1 (B7, v4.26.0) — SPECIALIST decomposition: each Phase-1 duty runs as its own
518
518
  // agent ("ognuno fa una cosa"), with handoff via /tmp files so the owner's context stays
@@ -54,6 +54,15 @@ This repo uses a simplified Git Flow.
54
54
 
55
55
  Before committing, ensure only files related to the current task are staged. Run `git status` and `git diff --staged` to verify. Never bundle unrelated changes into a commit.
56
56
 
57
+ ### Worktree isolation (MANDATORY for every subagent spawned into a worktree)
58
+
59
+ A subagent working a card runs inside a git **worktree** — a separate checkout that shares the same repository as the main checkout. The orchestrator gives you the worktree's absolute path. Every path you **Edit / Write / create / delete MUST resolve UNDER that worktree root.** NEVER edit, write, or delete any path under the **main repo root** — it is checked out live by other terminals/sessions and is NOT your workspace; touching it corrupts their working tree.
60
+
61
+ - The canonical copy of every source/doc/config file **exists in your worktree** — edit THAT copy, identified by its worktree-relative path, even if you happen to know the file's absolute path in the main repo.
62
+ - `cd <worktree>` is **NOT sufficient**: an absolute path that points into the main repo (e.g. `/…/<project>/docs/…` instead of `/…/<project>/.worktrees/<branch>/docs/…`) bypasses the `cd` and writes the shared checkout. Always Edit/Write the worktree-rooted path.
63
+ - If a file you need appears to live ONLY in the main repo and not in your worktree, **STOP and report it** — do not reach outside the worktree to "fix" it.
64
+ - **Exception:** the merge/finalizer step (and only it) legitimately operates on main-repo git state. Card-implementation and review/doc writers never do.
65
+
57
66
  ## Investigation Guidelines
58
67
 
59
68
  When investigating whether a feature has been implemented, always check git history (`git log --oneline --all --grep`) and actual source code before claiming it exists or doesn't exist.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.55.1",
3
+ "version": "4.56.0",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"