baldart 4.61.1 → 4.63.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,57 @@ 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.63.0] - 2026-06-23
9
+
10
+ **Fixes from the FEAT-0042 post-mortem — the first real `/new` run on v4.62.0.** v4.62.0 worked (that run billed −29% vs the FEAT-0041 baseline, 1 unplanned fix-coder vs 4, `/mw` and the durable tracker held), but the deep post-mortem surfaced a new class of bug and two real leaks — including one *introduced by* v4.62.0 itself.
11
+
12
+ **MINOR** — adds a `doctor` diagnostic + guidance/agent edits; no layout/install change, **no new `baldart.config.yml` key**.
13
+
14
+ ### Fixed
15
+
16
+ - **CLI ↔ framework version-skew silently disabled the Codex broker teardown (the v4.62.0 leak)** — `merge-cleanup.md` / `new/SKILL.md` / `new2/SKILL.md` + `src/commands/doctor.js`. The framework prose ships via the git subtree (current), but `npx baldart` resolves the SEPARATELY-installed CLI; on a consumer whose global CLI predated v4.62.0, `npx baldart teardown-codex-broker` returned `unknown command`, which `2>/dev/null || true` masked as a no-op → the Codex broker + its Playwright/… MCP children survived every run (FEAT-0042: 5 brokers leaked). The teardown snippets now **capture stderr (no masking), detect `unknown command`, warn to `npm i -g baldart@latest`, fall back to a direct `pkill` of the broker by cwd, and assert the broker is actually gone**. `baldart doctor` gains a **CLI↔framework version-skew check** (warns when the installed CLI is older than the framework, so new `npx baldart` subcommands can't silently fail). The earlier `$MAIN`-non-persistence hypothesis was **refuted** by the post-mortem's adversarial verification — the run passed the correct absolute `--cwd`.
17
+ - **Fix-pass coder could edit the live MAIN repo (worktree-isolation gap)** — `coder.md` + `completeness.md`. The reactive AC-fix-pass spawn had no canonical worktree-isolation clause (it lived only in `implement.md`'s per-card brief), so an improvised thinner brief dropped it and a fix-coder Edited `<main>/src/lib/i18n/*.ts` on the MAIN repo, then reverted + re-applied (~3M wasted; the `tsc 2353` it triggered was a stale artifact, not a real defect). Now: `coder.md` STEP 0 carries an **intrinsic per-Edit path guard** (every Edit path must be worktree-rooted; `cd` is not enough; assert `.worktrees/` before editing shared/locale files) — applies to every spawn even when the brief omits it — and the fix-pass spawn brief must carry the verbatim isolation clause.
18
+ - **i18n locale-parity gate ↔ coder "do-not-translate" contradiction** — `implement.md` / `coder.md` / `i18n-protocol.md` / `prd-card-writer.md`. The `coder` writes the source locale only (by design), but a project's own locale-parity test (a `test`/`build` gate) fails on a source-only commit → `/new` spawned `i18n-translator` reactively (~2M round-trip). Resolved: a **planned per-card `i18n-translator` fill pass** runs after the coder and before the parity gate (when `has_i18n` + new source keys), a parity failure is explicitly an `i18n-translator` task (never a coder retry), and the ownership docs (coder STEP 9.6, the i18n cascade, the P3 DoD line) are reconciled so the coder stays source-only with no contradiction.
19
+
20
+ ### Changed
21
+
22
+ - **`new-session-audit.mjs` detector false-positives fixed (audit tooling).** `B1_mw_delegation` now inspects the actual `Skill` tool-call inputs (not the raw transcript, which quotes `new:new`/`__phase6_merge__` as the merge-cleanup.md "don't do this" text → a false FAIL even when the `/mw` call was correct); `B3_nul_grep` matches only genuine output phrases (`Binary file … matches`, `graphify-out`), not the v4.62.0 guidance text; in-context-merge detection is scoped to Bash commands; and command/mode detection now selects the actual `/new` line (not a later `/model`).
23
+ - **Tracker-write cadence coarsened (O2/M3).** The v4.62.0 "lone tracker-Edit = violation" prose did not change behavior (a measured run still paid 38 standalone tracker-Edit turns ≈13.6M / 9%), so `SKILL.md` § Update rules now mandates tracker writes only at **card-state-changing boundaries**, always co-emitted with that action — recovery reconstructs the phase from commits + `phase_module_loaded` + backlog statuses, so per-phase writes are not needed.
24
+ - **markdownlint debt prevented at `/prd` authoring (L1/L2)** — `prd-writing-phase.md` gains a markdown-hygiene rule: escape a literal `|` in a prose/table cell (`\|` or a code span — the `isManagerOf||isOwner` incident), honor MD058, and run `markdownlint` on generated docs when configured, rather than shipping debt for `/new`'s doc-review to catch.
25
+
26
+ ### Deferred (documented)
27
+
28
+ - **Codex sentinel-JSON prompt tax (~270k/batch)** — `new-card-review.js` already tolerates Codex's prose output (it fetches both the sentinel JSON and the prose tail), the gain is ~0.1%, and the Codex relay is a delicate, much-iterated subsystem; not worth a regression risk in this sweep.
29
+
30
+ ## [4.62.0] - 2026-06-22
31
+
32
+ **Token-economy + reliability hardening for `/new` and `/prd`, distilled from a real `/new FEAT-0041 -full -auto` run that billed ~364M tokens (~95% cache_read).** A deep multi-agent post-mortem (with adversarial verification that refuted several plausible-but-wrong hypotheses — e.g. the workflow-`<result>`-payload-bloat theory was false by ~430×) traced the cost to a small set of root causes: in-context work that should have been off-context (a hallucinated `/mw` delegation and a killed-final-review recovery, ~36M combined), friction multipliers (a NUL-byte/GNU-grep trap ~8–12M, ~28 lone tracker-Edit turns ~11M), and `/prd` design gaps that pushed work downstream into `/new`'s expensive orchestrator context. Plus a proactive Codex-broker teardown so terminated reviews stop leaking MCP processes.
33
+
34
+ **MINOR** — adds one CLI verb (`baldart teardown-codex-broker`) + new util export + skill/agent guidance edits; no layout/install change, **no new `baldart.config.yml` key** (so the schema-change propagation rule does NOT apply). All `/new` paths without the `Workflow` tool, and all Codex-less installs, are unaffected.
35
+
36
+ ### Added
37
+
38
+ - **`baldart teardown-codex-broker --cwd <path>`** (`src/commands/teardown-codex-broker.js` + `bin/baldart.js`) + **`tearDownCodexBroker({cwd, pluginScriptsDir})`** in `src/utils/codex-orphans.js` — a **proactive, cwd-scoped** teardown of the Codex `app-server` broker bound to a specific worktree, freeing its Playwright/Figma/… MCP children. The Codex plugin already ships a clean per-cwd broker shutdown but only fires it on Claude Code `SessionEnd`; a `/new` run that ends (or is killed mid-final-review) hours earlier leaves the broker + MCP subtree burning CPU. Tier 1 reuses the plugin's own `loadBrokerSession`→`sendBrokerShutdown`→`teardownBrokerSession` (graceful); Tier 2 (plugin lib absent / no `broker.json` but the process lingers) matches the broker by its `--cwd` token and SIGTERM-pgroup→SIGKILLs the subtree (reusing `listProcesses`/`collectTree`). Scoped strictly by cwd — never touches a broker for another worktree or the user's interactive session. Validated end-to-end against a real 50-minute-orphaned broker (killed 11 processes). Doctor's `reapOrphans` stays the reactive safety net.
39
+
40
+ ### Fixed (`/new` — bugs surfaced by the post-mortem)
41
+
42
+ - **`/mw` Phase-6 merge now has a concrete `Skill()` invocation** (`merge-cleanup.md` + `worktree-manager/SKILL.md`). The prose-only "delegate to `/mw`" let the orchestrator hallucinate `Skill({skill:"new:new", args:"__phase6_merge__ …"})` → `Unknown skill` → it ran the **entire** Phase-6 merge inline in its ~590k-token context (~20M cache_read). Now: a literal `Skill({skill:"worktree-manager", args:"/mw worktreePath=… checksAlreadyPassed=true"})` + a HARD "never fall back to manual git" rule (interactive → ask; AUTONOMOUS → follow-up card).
43
+ - **Killed `new-final-review` workflow now recovers off-context** (`final-review.md` F.1.5 + `new-final-review.js`). When the workflow was KILLED mid-Fix-phase (no structured return, no notification), the orchestrator hand-parsed the journal (explicitly forbidden) and re-verified all findings + committed fixes in-context (~16M). New "workflow-killed recovery" sub-protocol: detect non-completion, delegate ONE off-context subagent to validate+commit the worktree's uncommitted Fix diffs, never re-verify in-context, never block on a human under AUTONOMOUS. The workflow now emits a **terminal completion sentinel** (its absence ⇒ killed) and the Fix output is documented as uncommitted-until-F.5.
44
+ - **`rg` over GNU `grep` for source searches** (`code-search-protocol.md` + `coder.md`). A single NUL byte in a large generated file silently flips GNU `grep` to binary mode (empty output + exit 1, indistinguishable from "no match"); three coders independently re-discovered one such byte in a `data.ts` (~8–12M). Now a HARD default: prefer `rg`; treat empty+exit-1 on an expected-match file as a binary-mode trap, never as "symbol absent".
45
+ - **Batch tracker relocated off volatile `/tmp`** (`SKILL.md` + `metrics.md`). The recovery-SSOT tracker lived in `/tmp` and was wiped during a pause, forcing an in-context rebuild. Now `$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md` — repo-local, session-stable, survives the worktree removal, never in `git status` (no partition/`.gitignore` change). Plus a degrade branch that reconstructs run state from commits + backlog statuses if the tracker is ever missing.
46
+
47
+ ### Changed (`/new` — optimizations)
48
+
49
+ - **A turn whose only tool call is a tracker `Edit` is now an explicit VIOLATION** (`SKILL.md` § Context economy) — a measured run paid ~28 such standalone turns (~11M). Ride the tracker write along with the next real tool call.
50
+ - **Out-of-lane `preValidated` guard + build-claim reconciliation** in `new-final-review.js`. A narrow specialist (doc-reviewer / api-perf-cost-auditor) FP-checking a finding OUTSIDE its lane no longer short-circuits to VERIFIED — it routes to the correct domain specialist. And a VERIFIED BLOCKER/HIGH asserting a build/compile failure that the SAME review's build/tsc gate contradicts (PASS) is auto-demoted to NEEDS_MANUAL_CONFIRMATION instead of gating the merge on a phantom (the false-positive perf BLOCKER the post-mortem caught).
51
+ - **Codex broker teardown wired into the run-end finalizers** — `merge-cleanup.md` Phase 6c (cwd-scoped teardown FIRST, then the cumulative `reap-orphans`), `SKILL.md` terminal-hygiene (HALT-before-6c), `new2/SKILL.md` Step 5, and a `codex-gate.md` note that the broker is shared/warm and must NOT be torn down per-card.
52
+
53
+ ### Changed (`/prd` — design gates that prevent downstream `/new` waste)
54
+
55
+ - **Consumer enumeration for shared multi-mode functions** (`discovery-phase.md` ISA dimension 9) — when the feature changes a shared list/query/selector/filter fn used in >1 mode/call-site, EVERY consumer is now enumerated as its own ISA touchpoint. This closes the gap that let an orders-wizard catalog picker keep showing hidden products (the exclusion was modeled only at the picker-mode call; the catalog-mode consumer page was unowned → surfaced as 2 HIGH only at the final review, ~28M).
56
+ - **Per-AC seam-file closure + `requirements`-at-write-time** (`card-schema.md` authoring invariants, `audit-phase.md` Codex audit, `validation-phase.md` BLOCKING gates d/e, `prd-card-writer.md`). Every AC's render/seam file (and every file named in requirements/AC/integration_points) must be in `files_likely_touched` (⊆ check); every ISA touchpoint must be owned by some card; and a derivable `requirements` block must be emitted at authoring time — so `/new` no longer pays MAY-EDIT stalls, unplanned fix-coders, or an in-context requirements back-fill.
57
+ - **i18n DoD parity gated on `features.has_i18n`** (`prd-card-writer.md`) — a card introducing user-facing strings now carries a DoD line requiring translation to ALL maintained locales (not just "registered"), shifting the reactive `i18n-translator` spawn author-time-left.
58
+
8
59
  ## [4.61.1] - 2026-06-22
9
60
 
10
61
  **`/new` pre-flight no longer wrongly HALTs/skips well-specified `/prd` cards that merely omit the top-level `requirements` block.** Real-world friction: implementing a `/prd`-designed epic with `/new` repeatedly hit *"N cards lack the top-level `requirements` field (profile CHILD)"* even though those cards had complete `scope`, `scope_boundaries`, `acceptance_criteria`, and `business_rationale` — the requirements were fully derivable, and the user had to hand-back-fill them every time or risk the gate destroying the epic. Root cause: `card-schema.md` listed `requirements` as an **unconditional** HALT (non-derivable), so a derivable-but-omitted block forced an ask/skip. The `prd-card-writer` still mandates emitting `requirements`; this only adds the consumer-side safety net that already exists for `review_profile`/`owner_agent`.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.61.1
1
+ 4.63.0
package/bin/baldart.js CHANGED
@@ -164,6 +164,16 @@ program
164
164
  await reapCommand({ json: !!options.json, dryRun: !!options.dryRun });
165
165
  });
166
166
 
167
+ program
168
+ .command('teardown-codex-broker')
169
+ .description('Proactively end the Codex app-server broker bound to a specific worktree cwd (and its MCP children). Used by the /new run-end + abort finalizers. Scoped by --cwd — never touches a broker serving another worktree or your interactive session.')
170
+ .option('--cwd <path>', 'The workspace cwd whose broker to end (the /new batch worktree path). Defaults to the current directory.')
171
+ .option('--json', 'Machine-readable output: emit a single JSON result object on stdout')
172
+ .action(async (options) => {
173
+ const teardownCommand = require('../src/commands/teardown-codex-broker');
174
+ await teardownCommand({ cwd: options.cwd, json: !!options.json });
175
+ });
176
+
167
177
  const overlayGroup = program
168
178
  .command('overlay')
169
179
  .description('Author and check .baldart/overlays/ — scaffolds, validates, and detects drift on skill/agent/command overlays');
@@ -24,6 +24,7 @@ Violating this rule has caused repeated user frustration. Treat it as a blocking
24
24
 
25
25
  ## Before Starting
26
26
  0. You were spawned by the orchestrator **already on the correct branch / worktree**. The heavy/light decision (worktree or not) was taken before your spawn — see `AGENTS.md § Post-Approval Complexity Gate`. Do not create or switch branches/worktrees. Safety check: at startup run `git rev-parse --show-toplevel` and `git branch --show-current`. If you are inside the main repo but on a branch different from the project's trunk branch (`git.trunk_branch` in `baldart.config.yml`, autodetected default), or the orchestrator asks you to work on a new branch, STOP and report the error: work that requires a new branch must happen inside a worktree (typically `.worktrees/*`), not on the main repo. Do not proceed until you are given a correct worktree path.
27
+ - **WORKTREE ISOLATION — per-Edit path guard (HARD, intrinsic — applies to EVERY spawn, even when the task brief omits it).** Capture your worktree root at startup: `WT="$(git rev-parse --show-toplevel)"`. **Every** path you `Edit`/`Write`/`create` MUST be UNDER `$WT`. `cd`-ing into the worktree is NOT enough — an **absolute** path (or a `../`) that lands in the main repo (`<main>/…`, no `.worktrees/` segment) is checked out live by other sessions and silently pollutes it. Before editing a path, assert it is worktree-rooted; this is non-negotiable for **shared / generated / locale files** (`i18n`/locale `*.ts`, `data.ts`, design-system files) where a main-repo write is most damaging and hardest to spot. If a brief gives you a bare relative path, resolve it against `$WT`, never the process cwd. (FEAT-0042 incident: a fix-pass coder Edited `<main>/src/lib/i18n/{en,fr,es,de}.ts` on the MAIN repo, then had to `git checkout --` + re-apply in the worktree — wasted ~3M tokens and risked main-repo corruption.)
27
28
  1. Read `AGENTS.md`, `agents/index.md`, and `.claude/agents/REGISTRY.md`.
28
29
  2. Verify a backlog card exists for this work.
29
30
  3. Update the card to `IN_PROGRESS`.
@@ -62,7 +63,7 @@ Load context in this exact order before touching any file:
62
63
  2. Add the new key to the source-language native locale file (the `source` value).
63
64
  3. Add a **stub entry** to the registry (`${paths.i18n_registry}`): `source`, a **hyper-brief `context`** (what the label is for / where it appears — one line), and `domain` (the feature area). Set `char_limit`/`icu` when relevant. The QUALITY of `context` is curated by `doc-reviewer` — your job is to never leave the entry missing.
64
65
  4. Follow the naming convention (`namespace.domain.key`, semantic not literal, `global.*` for genuinely reused strings) unless an overlay overrides it.
65
- 5. Do NOT translate into other languages (that is `i18n-translator` / the `/i18n` skill) and do NOT curate other keys (that is `doc-reviewer`). Skipping this step when the feature is on is a protocol violation: a hardcoded user-facing string, or a key referenced in `t()` with no registry entry (`I18N_REGISTRY_DRIFT`), fails review.
66
+ 5. Do NOT translate into other languages (that is `i18n-translator` / the `/i18n` skill) and do NOT curate other keys (that is `doc-reviewer`). Skipping this step when the feature is on is a protocol violation: a hardcoded user-facing string, or a key referenced in `t()` with no registry entry (`I18N_REGISTRY_DRIFT`), fails review. **You source-only by design — even if the project has a locale-PARITY test that fails on your source-only commit, do NOT add the other locales yourself: the `/new` pipeline runs a planned `i18n-translator` fill pass after you (and before the parity gate) to satisfy it (`implement.md` Phase 2 step 9; `i18n-protocol.md` cascade). Adding translations yourself is the violation, not the missing locales.**
66
67
 
67
68
  ## Conditional Requirements — Binary-Outcome Items (MUST)
68
69
 
@@ -70,7 +71,7 @@ Any requirement of the form "verify X; if found → do A; if missing → do B/le
70
71
  is a **binary-outcome item**. BOTH branches have a mandatory artifact — silence is never valid.
71
72
 
72
73
  Rules:
73
- 1. **Verify actively**: read the relevant code (don't assume). Use Grep/Read.
74
+ 1. **Verify actively**: read the relevant code (don't assume). Use Grep/Read. **Prefer `rg` over GNU `grep` on source** — GNU `grep` silently flips to binary mode on the first NUL byte (common in large generated/bundled `data.ts`-style files) and then prints nothing + exits 1, *indistinguishable from "no match"*. An empty result + exit 1 on a file you expect to match is a binary-mode trap, NOT proof the symbol is gone: re-run with `rg` (or `grep -a`) before acting. See `framework/agents/code-search-protocol.md` § Budget.
74
75
  2. **Branch A (condition satisfied)**: implement A as specified.
75
76
  3. **Branch B (condition not met)**: produce B — either:
76
77
  - A code change (add the missing piece), OR
@@ -433,7 +433,15 @@ Every card MUST include ALL fields from the template:
433
433
  - `scope` (summary, users, value)
434
434
  - `requirements` (concrete, specific)
435
435
  - `acceptance_criteria` (testable `[ ] [AC-N]` items)
436
- - `definition_of_done` (checklist including doc invariants)
436
+ - `definition_of_done` (checklist including doc invariants). **When `features.has_i18n: true`** AND the
437
+ card introduces user-facing strings, the DoD MUST carry an i18n-completeness line — NOT merely "strings
438
+ via `t()` + registered", but: *"new keys authored in the source locale + the i18n context registry (by
439
+ the `coder`), then filled to ALL maintained target languages by the `/new` pipeline's planned
440
+ `i18n-translator` pass; the i18n parity / no-placeholder gate passes."* This is an OUTCOME the card
441
+ commits to, owned by the pipeline (`implement.md` Phase 2 step 9), **not** a coder-translates instruction
442
+ — the coder stays source-only (`coder.md` STEP 9.6). Stating it makes the translation pass a planned
443
+ step instead of a reactive ~2M-token round-trip after a parity-gate failure. Cross-ref
444
+ `framework/agents/i18n-protocol.md`.
437
445
  - `depends_on` / `blocks` (card IDs)
438
446
  - `estimated_complexity`
439
447
  - `review_profile` (`skip`|`light`|`balanced`|`deep` — computed via Rule C, or honored if pre-set)
@@ -100,13 +100,20 @@ precedence caveats: `framework/agents/effort-protocol.md`.
100
100
 
101
101
  ## Context Tracking (CRITICAL)
102
102
 
103
- You MUST maintain a **persistent tracking file** at `/tmp/batch-tracker-<FIRST-CARD-ID>.md` throughout the entire batch run (e.g., `/tmp/batch-tracker-FEAT-0396.md`). Use the **first card ID** from the batch as the suffix. This ensures multiple `/new` sessions running in parallel terminals (e.g., one per worktree) do NOT conflict.
103
+ You MUST maintain a **persistent tracking file** (`$TRACKER`) throughout the entire batch run.
104
+ `$TRACKER` is deterministic — recompute it any time (even after a compaction) from the first card ID:
104
105
 
105
- This file is your single source of truth — if your context gets compacted or you lose track of what happened, **re-read this file first**.
106
+ ```
107
+ $TRACKER = "$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md"
108
+ ```
109
+
110
+ i.e. inside the repo's **shared git dir** (e.g. `<main>/.git/baldart/run/batch-tracker-FEAT-0396.md`). Use the **first card ID** as the suffix so parallel-terminal `/new` sessions do NOT conflict; `mkdir -p "$(dirname "$TRACKER")"` once before the first write. **Why the shared git dir and NOT `/tmp`:** `/tmp` is wiped on idle/reboot — a real run lost its tracker mid-pause when `/tmp` was cleaned and had to rebuild it in-context. `git rev-parse --git-common-dir` resolves to `<main>/.git` **even when `/new` runs from inside a worktree** (unlike `--show-toplevel`), so the tracker is session-stable, survives the Phase 6 worktree removal, and is **never tracked by git** (zero `git status` pollution — no clean-tree-partition or `.gitignore` change needed).
111
+
112
+ This file is your single source of truth — if your context gets compacted or you lose track of what happened, **recompute `$TRACKER` and re-read it first** (§ "Context recovery protocol").
106
113
 
107
114
  ### Tracking file format
108
115
 
109
- At batch start — **before Phase 0 runs** (Phase 0 step 1 writes to the `## Worktree` section and Phase 0 step 5 writes the phase log, so the file MUST already exist) — create `/tmp/batch-tracker-<FIRST-CARD-ID>.md` with:
116
+ At batch start — **before Phase 0 runs** (Phase 0 step 1 writes to the `## Worktree` section and Phase 0 step 5 writes the phase log, so the file MUST already exist) — create `$TRACKER` with:
110
117
 
111
118
  ```markdown
112
119
  # Batch Run: [CARD-IDS]
@@ -160,7 +167,7 @@ Trunk branch: [resolved git.trunk_branch — Phase 0 step 0 populates]
160
167
  ### Update rules
161
168
 
162
169
  - **Before starting a card**: move it to `## Current Card` with phase info, and record `phase_module_loaded: <modulo>` as you Read each phase module (per § "Routing" HARD RULE) so context recovery can re-load it.
163
- - **After each phase**: update the current card's phase status in the tracker.
170
+ - **At card-state-changing boundaries only** (NOT after every sub-step): update the tracker's phase status when something recovery-relevant changes — a commit, a gate verdict, a blocker, a phase-module load. **Always co-emit the `Edit` with that action's tool call** (never a standalone status-only turn — § "Context economy" makes a lone tracker-`Edit` turn a violation). A measured run still paid 38 standalone tracker-Edit turns (~13.6M / ~9%) because "after each phase" was read as "emit a status turn per phase" — so the cadence is deliberately coarsened here: between two real actions there is nothing to write. Fine-grained phase recovery does not depend on per-phase writes — § "Context recovery protocol" reconstructs the current phase from the last commit + `phase_module_loaded` + backlog statuses.
164
171
  - **After completing a card**: move it from `Current Card` to `## Completed Cards` with:
165
172
  - Commit hash
166
173
  - One-line summary of what was implemented
@@ -173,14 +180,14 @@ Trunk branch: [resolved git.trunk_branch — Phase 0 step 0 populates]
173
180
  - QA findings file (e.g. `/qa/FEAT-XXXX.md` or "skipped")
174
181
  - **card_status: DONE (verified)** — confirms the backlog YAML was updated and re-read to verify
175
182
  - **When blocked**: log the blocker in `## Issues & Flags`.
176
- - **On context recovery**: if you ever feel lost or after context compaction, IMMEDIATELY read your tracker file (`/tmp/batch-tracker-<FIRST-CARD-ID>.md`) to restore your state.
183
+ - **On context recovery**: if you ever feel lost or after context compaction, IMMEDIATELY read your tracker file (`$TRACKER`) to restore your state.
177
184
  - **The tracker is internal and is the ONLY state surface** (since v4.47.0 — see § "State surface — the tracker only"): the user does NOT see it, and there is no separate user-visible mirror to keep in sync. Do NOT spend a dedicated turn restating a transition to the user (no TaskUpdate, no Progress-Bar block) — surface progress only as a short prose line folded into a turn you are already taking for real work (§ "Context economy" turn-count rule).
178
185
 
179
186
  ---
180
187
 
181
188
  ## State surface — the tracker only
182
189
 
183
- `/new` keeps a **single** state surface: the internal `/tmp/batch-tracker-<FIRST-CARD-ID>.md`
190
+ `/new` keeps a **single** state surface: the internal `$TRACKER`
184
191
  recovery SSOT (§ "Context Tracking"). There is **no separate user-visible mirror**. The Progress
185
192
  Bar markdown block and the per-transition native Task spine (TaskCreate/TaskUpdate) were **removed
186
193
  for context economy (v4.47.0)**: every standalone TaskUpdate / Progress-Bar emission was its own
@@ -267,6 +274,14 @@ baselines. Keep that bulk on disk and pass **paths**, not bodies.
267
274
  > the transition happens (a compaction between would lose it — § "Context recovery protocol" re-reads
268
275
  > the tracker, not your memory).
269
276
  >
277
+ > **A turn whose ONLY tool call is a tracker `Edit` is a VIOLATION.** There is ALWAYS a next real action
278
+ > to co-emit it with — the next phase's first `Read`/`Bash`, the card hand-off's next-card `Read`, the
279
+ > commit. A measured run paid ~28 standalone tracker-`Edit` turns (~490k replay each ≈ **~11M wasted**)
280
+ > despite the batching rule above. So make it reflexive: if you are about to emit a lone tracker `Edit`,
281
+ > STOP and fold it into the next real tool call's message instead. The only turns that may carry the
282
+ > tracker write alone are the unavoidable phase-boundary writes where there is genuinely no next action
283
+ > yet (rare) — and even there, prefer to ride it along with the first action of the next phase.
284
+ >
270
285
 
271
286
  > **HARD RULE — orchestrator is near-silent / maximally cryptic (since v4.49.0).** `/new` is NOT
272
287
  > conversational: the user does not read its running narration and keeps the orchestrator attached ONLY so it
@@ -380,7 +395,8 @@ per-card nei sub-step D.x (mai aggregate). Caricalo quando Pre-flight seleziona
380
395
  ## Context recovery protocol
381
396
 
382
397
  If at ANY point you are unsure where you are in the batch:
383
- 1. Read your tracker file (`/tmp/batch-tracker-<FIRST-CARD-ID>.md`)
398
+ 1. **Recompute `$TRACKER`** = `$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md` (the first card ID is in the batch args — the path is deterministic) and Read it.
399
+ - **If the tracker is MISSING** (a pre-this-release run still on volatile `/tmp`, or the file was deleted during a pause): do **NOT** rebuild it from memory in-context. **Reconstruct from the durable witnesses** — git + the backlog, which are the real SSOT the tracker only caches: the worktree branch's commits (`git -C <worktree> log --oneline "$TRUNK"..HEAD` — each `[CARD-ID]` commit ⇒ that card progressed/completed) and each batch card's backlog YAML `status:` (`DONE` ⇒ completed). Re-create `$TRACKER` from those facts (which cards are DONE, which is current), then continue from there.
384
400
  2. Check `## Current Card` — if populated, resume that card at the listed phase.
385
401
  2b. **Pre-flight not finished?** If `## Current Card` is empty / `(none — starting pre-flight)` AND `## Worktree` still shows skeleton placeholders (no real `Path:`/`Port:`), then a compaction hit mid-pre-flight (most likely during the step-5 background barrier). Re-enter **Pre-flight at step 4** (Read the `setup.md` module first per § "Routing"): its step-4a2 git pre-check re-detects an already-created worktree via `git worktree list` and resumes-or-resets it safely — so re-entry never double-creates or fails-loud.
386
402
  3. **Re-Read the phase module** named in `## Current Card`'s `phase_module_loaded:` field (a compaction may have evacuated it from context — see § "Routing" HARD RULE). If the field is absent, map the listed phase to its module via the § "Routing" table and Read that.
@@ -388,6 +404,21 @@ If at ANY point you are unsure where you are in the batch:
388
404
  5. Check `## Completed Cards` — know what's already done (don't redo).
389
405
  6. Continue the pipeline from where you left off.
390
406
 
407
+ **Terminal hygiene — tear down the Codex broker on ANY exit before Phase 6c.** The normal close runs the
408
+ cwd-scoped Codex broker teardown in Phase 6c step 5b. But if the batch ends **before** reaching Phase 6c —
409
+ an unrecoverable HALT, or resuming a run whose final-review workflow was KILLED (see `final-review.md`
410
+ § "workflow-killed recovery") and then deciding to stop — the batch's `codex app-server` broker (and its
411
+ Playwright/… MCP children) is still alive. Before exiting, run it once (idempotent, cwd-scoped, NON-blocking):
412
+ ```bash
413
+ WT="$MAIN/<relative worktree Path from tracker ## Worktree>"
414
+ TD="$(npx baldart teardown-codex-broker --cwd "$WT" 2>&1)"
415
+ printf '%s' "$TD" | grep -qi "unknown command" \
416
+ && { echo "AUTO: codex-teardown CLI unavailable — run npm i -g baldart@latest; killing broker by cwd"; pkill -f "app-server-broker\.mjs.*--cwd $WT" 2>/dev/null || true; } \
417
+ || printf '%s\n' "$TD"
418
+ ```
419
+ This NEVER touches a broker for another worktree or the user's interactive session (different cwd). If the
420
+ orchestrator is hard-killed and never resumed, the `baldart doctor` reaper is the final safety net.
421
+
391
422
  ---
392
423
 
393
424
 
@@ -37,6 +37,13 @@ Log the detector output in the tracker for traceability. **This step never short
37
37
 
38
38
  #### Step C — Invoke per-card `/codexreview` (ALWAYS)
39
39
 
40
+ > **Do NOT tear down the Codex broker per-card.** The `codex app-server` broker is **one per worktree
41
+ > cwd, shared and warm-reused across every per-card + final-review Codex call** in the batch. Ending it
42
+ > after a card would force a cold re-spawn (~2s) for the next card's review. The broker (and its MCP
43
+ > children) is torn down ONCE at run-end — `merge-cleanup.md` Phase 6c step 5b (cwd-scoped
44
+ > `baldart teardown-codex-broker`), with the `baldart doctor` reaper as the safety net. Per-card hygiene
45
+ > is none.
46
+
40
47
  For EVERY card (no conditional skip — the gate ALWAYS runs; only its DEPTH varies):
41
48
 
42
49
  0. **Select the Codex review profile (deterministic — Review Profile Selector)** by mapping the
@@ -256,7 +256,7 @@ This gate enforces `framework/agents/workflows.md § Scope Closure Discipline` a
256
256
  For each `deferred` row whose `User-approved?` is `pending`, invoke `AskUserQuestion` with:
257
257
  - Question: `"AC-<N> of <CARD-ID> non implementata. Testo verbatim: '<AC text>'. Motivazione dell'implementer: '<reason or 'nessuna — silent skip'>'. Come procedo?"`
258
258
  - Options (max 4):
259
- 1. **"Implementa adesso"** — spawn a targeted fix `coder` agent scoped to this card's MAY EDIT files (from `## File Ownership Map`) with the instruction "Implement ONLY AC-<N>: '<AC text>'. Do not refactor or expand scope." Re-run Phase 2.5 verification on the resulting diff — **including the mandatory sub-steps 5b (API contract), 5c (alias-mutation), 5d (caller-pattern test)** if the fix touched a route, an exported helper, or a call site (a single-AC fix can still introduce these). **Hard cap: 2 attempts on the same AC.** On the 2nd failure, do NOT re-offer "Implementa adesso" — re-invoke `AskUserQuestion` with only options 2/3/4 (approve deferral / follow-up card / stop), so the loop cannot recur unbounded.
259
+ 1. **"Implementa adesso"** — spawn a targeted fix `coder` agent scoped to this card's MAY EDIT files (from `## File Ownership Map`) with the instruction "Implement ONLY AC-<N>: '<AC text>'. Do not refactor or expand scope." **The brief MUST carry the verbatim WORKTREE ISOLATION clause** (the same one the per-card brief uses — `implement.md` § FORBIDDEN: "every path you Edit/Write/create MUST be UNDER the worktree root `<ABSOLUTE_WORKTREE_PATH>`; `cd` is not enough; an absolute main-repo path bypasses it"). Do NOT improvise a thinner fix-pass brief that drops it — a reactive fix-coder with a weak brief is exactly where the FEAT-0042 main-repo i18n write happened (`coder.md` STEP 0's per-Edit path guard is the intrinsic backstop, but the brief must still carry it). Re-run Phase 2.5 verification on the resulting diff — **including the mandatory sub-steps 5b (API contract), 5c (alias-mutation), 5d (caller-pattern test)** if the fix touched a route, an exported helper, or a call site (a single-AC fix can still introduce these). **Hard cap: 2 attempts on the same AC.** On the 2nd failure, do NOT re-offer "Implementa adesso" — re-invoke `AskUserQuestion` with only options 2/3/4 (approve deferral / follow-up card / stop), so the loop cannot recur unbounded.
260
260
  2. **"Approva il deferral"** — record `[USER-APPROVED DEFERRAL] <today>: <user-supplied reason>` on its own line in the card's `implementation_notes`. Mark the row `User-approved? yes`.
261
261
  3. **"Sposta su follow-up card"** — create the follow-up card by **delegating to the `prd-card-writer` agent in Standalone Single-Card Mode** (NOT a hand-written stub — backlog cards are owned by `prd-card-writer`, same discipline as `new2`/`new2-resolve`). Brief it: `MODE: standalone`, write `${paths.backlog_dir}/<CARD-ID>-followup-AC<N>.yml` with `status: TODO`, derived from the deferred AC — a non-empty `requirements` (≥1, derived from the AC), `acceptance_criteria` = the verbatim AC text (≥1), `files_likely_touched` (≥1, carried from the parent card's ownership map for this AC), `owner_agent` routed to the AC's domain, and `review_profile` per Rule C — i.e. the full STANDALONE baseline of `framework/agents/card-schema.md`. If `prd-card-writer` is unavailable (total outage), fall back to a minimal valid stub carrying at least the step-1b HALT fields (`requirements`/`acceptance_criteria`/`files_likely_touched`/`scope`); the consumer back-fill (step 1b-ii) then fills `review_profile`/`owner_agent` on first ingestion. Mark the row `User-approved? yes (follow-up: <new-card-id>)`. Do NOT proceed to Phase 2.55 until the follow-up file exists on disk and passes the step-1b field check.
262
262
  4. **"Ferma il batch"** — halt the orchestrator, leave the worktree intact, log the reason in `## Issues & Flags`. Do NOT commit.
@@ -136,6 +136,36 @@ that is a **gate violation**: log it as
136
136
  new-final-review workflow (engine: <codexEngine>, fixes: <fixesApplied.length>+<docFixesApplied.length>, residual: <residual.length>)`
137
137
  in the tracker.
138
138
 
139
+ - **IF the delegated workflow did NOT return a structured value** — the `Workflow`
140
+ tool result is `null` / an error, OR no completion `<task-notification>` arrived
141
+ after a bounded poll window — the run was **killed / aborted mid-flight** (e.g. a
142
+ user interrupt, an OOM, a rate-limit kill — the same abort class as the team-mode
143
+ empty-result post-mortem). **Do NOT improvise an in-context recovery.** A measured
144
+ run that hand-parsed the journal and **re-verified all findings + committed the Fix
145
+ diffs in the orchestrator's own ~540k-token context cost ~16M cache_read** — the
146
+ single largest avoidable block in that batch. Recover OFF-context instead:
147
+ 1. **Detect, don't reconstruct.** You MAY read ONLY the journal's *terminal status*
148
+ to confirm non-completion (`status` ≠ `completed`, or a `fix … started` entry with
149
+ no matching result). You may **NOT** rebuild the findings/verdicts from the journal —
150
+ that is the forbidden hand-parse, and the Fix phase may have run **partially**, so
151
+ the journal is an unreliable, half-written picture.
152
+ 2. **Locate the partial work on disk, hands-off.** `git -C <worktree> status --porcelain`
153
+ + `git -C <worktree> diff --stat` (name/stat only — never read the diff bodies into
154
+ your context). Uncommitted changes = the killed Fix phase's unvalidated output.
155
+ 3. **Delegate ONE off-context subagent** (a `qa-sentinel` for the build/lint/test/tsc
156
+ gate, plus — if the diff is non-trivial — a `code-reviewer`/`coder` to judge the
157
+ partial fixes against the F.3 findings file) to: verify the uncommitted diffs
158
+ compile and pass the gates, **commit them if good or revert them if broken**, and
159
+ return a **COMPACT residual** (the still-unaddressed findings, slim). The
160
+ orchestrator reads only that residual — it does NOT re-verify findings itself.
161
+ 4. **Carry `residual` into Step F.5** exactly as the happy path does. **AUTONOMOUS**:
162
+ NEVER stop and wait for a human "riprendi" — if the recovery subagent cannot fully
163
+ resolve, materialize a follow-up card with the residual and proceed (log
164
+ `AUTO: final-review-killed → recovered off-context (residual: N) [+ follow-up <id>]`).
165
+ **Interactive**: surface the residual terse and continue; only `AskUserQuestion` if
166
+ the recovery subagent itself reports an unsafe state (e.g. a broken build it could
167
+ not revert cleanly).
168
+
139
169
  - **ELSE** (no `Workflow` tool — older Claude Code, `disableWorkflows`, or a
140
170
  Codex install where workflows have no equivalent) → run the **inline F.2–F.4
141
171
  below exactly as written**. This prose is the SSOT and the always-available
@@ -373,6 +373,7 @@
373
373
  ```
374
374
  The `echo "<gate>:$?"` lines surface only the **exit code** inline — the full log stays on disk. When `stack.language` does NOT include `typescript`, skip the `tsc` line (no equivalent gate). Capturing to `/tmp/*-<CARD-ID>.txt` guarantees the failing output is available for step 9. On a non-zero exit, read a **bounded** extract only (`tail -n 30 /tmp/<gate>-<CARD-ID>.txt`), never the whole log — do NOT run the gates without redirect-to-file capture.
375
375
  9. **If any check fails**: categorize the error (`lint | TypeScript | test | build | i18n`), log it in the tracker as `retry-cause: <category>`. An `i18n` failure means a hardcoded user-facing string — the fix is to externalize it via `t()` + register the key (coder STEP 9), never to weaken the gate. Because the gate is **diff-scoped to this card's changed files**, an `i18n` failure is unambiguously a string **this card** introduced — it is never pre-existing baseline debt in an untouched file (so do NOT spend a retry triaging "is this mine?"; it is). The Final review re-runs the gate over the whole batch diff as the merge backstop.
376
+ - **i18n locale-PARITY ≠ anti-hardcoded (do NOT retry the coder on it).** The `i18n` category above is the *anti-hardcoded* gate. A separate **locale-parity** failure — the project's OWN parity test/build complaining that a new key exists in the source locale but not in the others — surfaces under `test`/`build`, and is **NOT a `coder` retry**: the `coder` writes the source locale only **by design** (`coder.md` STEP 9.6), so retrying it just loops. Route it to **`i18n-translator`** (fills the missing target locales context-aware from the registry), then re-run the gate. **Proactively — to avoid the fail-then-fix round-trip (a measured ~2M tokens, FEAT-0042): when `features.has_i18n` AND this card's diff added new source-locale keys, spawn the `i18n-translator` fill pass right after the coder commits and BEFORE these gates**, so parity holds first-try. This is the planned per-card i18n cascade step (`agents/i18n-protocol.md`); the coder still never translates.
376
377
  **Code-recovery check (MUST do before rewriting)**: before spawning a fix agent or rewriting code, check whether lint-staged or a pre-commit hook removed code that is actually needed (e.g. a field "unused" at commit time but consumed by later code). Inspect the captured diff and `git diff`/`git log -p` for the worktree branch and restore the needed code by an explicit file write. **Do NOT `git stash pop` inside the worktree** — `refs/stash` is globally shared across worktrees (`$GIT_COMMON_DIR`), so a stash created or popped in a worktree can corrupt another worktree's state (see Phase 4 WORKTREE COMMIT RULE). If you need to set work aside in a worktree, make a clearly-labelled WIP commit and record its hash in the tracker for later squash, never a stash.
377
378
  When spawning a fix agent: scope it exclusively to this card's Edit-allowed files (from `## File Ownership Map`) — it MUST NOT touch files owned by other cards. Pass the fix agent: the **path** to the captured gate log (`/tmp/<gate>-<CARD-ID>.txt` — it Reads the log itself; do NOT inline-paste the full log into its prompt), the error category, and the explicit list of files it may edit. Do NOT ask the user — just fix and re-run. Fix the code, not the tests (unless the test itself is wrong). Repeat up to **3 times**. **Stuck-loop guard**: compare the failing error's fingerprint (file:line + message) between retries; if the SAME error reproduces on 2 consecutive retries, the fix is not converging (often a constraint outside the card's ownership map) — stop early, log `[STUCK-LOOP] <error>` in `## Issues & Flags`, and escalate to the user rather than burning the 3rd retry on an identical failure.
378
379
  10. If still failing after 3 retries (or on a stuck loop), log the failure in `## Issues & Flags` and ask the user before continuing.
@@ -14,10 +14,16 @@ After the final review passes AND all cards are committed in the worktree, deleg
14
14
  git status --porcelain
15
15
  ```
16
16
  If ANY uncommitted files exist (staged, unstaged, or untracked), commit them NOW with `[safety] Auto-commit remaining files before merge`. Do NOT proceed to `/mw` with a dirty worktree — files WILL be lost during rebase.
17
- 2. Invoke `/mw` with:
18
- - The worktree path and branch from the tracker
19
- - `checksAlreadyPassed: true` (final review + QA already validated the build)
20
- - All card IDs for the commit message
17
+ 2. **Invoke the worktree-manager skill via the Skill tool — this is the ONLY merge mechanism.** The skill that owns `/mw` is named **`worktree-manager`** (NOT `new`, NOT `new:new`, NOT `mw`). Call it exactly, with a concrete payload — never describe the delegation in prose and hope:
18
+
19
+ ```
20
+ Skill({ skill: "worktree-manager",
21
+ args: "/mw worktreePath=<absolute worktree path from tracker ## Worktree> checksAlreadyPassed=true" })
22
+ ```
23
+
24
+ - `worktreePath` — the absolute path from the tracker `## Worktree` section (REQUIRED). The skill resolves the branch + the card IDs for the commit message from the worktree/registry itself — do **NOT** invent `branch` / `card_ids` / `worktree_path` payload keys (the real input contract is `{ worktreePath, checksAlreadyPassed }`, worktree-manager/SKILL.md § "/mw programmatic").
25
+ - `checksAlreadyPassed=true` — the final review + QA already validated the build, so the skill skips re-running quality gates (the safety commit ALWAYS still runs regardless).
26
+ - **HARD — never hand-merge in this orchestrator.** If the Skill call errors (unknown skill, bad args) or the skill STOPs on a conflict/build failure, do **NOT** fall back to inline `git merge` / `git rebase` / `git push` here. Hand-merging runs the entire Phase 6 in the cached prefix (a measured **~20M-token** regression) and risks a non-isolation-safe `git checkout` on the shared main repo. Instead: surface the skill's error/STOP report verbatim, then — **interactive** → `AskUserQuestion` how to proceed; **AUTONOMOUS** → leave the worktree intact and materialize a follow-up card describing the merge blocker (log `AUTO: merge-delegation-failed → follow-up <id>`). `new:new` and `__phase6_merge__` are hallucinations — there is no such skill or arg; the only valid call is the `worktree-manager` one above.
21
27
  3. The skill handles: safety commit of any remaining uncommitted files (step 3), rebasing onto the latest trunk (`$TRUNK` = `git.trunk_branch`) (step 4b — auto-resolves doc conflicts, stops on code conflicts), merging into the trunk via the configured `git.merge_strategy` (step 4c — `pr` uses `gh pr merge`, `local-push` does a direct FF push to `origin/$TRUNK`; NEITHER runs `git checkout` of the trunk on the main repo, respecting the absolute terminal-isolation rule), post-merge verification, worktree removal, registry cleanup, and remote branch deletion.
22
28
  4. **If code merge conflicts** → the skill STOPs and reports. Doc-only conflicts (ssot-registry.md, project-status.md, etc.) are auto-resolved by keeping both sides.
23
29
  5. **If post-merge build fails** → the skill STOPs and keeps the worktree intact for investigation.
@@ -186,19 +192,50 @@ The most common failure mode is leaving cards IN_PROGRESS after merge. This crea
186
192
  - Options: `[Lascia lo stash + apri istruzioni per merge manuale]` / `[Mostrami il conflitto inline]` / `[Halt]`.
187
193
  - **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (a stash-pop conflict needs human merge) → leave the stash intact (NEVER drop it) and materialize a follow-up card with the restore instructions; log `AUTO: stash-restore-conflict → follow-up <id> (stash retained)`.**
188
194
 
189
- 5b. **Process hygiene — reap orphaned Codex MCP servers (NON-BLOCKING)**. This
190
- batch's per-card / final-review Codex calls drive `codex app-server`, whose
191
- broker spawns the MCP servers declared in `~/.codex/config.toml` (Playwright,
192
- …) as children; when a broker dies the OS reparents those MCP servers to init
193
- (ppid 1) and they keep burning CPU. Sweep them now so the batch ends clean:
194
- ```bash
195
- npx baldart reap-orphans 2>/dev/null || true
196
- ```
197
- This reaps ONLY orphaned MCP servers (ppid 1 their broker is already dead
198
- stdio is broken dead weight). It deliberately NEVER kills a live
199
- `codex app-server` broker (a shared, detached runtime that may still serve the
200
- user's interactive session). Never gate the close on this any error or a
201
- "nothing to reap" result is fine; capture its one-line summary for the log.
195
+ 5b. **Process hygiene — end THIS batch's Codex broker, then sweep stragglers (NON-BLOCKING, two steps)**.
196
+ This batch's per-card / final-review Codex calls drive a `codex app-server` broker — **one per
197
+ worktree cwd, shared across all the batch's Codex calls** — whose broker spawns the MCP servers
198
+ declared in `~/.codex/config.toml` (Playwright, …) as children. The Codex plugin only ends that
199
+ broker on Claude Code `SessionEnd` (which can be hours away), so without this the broker and its MCP
200
+ children keep burning CPU for the rest of the session.
201
+ 1. **Proactive, cwd-scoped teardown FIRST** — end the broker bound to THIS batch's worktree. Resolve
202
+ the absolute worktree path from the tracker `## Worktree` `Path:` against `$MAIN`, and tear it down
203
+ **even though `/mw` already removed the worktree directory** (the broker is matched by its `--cwd`
204
+ token / state-hash, both of which survive the dir removal):
205
+ ```bash
206
+ WORKTREE="$MAIN/<relative Path from tracker ## Worktree>" # absolute pathresolve $MAIN in THIS shell
207
+ # Capture stderr do NOT mask with 2>/dev/null (that silently hid a real leak: a consumer whose
208
+ # installed CLI predates the subcommand got `unknown command`, swallowed, and the broker survived).
209
+ TD_OUT="$(npx baldart teardown-codex-broker --cwd "$WORKTREE" 2>&1)"
210
+ if printf '%s' "$TD_OUT" | grep -qi "unknown command"; then
211
+ # CLI/framework version skew: the prose ships via the git subtree (current), but `npx baldart`
212
+ # resolves the separately-installed CLI, which may be older and lack this subcommand. Don't leak
213
+ # silently — warn + kill the broker directly by cwd so its MCP children orphan to init and the
214
+ # reap-orphans line below sweeps them.
215
+ echo "AUTO: codex-teardown CLI unavailable — installed baldart predates v4.62.0; run \`npm i -g baldart@latest\`. Falling back to a direct broker kill."
216
+ pkill -f "app-server-broker\.mjs.*--cwd $WORKTREE" 2>/dev/null || true
217
+ else
218
+ printf '%s\n' "$TD_OUT" # one-line summary (method / killed N) for the log
219
+ fi
220
+ ```
221
+ This is **scoped strictly by cwd** — it ends ONLY this worktree's broker (graceful plugin shutdown
222
+ when the broker.json is present, else a signal to the cwd-matched broker subtree). It NEVER touches
223
+ a broker serving another worktree or the user's interactive Codex session on the main repo (a
224
+ different cwd). Do NOT run this per-card — the broker is shared and warm across the batch; ending it
225
+ mid-batch would force a cold re-spawn for the next card.
226
+ 2. **Then the cumulative orphan sweep + assert** (second line) — mop up any MCP server orphaned to init
227
+ (ppid 1), then VERIFY this worktree's broker is actually gone (so a skewed CLI / partial kill is
228
+ surfaced, not silently tolerated):
229
+ ```bash
230
+ npx baldart reap-orphans 2>/dev/null || true
231
+ ps -eo command | grep -q "app-server-broker\.mjs.*--cwd $WORKTREE" \
232
+ && echo "WARN: a Codex broker for $WORKTREE is STILL alive after teardown — update baldart (npm i -g baldart@latest) and/or re-run reap-orphans." || true
233
+ ```
234
+ `reap-orphans` reaps ONLY orphaned MCP servers (ppid 1 ⇒ their broker is already dead ⇒ stdio is
235
+ broken ⇒ dead weight); it NEVER kills a live broker. The assert is the safety net for the skew bug
236
+ (FEAT-0042: 5 brokers survived because `teardown-codex-broker` was masked as a no-op on an old CLI).
237
+ Never gate the close on these — any error or a "nothing to do" result is fine; capture each one-line
238
+ summary for the log.
202
239
 
203
240
  6. **Log and exit**:
204
241
  ```
@@ -208,7 +245,8 @@ The most common failure mode is leaving cards IN_PROGRESS after merge. This crea
208
245
  Divergence (local…origin/$TRUNK): <0\t0 | resolved: pushed/cherry-picked/ff-pulled/rebased>
209
246
  Sync-deferred markers: <none | reconciled | user-retained>
210
247
  Phase 0 snapshot restore: <n/a | popped clean | conflict-deferred-to-user>
211
- Codex MCP hygiene: <reaped N/M | nothing to reap | skipped (error)>
248
+ Codex broker teardown: <graceful (pid) | signalled N | none | skipped (error)>
249
+ Codex MCP hygiene (reap): <reaped N/M | nothing to reap | skipped (error)>
212
250
  Completed: <timestamp>
213
251
  ```
214
252
  If any step ended in HALT, set `Status: HALT` and report — Phase 7 must NOT start with an unclean main repo unless the user explicitly chose `[Lascia così]`.
@@ -12,7 +12,7 @@ has no blocking consequence the honest label is "monitoring signal".
12
12
 
13
13
  **Steps:**
14
14
 
15
- 1. Read the batch tracker file (`/tmp/batch-tracker-<FIRST-CARD-ID>.md`) to extract — every field below has a NAMED upstream producer; read the structured key, do NOT re-parse prose:
15
+ 1. Read the batch tracker file (`$TRACKER` = `$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md`, see new/SKILL.md § "Context Tracking") to extract — every field below has a NAMED upstream producer; read the structured key, do NOT re-parse prose:
16
16
  - Start timestamp (from `Started:` field)
17
17
  - Worktree-creation timestamp (from `## Worktree` — the time Phase 0/Pre-flight created the worktree; the strategy-independent cycle-time anchor, see step 2)
18
18
  - Card list and total count
@@ -49,7 +49,7 @@ has no blocking consequence the honest label is "monitoring signal".
49
49
  4. Copy the batch tracker to archive:
50
50
 
51
51
  ```bash
52
- cp /tmp/batch-tracker-<FIRST-CARD-ID>.md "$METRICS/archive/"
52
+ cp "$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md" "$METRICS/archive/"
53
53
  ```
54
54
 
55
55
  5. Note in tracker: `## Metrics Log: WRITTEN (run_id: batch-<FIRST-CARD-ID>)`
@@ -316,14 +316,27 @@ returns when the batch is done. It returns:
316
316
  `/new` on the hard-case follow-ups (vs leaving them tracked). In autonomous mode it is
317
317
  `{ eligible:N, offered:false, ran_new:false }`.
318
318
  Do NOT re-summarise the cards — the workflow already did.
319
- 6. **Process hygiene — reap orphaned Codex MCP servers (NON-BLOCKING).** The batch's per-card Codex
320
- finder calls drive `codex app-server`, whose broker spawns the `~/.codex/config.toml` MCP servers
321
- (Playwright, …) as children; when a broker dies they leak to init (ppid 1) and keep burning CPU.
322
- Sweep them in the main context (the workflow sandbox cannot run Bash, so this MUST run here, after
323
- the workflow returns) so the run ends clean:
319
+ 6. **Process hygiene — end this batch's Codex broker, then sweep stragglers (NON-BLOCKING, two lines).**
320
+ The batch's per-card Codex finder calls drive a `codex app-server` broker (one per worktree cwd,
321
+ shared across the batch), whose broker spawns the `~/.codex/config.toml` MCP servers (Playwright, )
322
+ as children; the Codex plugin only ends that broker on Claude Code `SessionEnd`, so without this it
323
+ and its MCP children keep burning CPU for the rest of the session. The workflow sandbox cannot run
324
+ Bash, so BOTH lines MUST run here in the main context, after the workflow returns:
324
325
  ```bash
326
+ # 1. Proactive, cwd-scoped teardown FIRST — end the broker bound to THIS batch's worktree.
327
+ # `worktreePath` is the workflow return's worktreePath (preflight); the broker matches by --cwd /
328
+ # state-hash, so this works even though the workflow already merged + removed the worktree dir.
329
+ # Capture stderr (NOT 2>/dev/null) — a CLI older than the framework lacks this subcommand and would
330
+ # silently no-op (the FEAT-0042 leak); detect it, warn, and kill the broker by cwd as fallback.
331
+ WT="<worktreePath from the workflow result>"
332
+ TD="$(npx baldart teardown-codex-broker --cwd "$WT" 2>&1)"
333
+ printf '%s' "$TD" | grep -qi "unknown command" \
334
+ && { echo "AUTO: codex-teardown CLI unavailable — run npm i -g baldart@latest; killing broker by cwd"; pkill -f "app-server-broker\.mjs.*--cwd $WT" 2>/dev/null || true; } \
335
+ || printf '%s\n' "$TD"
336
+ # 2. Then the cumulative orphan sweep (second line) — mop up any MCP already orphaned to init.
325
337
  npx baldart reap-orphans 2>/dev/null || true
326
338
  ```
327
- Reaps ONLY orphaned MCP servers (ppid 1 broker dead); NEVER kills a live `codex app-server`
328
- broker. Non-blocking any error / "nothing to reap" is fine; fold its one-line summary into the
329
- record (`codex_mcp_reaped`).
339
+ The teardown is **scoped strictly by cwd** it NEVER touches a broker serving another worktree or the
340
+ user's interactive Codex session (a different cwd). `reap-orphans` reaps ONLY orphaned MCP servers
341
+ (ppid 1 ⇒ broker dead) and NEVER kills a live broker. Non-blocking — any error / "nothing to do" is
342
+ fine; fold both one-line summaries into the record (`codex_broker_teardown`, `codex_mcp_reaped`).
@@ -183,6 +183,16 @@ Database-specific (apply the subset matching `stack.database`):
183
183
 
184
184
  Card structure:
185
185
  - files_likely_touched missing entries or conflicting across cards
186
+ - **per-AC seam-file closure**: for EACH acceptance_criteria entry, the render/seam file it
187
+ mechanically touches MUST be in files_likely_touched — an AC that adds a required prop, changes a
188
+ shared component's public signature, or wires a value into a page but does NOT list that page /
189
+ render-site / view-model file is a HIGH gap (it forces a MAY-EDIT stall or an unplanned fix-coder
190
+ in /new, and lets a coder dodge the AC by making the prop optional). Also flag any file named in a
191
+ card's requirements / acceptance_criteria / integration_points that is absent from
192
+ files_likely_touched (the ⊆ coverage rule).
193
+ - **shared multi-mode function consumers**: if a card changes a shared list/query/selector/filter fn
194
+ used in >1 mode/call-site, every consumer call-site must be owned by some card's
195
+ files_likely_touched — flag any unowned consumer (the "hidden-still-shown-in-the-other-mode" class).
186
196
  - areas field incomplete
187
197
  - git_strategy set to TBD
188
198
  - acceptance_criteria not binary testable
@@ -764,6 +764,18 @@ Instead, run the ISA scan and present findings for validation.
764
764
  utilities, API route indexes
765
765
  - SHARED STATE: stores, contexts, providers that manage related data,
766
766
  session/cache keys
767
+ - SHARED MULTI-MODE FUNCTIONS (consumer enumeration): when this feature changes
768
+ the behavior of a shared list/query/selector/filter/mapper function that is
769
+ called from MORE THAN ONE site or in MORE THAN ONE mode (e.g. a `listProducts`
770
+ used both in a "picker" mode AND a "catalog" mode), enumerate EVERY call-site /
771
+ consumer of that function as its OWN touchpoint — not just the definition site
772
+ or the most obvious caller. A filter/flag added at the definition that one
773
+ consumer relies on is NOT automatically honoured where another consumer calls
774
+ the same function in a different mode. (This is the gap that let an orders-wizard
775
+ catalog picker keep showing hidden products: the exclusion was modeled only at
776
+ the picker-mode call, while the live catalog-mode consumer page was never
777
+ enumerated → it surfaced as a HIGH only at the final review.) Grep the function
778
+ name across the codebase and list each distinct caller file:line.
767
779
 
768
780
  For each touchpoint found, report:
769
781
  - Category (from above)
@@ -152,6 +152,13 @@ Before presenting the PRD:
152
152
  - Verify canonical sources resolve through ssot-registry or valid backlog links.
153
153
  - Flag any `REGISTRY_GAP` (feature not yet in ssot-registry).
154
154
  - Remove temp/local/absolute paths from canonical sections.
155
+ - **Markdown hygiene (prevents the markdownlint debt that surfaces downstream in `/new`).** A literal
156
+ `|` inside prose or a table CELL is parsed as a column delimiter and breaks the table / trips
157
+ markdownlint (FEAT-0043: `isManagerOf||isOwner` in a cell). Escape any literal pipe in a cell as `\|`,
158
+ or wrap the expression in an inline code span (`` `isManagerOf || isOwner` ``). Also: a table needs a
159
+ delimiter row and a blank line before/after (MD058). If a `markdownlint` config is present in the repo,
160
+ run it on the generated docs before commit and fix any finding — do not ship markdown debt for `/new`'s
161
+ doc-review to catch later.
155
162
 
156
163
  ## Step 4.5 — API Performance Gate
157
164
 
@@ -33,7 +33,34 @@ Mark task 5 as `in_progress`.
33
33
  - **Consistency WARN (soft)**: a `feature`/`enhancement` card with `review_profile: light` may violate Rule C. Log `"[REVIEW-PROFILE-WARN] Card <ID> is feature/enhancement but review_profile: light — verify against Rule C in prd-card-writer.md."` **Do NOT auto-bump the field** — `review_profile` is computed by `prd-card-writer` (Rule C); see `prd-card-writer.md` for the criteria. This is a soft gate: log the warning and surface it for human review.
34
34
  - **Epics**: `review_profile` MUST be `skip` (trackers have no code work). Any other value = WARN, normalize to `skip`.
35
35
 
36
- **Gate**: every card has `owner_agent` AND `review_profile` in their enums. WARN entries from checks (b)/(c) are noted in the audit log; user may accept them after review.
36
+ d. **`requirements` presence (BLOCKING PO1)** for each CHILD/STANDALONE card whose
37
+ `acceptance_criteria` AND `scope` are both non-empty, `requirements` MUST be present and
38
+ non-empty. A freshly authored `/prd` card with derivable material but no `requirements` is an
39
+ authoring miss that shifts a measured ~3–4M-token back-fill into `/new`'s orchestrator context
40
+ (see `card-schema.md` § "Authoring completeness invariants"). `/prd` fixes it now for free.
41
+ Message: `"Card <ID> has acceptance_criteria + scope but no requirements — emit requirements
42
+ (faithful restatement of AC + scope) before commit."` (The `/new` conditional back-fill remains
43
+ only as the safety net for legacy / externally-authored cards.)
44
+
45
+ e. **files_likely_touched coverage (BLOCKING — P1/P2 closure)** — two sub-checks:
46
+ - **⊆ coverage (P2):** every file path named in a card's `requirements` /
47
+ `acceptance_criteria` / `integration_points` MUST also appear in that card's
48
+ `files_likely_touched`. An AC that adds a required prop / changes a shared component signature
49
+ / wires a value into a page but does NOT list the render-site / page / view-model file is a
50
+ BLOCKER (it forces a MAY-EDIT stall or an unplanned fix-coder in `/new`, and lets a coder dodge
51
+ the AC by making the prop optional). Message: `"Card <ID>: file <path> named in <field> is
52
+ missing from files_likely_touched."`
53
+ - **ISA-touchpoint ownership (P1 AC-descent):** every touchpoint in the state file
54
+ `## Integration Surface Map` — including every consumer enumerated for a SHARED MULTI-MODE
55
+ function (discovery dimension 9) — MUST be owned by some card's `files_likely_touched`. A PRD
56
+ surface / ISA file owned by NO card is a BLOCKER (this is the gap that otherwise surfaces only
57
+ at `/new`'s expensive final review). Message: `"ISA touchpoint <path> (<category>) is owned by
58
+ no card — assign it to a card's files_likely_touched."`
59
+ Both are SEMANTIC (path extraction from prose / ISA needs interpretation) — bias toward flagging.
60
+
61
+ **Gate**: every card has `owner_agent` AND `review_profile` in their enums, `requirements` present when
62
+ derivable (d), and full files_likely_touched coverage (e). WARN entries from checks (b)/(c) are noted in
63
+ the audit log; user may accept them after review. The (d)/(e) BLOCKERs must be fixed before commit.
37
64
 
38
65
  1. **Field Name Audit (runs after item 0 — only when item 0 passes for a given card; skipped for cards blocked in item 0)** — for each generated card:
39
66
 
@@ -100,6 +100,14 @@ Output: `{ path: string, branch: string, port: number }`
100
100
  Input: `{ worktreePath: string, checksAlreadyPassed?: boolean }`
101
101
  Output: `{ merged: boolean, developVerified: boolean, prNumber: number|null, mergeCommit: string, strategy: "pr"|"local-push" }`
102
102
 
103
+ **Invocation (via the Skill tool — the `/new` Phase 6 merge gate, and any orchestrator):**
104
+
105
+ ```
106
+ Skill({ skill: "worktree-manager", args: "/mw worktreePath=<absolute path> checksAlreadyPassed=true" })
107
+ ```
108
+
109
+ The skill name is **`worktree-manager`** (the skill that owns the `/mw` command) — callers must NOT guess `new` / `new:new` / `mw`. The `/mw` flow parses `worktreePath=` (the merge target; when absent it falls back to reading the registry to identify the active worktree) and `checksAlreadyPassed=` from the args string. Pass ONLY those two keys — the branch and the card IDs for the commit message are resolved from the worktree/registry, never from invented payload keys.
110
+
103
111
  ## Programmatic API — docs mode (for /prd orchestrator)
104
112
 
105
113
  The docs mode is the **lean** counterpart to `/nw` and `/mw`. It is intentionally
@@ -448,6 +448,25 @@ raw.forEach((f, i) => { f.finding_id = `${f.source || 'src'}#${i}:${f.finding_id
448
448
  phase('Verify')
449
449
  const classified = (await parallelCapped(raw.map((f) => () => verifyFinding(f)), MAX_PARALLEL)).filter(Boolean)
450
450
 
451
+ // O5(b) — deterministic build-claim reconciliation. A VERIFIED BLOCKER/HIGH asserting a build/compile/
452
+ // type/symbol FAILURE is self-contradicted when THIS same review's qa-sentinel build/tsc gate PASSed:
453
+ // the build is green, so the claim cannot be a real build-breaker. Auto-demote it to
454
+ // NEEDS_MANUAL_CONFIRMATION (never silently drop — it still surfaces to the human) instead of letting it
455
+ // gate the merge or spawn a fixer on a phantom. Skipped when the build gate is itself FAIL or absent.
456
+ const BUILD_CLAIM_RE = /\b(build|compil|tsc|type[-\s]?error|does\s*not\s*compile|exported\s+member|missing\s+(symbol|export|function|import)|cannot\s+find\s+(name|module))\b/i
457
+ const buildGatePass = gateTable.some((g) => /build|tsc|type|compile/i.test(g.gate) && g.status === 'PASS')
458
+ const buildGateFail = gateTable.some((g) => /build|tsc|type|compile/i.test(g.gate) && g.status === 'FAIL')
459
+ if (buildGatePass && !buildGateFail) {
460
+ for (const f of classified) {
461
+ if (f.classification === 'VERIFIED' && (f.severity === 'BLOCKER' || f.severity === 'HIGH') &&
462
+ BUILD_CLAIM_RE.test(`${f.title || ''} ${f.evidence || ''} ${f.minimal_fix_direction || ''}`)) {
463
+ f.classification = 'NEEDS_MANUAL_CONFIRMATION'
464
+ f.reconciliation = 'auto-demoted: build/compile failure claim contradicted by a green build/tsc gate'
465
+ log(`Final review: auto-demoted [${f.finding_id}] ${f.severity} (build-failure claim) → NEEDS_MANUAL_CONFIRMATION — the qa build/tsc gate PASSed, contradicting it.`)
466
+ }
467
+ }
468
+ }
469
+
451
470
  // Route a finding to the specialist that OWNS its domain (never a generic code-reviewer for
452
471
  // doc/api/security). Kept in sync with new2-resolve.js normDomain() routing buckets.
453
472
  function domainVerifier(domain) {
@@ -460,7 +479,16 @@ function domainVerifier(domain) {
460
479
  }
461
480
 
462
481
  async function verifyFinding(f) {
463
- if (f.preValidated || (typeof f.confidence === 'number' && f.confidence >= 80)) {
482
+ // O5(a) out-of-lane preValidated guard. A NARROW specialist (doc-reviewer / api-perf-cost-auditor)
483
+ // FP-checks ONLY its own lane in the finding pass; its `preValidated` (and self-reported confidence) do
484
+ // NOT authoritatively cover a finding it emitted OUTSIDE that lane (e.g. the perf auditor asserting a
485
+ // `domain: code` build/symbol failure). When the source is such a specialist and the finding's domain
486
+ // routes to a DIFFERENT verifier, strip the short-circuit so the correct domain specialist validates it.
487
+ // 'codex' / 'code-reviewer' are broad code reviewers (lane = all code domains) → unchanged. This is what
488
+ // let a phantom perf "BLOCKER" (a symbol it claimed missing, which existed) reach the skill un-verified.
489
+ const NARROW_SPECIALISTS = new Set(['doc-reviewer', 'api-perf-cost-auditor'])
490
+ const outOfLane = NARROW_SPECIALISTS.has(f.source) && domainVerifier(f.domain) !== f.source
491
+ if (!outOfLane && (f.preValidated || (typeof f.confidence === 'number' && f.confidence >= 80))) {
464
492
  return { ...f, classification: 'VERIFIED' }
465
493
  }
466
494
  const verifier = domainVerifier(f.domain)
@@ -630,6 +658,13 @@ if (applyFixes && editableFiles.length) {
630
658
  log(`Final Fix phase done: ${fixesApplied.length} fixed, ${docFixesApplied.length} doc-applied, ${residual.length} residual${checksFailed ? ', post-fix checks FAILED' : ''}.`)
631
659
  }
632
660
 
661
+ // B2 — terminal completion sentinel. The skill's F.1.5 "workflow-killed recovery" branch treats the
662
+ // ABSENCE of this line (with the run journal's terminal status ≠ completed) as "killed mid-flight".
663
+ // Emitting it ONLY here — after every phase finished and the additive fields are assembled — makes it an
664
+ // unambiguous clean-finish marker. NOTE: when applyFixes, the fixes are applied to the WORKTREE but NOT
665
+ // committed (the orchestrator commits in F.5), so a run killed before this line leaves them as
666
+ // uncommitted worktree diffs that the recovery branch re-validates + commits off-context.
667
+ log(`NEW-FINAL-REVIEW COMPLETE — engine=${codexEngine}, findings=${findings.length}, ${applyFixes ? `fixes=${fixesApplied.length}+${docFixesApplied.length} (UNCOMMITTED in worktree), residual=${residual.length}` : 'review-only'}, gates=${gateTable.map((g) => g.gate + ':' + g.status).join(',') || 'none'}.`)
633
668
  const base = { codexEngine, findings, noActionFindings, gateTable, summary }
634
669
  // Additive ONLY when the Fix phase ran — otherwise byte-identical to today's shape (new2 untouched).
635
670
  return applyFixes ? { ...base, fixesApplied, docFixesApplied, residual } : base
@@ -102,6 +102,31 @@ blocks where intent is genuinely missing:
102
102
  - **WARN (expected but tolerable absent):** `canonical_docs` / `links.prd` on a STANDALONE /
103
103
  non-PRD card. Log, do not block.
104
104
 
105
+ ## Authoring completeness invariants (write-side — keep `/new` from paying for it)
106
+
107
+ These bind the card AUTHOR (prd-card-writer) so a card never reaches `/new` with a gap `/new`
108
+ must then patch in its expensive orchestrator context. Two recurring leaks, each with a measured
109
+ downstream cost in a real batch:
110
+
111
+ - **Per-AC seam-file closure (P2).** Every `acceptance_criteria` entry must have, in
112
+ `files_likely_touched`, the **render/seam file it mechanically touches** — not only the
113
+ definition site. An AC that **adds a required prop, changes a shared component's public
114
+ signature, or wires a new value into a page** MUST list that component's **render-site / page
115
+ file** (e.g. an AC "Step-2 row shows a `notOrderable` flag" must list the `page.tsx` /
116
+ view-model that builds the row, not just the data fn). A coder may NOT satisfy such an AC by
117
+ making the new prop **optional** to dodge an unlisted file — so if the file is missing the card
118
+ is under-specified. The MAY-EDIT list `/new` derives from `files_likely_touched` is only as
119
+ complete as this closure; a missing seam file caused MAY-EDIT stalls + unplanned fix-coders.
120
+ **Coverage rule:** every file named in a card's `requirements` / `acceptance_criteria` /
121
+ `integration_points` MUST also appear in `files_likely_touched` (⊆ check).
122
+ - **`requirements` emitted at write time (PO1).** For a CHILD / STANDALONE card whose
123
+ `acceptance_criteria` AND `scope` are present, **emit `requirements` at authoring time** (the
124
+ faithful restatement/decomposition of AC + scope — same synthesis the `/new` BACK-FILL would do).
125
+ Do NOT ship the field absent and rely on the `/new` conditional back-fill above: that back-fill
126
+ runs in the orchestrator's accumulated context (a measured ~3–4M-token in-context tax per batch
127
+ when most cards omit it). The back-fill stays as the safety net for legacy cards; a freshly
128
+ authored `/prd` card must not trigger it.
129
+
105
130
  A back-fill never rescues a card missing a HALT field: a card without `scope` blocks regardless
106
131
  of how many derivable fields were filled.
107
132
 
@@ -80,6 +80,16 @@ Before picking a tier, classify the query:
80
80
  Repeated grep on a large barrel burns the turn budget and risks the agent
81
81
  returning *before* it synthesizes its report.
82
82
  - **Git**: no hard budget.
83
+ - **Tool default — prefer `rg` over GNU `grep` for source searches.** `rg` searches
84
+ NUL-containing files as text by default; GNU `grep` silently switches to **binary
85
+ mode** on the first NUL byte (common in large generated / bundled / data files) and
86
+ then prints nothing and exits 1 — *indistinguishable from a genuine no-match*. So:
87
+ treat an empty `grep` result + exit 1 **on a file you EXPECT to match** as a
88
+ binary-mode trap, not "symbol absent" — re-run with `rg` (or `grep -a` to force
89
+ text mode) before concluding anything, and never spawn a fix to "add a missing
90
+ symbol" off a bare GNU-grep miss on a large file. (A measured `/new` run burned
91
+ ~8–12M tokens with three coders independently re-discovering one NUL byte in a
92
+ single multi-hundred-KB `data.ts`.)
83
93
 
84
94
  ## Fallback rules
85
95
 
@@ -135,6 +135,15 @@ Three enforcement layers (mirrors the design-system discipline): **per-task**
135
135
  context/orphans on new keys) → **weekly** (`i18n-align` routine audits + aligns
136
136
  target languages). The on-demand `/i18n audit` covers off-cadence checks.
137
137
 
138
+ **Per-card translation fill in `/new` (resolves the source-only ↔ parity-gate tension).** The
139
+ `coder` writes the SOURCE locale only (it never translates). But a project may run its OWN
140
+ **locale-parity test** as part of its `test`/`build` gate, which fails on a source-only commit. These
141
+ are not in conflict: in a `/new` run, when `features.has_i18n` AND a card's diff adds new source-locale
142
+ keys, the orchestrator runs a **planned `i18n-translator` fill pass after the coder and before the
143
+ parity gate** (`new/references/implement.md` Phase 2 step 9) — so parity holds first-try, the coder
144
+ still never translates, and the weekly `i18n-align` remains the steady-state aligner. A parity failure
145
+ is therefore an `i18n-translator` task, NEVER a `coder` retry.
146
+
138
147
  ## Ownership (strict specialization)
139
148
 
140
149
  - **`coder`** — externalizes strings + writes the registry STUB (`source`+`context`+`domain`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.61.1",
3
+ "version": "4.63.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"
@@ -39,6 +39,7 @@ const ToolCurrency = require('../utils/tool-currency');
39
39
  const CodexOrphans = require('../utils/codex-orphans');
40
40
  const UpdateNotifier = require('../utils/update-notifier');
41
41
  const cliPackageJson = require('../../package.json');
42
+ const { cmpVersions } = require('../utils/semver-lite');
42
43
 
43
44
  const FRAMEWORK_DIR = '.framework';
44
45
  const CONFIG_FILE = 'baldart.config.yml';
@@ -238,6 +239,20 @@ async function detectState(cwd, opts = {}) {
238
239
  };
239
240
  }
240
241
 
242
+ // CLI ↔ framework version skew (since v4.63.0). The framework PROSE ships via the git
243
+ // subtree (tracks repo HEAD, always current), but `npx baldart` resolves the SEPARATELY-
244
+ // installed CLI. When that CLI is OLDER than the framework, any prose calling a NEW
245
+ // `npx baldart` subcommand (e.g. `teardown-codex-broker`, added v4.62.0) silently fails as
246
+ // `unknown command` — a real, invisible regression (FEAT-0042: leaked Codex brokers/MCP
247
+ // every run). Surface it so the user updates the global CLI.
248
+ state.cliFrameworkSkew = null;
249
+ try {
250
+ if (state.frameworkVersion && cliPackageJson.version &&
251
+ cmpVersions(cliPackageJson.version, state.frameworkVersion) < 0) {
252
+ state.cliFrameworkSkew = { cli: cliPackageJson.version, framework: state.frameworkVersion };
253
+ }
254
+ } catch (_) { /* version parse failure → no skew claim */ }
255
+
241
256
  state.remote = await describeRemote(git, ledger.framework_repo || State.DEFAULT_REPO, !!opts.offline);
242
257
  state.commitsAhead = await commitsAheadOfRemote(git, state.remote);
243
258
  state.workingTreeDirty = await localFrameworkChanges(git);
@@ -1140,6 +1155,14 @@ function renderDiagnostic(state) {
1140
1155
  state.cliUpdate ? 'warn' : 'ok'
1141
1156
  ));
1142
1157
 
1158
+ if (state.cliFrameworkSkew) {
1159
+ console.log(statusLine(
1160
+ 'CLI↔framework',
1161
+ `CLI v${state.cliFrameworkSkew.cli} < framework v${state.cliFrameworkSkew.framework} — new \`npx baldart\` subcommands (e.g. teardown-codex-broker) silently fail. Run: npm i -g baldart@latest`,
1162
+ 'warn'
1163
+ ));
1164
+ }
1165
+
1143
1166
  console.log(statusLine('Git repo', state.isGitRepo ? 'yes' : 'no', state.isGitRepo ? 'ok' : 'err'));
1144
1167
 
1145
1168
  if (!state.isGitRepo) return;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `baldart teardown-codex-broker --cwd <path>` — proactively end the Codex
3
+ * app-server broker bound to a SPECIFIC workspace cwd (since v4.62.0).
4
+ *
5
+ * The reactive companion `baldart reap-orphans` only mops up MCP servers AFTER
6
+ * their broker has already died (ppid 1). This command is the PROACTIVE, cwd-
7
+ * scoped teardown: at the END of a `/new` batch (or an abort), end the broker
8
+ * the batch's worktree created so its Playwright/Figma/… MCP children stop
9
+ * burning CPU for the rest of the session — instead of waiting for the plugin's
10
+ * SessionEnd hook (which may be hours away) or the next reap sweep.
11
+ *
12
+ * SCOPE & SAFETY: scoped strictly by `--cwd`. It ends ONLY the broker whose
13
+ * `--cwd` matches; a broker serving another worktree, or the user's interactive
14
+ * Codex session on the main repo (a DIFFERENT cwd), is never touched. The SSOT
15
+ * for the teardown logic is `src/utils/codex-orphans.js` (`tearDownCodexBroker`);
16
+ * this command only frames it. Always exits 0 — hygiene, never a blocker.
17
+ */
18
+
19
+ const UI = require('../utils/ui');
20
+ const CodexOrphans = require('../utils/codex-orphans');
21
+
22
+ async function teardownCodexBroker(opts = {}) {
23
+ const json = !!opts.json;
24
+ const cwd = opts.cwd || process.cwd();
25
+
26
+ const result = {
27
+ schema: 'baldart.teardown-codex-broker/1',
28
+ cwd,
29
+ method: 'none',
30
+ brokerPid: null,
31
+ killed: 0,
32
+ failed: 0,
33
+ failures: [],
34
+ };
35
+
36
+ try {
37
+ const r = await CodexOrphans.tearDownCodexBroker({ cwd });
38
+ result.method = r.method;
39
+ result.brokerPid = r.brokerPid;
40
+ result.killed = (r.killed || []).length;
41
+ result.failed = (r.failed || []).length;
42
+ result.failures = r.failed || [];
43
+ if (r.error) result.error = r.error;
44
+ } catch (err) {
45
+ result.error = (err && err.message) || String(err);
46
+ }
47
+
48
+ if (json) {
49
+ process.stdout.write(JSON.stringify(result) + '\n');
50
+ return result;
51
+ }
52
+
53
+ if (result.error) {
54
+ UI.warning(`Codex broker teardown skipped (error: ${result.error}).`);
55
+ } else if (result.method === 'none') {
56
+ UI.success('Codex broker teardown: no broker bound to this cwd — nothing to do.');
57
+ } else if (result.method === 'graceful') {
58
+ UI.success(`Codex broker teardown: gracefully shut down the broker for this cwd (pid ${result.brokerPid ?? 'n/a'}) + its MCP children.`);
59
+ } else {
60
+ UI.success(`Codex broker teardown: signalled the broker subtree for this cwd (broker pid ${result.brokerPid ?? 'n/a'}, killed ${result.killed} process(es)).`);
61
+ if (result.failed > 0) {
62
+ UI.warning(`${result.failed} could not be killed:`);
63
+ result.failures.forEach((f) => console.log(` pid ${f.pid}: ${f.error}`));
64
+ }
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ module.exports = teardownCodexBroker;
@@ -40,6 +40,9 @@
40
40
  */
41
41
 
42
42
  const { execSync } = require('child_process');
43
+ const path = require('path');
44
+ const fs = require('fs');
45
+ const os = require('os');
43
46
 
44
47
  // Command signatures that identify an MCP server. When such a process is
45
48
  // orphaned (ppid 1) it is safe to reap (its stdio parent is gone).
@@ -172,11 +175,175 @@ function reapOrphans(orphans = [], procs = listProcesses()) {
172
175
  return { killed, failed };
173
176
  }
174
177
 
178
+ // ─────────────────────────────────────────────────────────────────────────────
179
+ // PROACTIVE, cwd-SCOPED broker teardown (since v4.62.0)
180
+ // ─────────────────────────────────────────────────────────────────────────────
181
+ //
182
+ // WHY (distinct from reapOrphans above)
183
+ // -------------------------------------
184
+ // reapOrphans is REACTIVE: it can only mop up MCP servers AFTER their broker has
185
+ // already died and the OS reparented them to init (ppid 1). It deliberately never
186
+ // ends a live broker (a detached broker at ppid 1 is indistinguishable from a
187
+ // leaked one by ppid alone).
188
+ //
189
+ // But the Codex plugin ALREADY ships a clean per-cwd broker teardown — it just
190
+ // only fires on Claude Code `SessionEnd` (session-lifecycle-hook.mjs). A `/new`
191
+ // run that ends — or is KILLED mid-final-review — long before the Claude session
192
+ // ends therefore leaves its broker (and the Playwright/Figma/… MCP grandchildren)
193
+ // alive for the rest of the session, burning CPU. `tearDownCodexBroker` closes
194
+ // that gap: at /new run-end / abort, end the broker bound to THAT worktree's cwd.
195
+ //
196
+ // SAFETY: scoped strictly by cwd. The broker is keyed per workspace-root, so a
197
+ // worktree's broker has no legitimate concurrent user — but a broker for a
198
+ // DIFFERENT cwd (another worktree, or the user's interactive session on the main
199
+ // repo) is NEVER matched. We never blanket-kill brokers.
200
+
201
+ // Broker-process command signatures (the spawned app-server broker + its server).
202
+ const CODEX_BROKER_SIGNATURES = [
203
+ /app-server-broker\.mjs/,
204
+ /codex\s+app-server/,
205
+ ];
206
+
207
+ // Best-effort locate the installed Codex plugin `scripts/` dir (for Tier 1).
208
+ function resolveCodexPluginScriptsDir() {
209
+ const home = process.env.HOME || os.homedir();
210
+ const base = path.join(home, '.claude', 'plugins');
211
+ const direct = [
212
+ path.join(base, 'marketplaces', 'openai-codex', 'plugins', 'codex', 'scripts'),
213
+ ];
214
+ for (const c of direct) {
215
+ try { if (fs.existsSync(path.join(c, 'lib', 'broker-lifecycle.mjs'))) return c; } catch (_) { /* ignore */ }
216
+ }
217
+ // Shallow scan: <plugins>/{marketplaces,cache}/<marketplace>/(plugins/)?codex/scripts
218
+ for (const sub of ['marketplaces', 'cache']) {
219
+ const d = path.join(base, sub);
220
+ let entries;
221
+ try { entries = fs.readdirSync(d); } catch (_) { continue; }
222
+ for (const mk of entries) {
223
+ for (const cand of [
224
+ path.join(d, mk, 'plugins', 'codex', 'scripts'),
225
+ path.join(d, mk, 'codex', 'scripts'),
226
+ ]) {
227
+ try { if (fs.existsSync(path.join(cand, 'lib', 'broker-lifecycle.mjs'))) return cand; } catch (_) { /* ignore */ }
228
+ }
229
+ }
230
+ }
231
+ return null;
232
+ }
233
+
234
+ // Extract the `--cwd <value>` token from a broker ps command line.
235
+ function brokerCwdOf(command) {
236
+ const m = String(command).match(/--cwd\s+(\S+)/);
237
+ return m ? m[1] : null;
238
+ }
239
+
240
+ // Canonicalize a path for comparison (resolve symlinks where possible).
241
+ function canonical(p) {
242
+ if (!p) return null;
243
+ try { return fs.realpathSync(p); } catch (_) { return path.resolve(p).replace(/\/+$/, ''); }
244
+ }
245
+
246
+ /**
247
+ * Proactively tear down the Codex app-server broker bound to a SPECIFIC `cwd`
248
+ * (typically a /new batch worktree), freeing its MCP-server children. Scoped by
249
+ * cwd — never touches a broker serving another worktree or the user's
250
+ * interactive session on the main repo. Never throws (hygiene, not a blocker).
251
+ *
252
+ * Tier 1 (graceful) — reuse the Codex plugin's OWN lib: loadBrokerSession(cwd) →
253
+ * sendBrokerShutdown(endpoint) (the broker closes its app-client + MCP children)
254
+ * → teardownBrokerSession(...) (unlink socket/pid/log + SIGTERM the broker pid)
255
+ * → clearBrokerSession(cwd). This is exactly the plugin's own stale-broker path.
256
+ * Tier 2 (fallback) — when the plugin lib is absent/unimportable OR there is no
257
+ * broker.json for this cwd but a broker process still lingers: ps-match the
258
+ * broker whose command carries `--cwd <cwd>` + a codex-app-server signature,
259
+ * SIGTERM its process group, then SIGKILL survivors in its subtree (reusing
260
+ * listProcesses/collectTree).
261
+ *
262
+ * @param {{ cwd: string, pluginScriptsDir?: string }} opts
263
+ * @returns {Promise<{ method:'graceful'|'signal'|'none', brokerPid:number|null,
264
+ * killed:number[], failed:Array<{pid:number,error:string}>, error?:string }>}
265
+ */
266
+ async function tearDownCodexBroker({ cwd, pluginScriptsDir } = {}) {
267
+ const none = { method: 'none', brokerPid: null, killed: [], failed: [] };
268
+ if (!cwd || process.platform === 'win32') return none;
269
+
270
+ // ---- Tier 1: graceful, via the plugin's own broker-lifecycle lib -----------
271
+ try {
272
+ const dir = pluginScriptsDir || resolveCodexPluginScriptsDir();
273
+ if (dir) {
274
+ const libPath = path.join(dir, 'lib', 'broker-lifecycle.mjs');
275
+ if (fs.existsSync(libPath)) {
276
+ const lib = await import(require('url').pathToFileURL(libPath).href);
277
+ if (typeof lib.loadBrokerSession === 'function') {
278
+ const session = lib.loadBrokerSession(cwd);
279
+ if (session && (session.endpoint || session.pid)) {
280
+ if (session.endpoint && typeof lib.sendBrokerShutdown === 'function') {
281
+ try { await lib.sendBrokerShutdown(session.endpoint); } catch (_) { /* ignore */ }
282
+ }
283
+ if (typeof lib.teardownBrokerSession === 'function') {
284
+ try {
285
+ lib.teardownBrokerSession({
286
+ endpoint: session.endpoint ?? null,
287
+ pidFile: session.pidFile ?? null,
288
+ logFile: session.logFile ?? null,
289
+ sessionDir: session.sessionDir ?? null,
290
+ pid: session.pid ?? null,
291
+ killProcess: (p) => { try { process.kill(p, 'SIGTERM'); } catch (_) { /* ignore */ } },
292
+ });
293
+ } catch (_) { /* ignore */ }
294
+ }
295
+ if (typeof lib.clearBrokerSession === 'function') {
296
+ try { lib.clearBrokerSession(cwd); } catch (_) { /* ignore */ }
297
+ }
298
+ return { method: 'graceful', brokerPid: session.pid ?? null, killed: session.pid ? [session.pid] : [], failed: [] };
299
+ }
300
+ }
301
+ }
302
+ }
303
+ } catch (_) {
304
+ // Tier 1 unavailable (plugin moved, import failed, …) → fall through to Tier 2.
305
+ }
306
+
307
+ // ---- Tier 2: signal the cwd-matched broker subtree -------------------------
308
+ try {
309
+ const target = canonical(cwd);
310
+ const procs = listProcesses();
311
+ const brokers = procs.filter((p) =>
312
+ matchesAny(CODEX_BROKER_SIGNATURES, p.command) && canonical(brokerCwdOf(p.command)) === target);
313
+ if (!brokers.length) return none;
314
+ const self = process.pid;
315
+ const killed = [];
316
+ const failed = [];
317
+ for (const b of brokers) {
318
+ // SIGTERM the broker's process group first (a detached broker is its own
319
+ // group leader) so it can run its shutdown handler; fall back to the bare pid.
320
+ try { process.kill(-b.pid, 'SIGTERM'); } catch (_) {
321
+ try { process.kill(b.pid, 'SIGTERM'); } catch (_) { /* ignore */ }
322
+ }
323
+ // Then SIGKILL any survivors in its subtree (force path — Tier 1 already failed/absent).
324
+ for (const pid of collectTree(b.pid, procs)) {
325
+ if (pid === self || !Number.isInteger(pid) || pid <= 1) continue;
326
+ try { process.kill(pid, 'SIGKILL'); killed.push(pid); }
327
+ catch (err) {
328
+ if (err && err.code === 'ESRCH') killed.push(pid);
329
+ else failed.push({ pid, error: (err && err.message) || String(err) });
330
+ }
331
+ }
332
+ }
333
+ return { method: 'signal', brokerPid: brokers[0].pid, killed, failed };
334
+ } catch (err) {
335
+ return { method: 'none', brokerPid: null, killed: [], failed: [], error: (err && err.message) || String(err) };
336
+ }
337
+ }
338
+
175
339
  module.exports = {
176
340
  MCP_SIGNATURES,
177
341
  CODEX_RUNTIME_SIGNATURES,
342
+ CODEX_BROKER_SIGNATURES,
178
343
  listProcesses,
179
344
  detectOrphans,
180
345
  collectTree,
181
346
  reapOrphans,
347
+ tearDownCodexBroker,
348
+ resolveCodexPluginScriptsDir,
182
349
  };