baldart 4.10.0 → 4.12.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,46 @@ 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.12.0] - 2026-06-04
9
+
10
+ **`/new` non gonfia più il context dell'orchestrator su long-run — il bulk content (log dei gate, `git diff`, file interi, baseline) non scorre più nel canale inline dell'orchestrator: vive su disco e viaggia per *path*.** Sintomo riportato dall'utente: su batch lunghi il consumo si impennava per il context bloat. Una prima ipotesi (i *finding* delle review che tornano inline + un card-runner sub-agent isolato + un "context purge" da rinforzare) è stata **sottoposta a review avversariale e refutata**: (1) l'orchestrator DEVE leggere il contenuto dei finding ai punti-decisione (domain-override partition, AC-Closure, `NEEDS_MANUAL_CONFIRMATION`, merge/dedup del Final gate) → un contratto "verdict-only" che li nasconde romperebbe la skill e i consumer condivisi (`/prd`, `/codexreview`); (2) un card-runner sub-agent non può ospitare i gate utente né garantire il nested-spawn dei reviewer; (3) un "compaction-lossless checkpoint" non regge (`/tmp` è single-point-of-failure, l'auto-compact non è controllabile da una skill). La verifica sul codice ha trovato che **il vero driver del bloat è il bulk content reso inline nei tool-result dell'orchestrator stesso**, non i finding: output dei gate `lint`/`tsc`/`test`/`build` (resi inline da `| tee`, ~40-50%), `git diff` completo passato inline ai simplify agent (~25-30%), Read di file interi nel completeness check, e baseline concatenati nel Final F.3. **MINOR** (cambia il comportamento di esecuzione di `/new`; nessuna nuova capability, nessun nuovo agente, nessuna chiave `baldart.config.yml`, nessun contratto di return degli agent toccato — la schema-propagation rule non si applica).
11
+
12
+ > **Why.** L'orchestrator di `/new` è un unico contesto long-lived su tutto il batch: ogni byte che rende inline (Bash tool-result, prompt che compone, file che Read-a) resta in finestra per l'intero run, e nessun "purge" via prompt recupera token. La fix attacca la radice — **tenere il bulk fuori dal canale inline** — senza toccare ciò che l'orchestrator può leggere ai punti-decisione (i finding restano file-resident e leggibili on-demand). Onestà sui limiti: questo riduce il *rate di accumulo* (stima ~40-50% sul footprint dominante), non azzera la crescita del context; il bound vero richiederebbe isolamento per-card a livello di harness, fuori dalla portata di una skill.
13
+
14
+ ### Changed
15
+
16
+ - **`framework/.claude/skills/new/SKILL.md`** — nuova sezione SSOT **`## Context economy (MANDATORY)`** (HARD RULE in 5 punti: gate-output discipline, diff-to-disk, findings file-resident, pass-paths-not-concatenations, targeted Reads). Applicata ai siti che la violavano:
17
+ - **Gate bash (Phase 2 step 8)** — `cmd 2>&1 | tee /tmp/x.txt` → `cmd > /tmp/x.txt 2>&1; echo "<gate>:$?"`: l'output completo resta su disco, inline solo l'exit code; su fallimento si legge un estratto *bounded* (`tail -n 30`), mai il log intero. Step 9 passa al fix agent il **path** del log, non il blob.
18
+ - **Tutti i re-run di gate foreground** (gap-fix Phase 2.5, Phase 2.55 step 5, Phase 3 step 16, pre-commit Phase 4, Final F.5, team D.1/D.3b) — pointer alla disciplina (redirect a disco, mai inline).
19
+ - **`git diff` (Phase 2.55 step 2-3)** — `git diff > /tmp/diff-<CARD>.txt`; i 3 simplify agent ricevono il **path** e leggono il diff da soli, niente «full diff» nei prompt. Team D.3b allineato.
20
+ - **Blocchi Codex (Phase 0 cross-card audit, Final F.3)** — `2>&1 | tee "$FILE"` → `> "$FILE" 2>&1` (niente copia inline; il file si riempie comunque in streaming per il polling F.4, commento aggiornato).
21
+ - **Final F.3 baseline** — `${ARCH_BASELINE}` (contenuti concatenati) → `${ARCH_BASELINE_PATHS}` (lista di path che Codex Read-a on-demand); F.2 non concatena più i per-card baseline.
22
+ - **Completeness check (Phase 2.5 step 4)** — Read *mirati* (grep con `offset`/`limit` stretti attorno al `file:line` del completion report) invece di Read di file interi.
23
+ - **Progress Bar** — blocco markdown completo solo alle boundary pesanti (card/wave/gate-decision/STOP); il movimento di fase intra-card si mostra via Task spine sub-state, non ri-emettendo la tabella a ogni fase.
24
+ - **Phase 5 step 31** — il "CONTEXT PURGE" è ri-etichettato come *reference-hygiene* (non token-reclamation, che un prompt non può ottenere); il leverage reale è upstream (la § Context economy tiene il bulk fuori dal context fin dall'inizio).
25
+
26
+ ## [4.11.0] - 2026-06-04
27
+
28
+ **Gli overlay non costringono più a riconciliare a ogni `baldart update` — la VERSION non è più incisa nell'identità dei file generati né usata come segnale di drift quando il base file non è cambiato.** Sintomo riportato dall'utente: ogni update richiedeva una riconciliazione. La verifica del sorgente ha trovato **due trigger indipendenti con la stessa radice** — la VERSION del framework veniva trattata come parte dell'identità di overlay/file-generati, ma viene bumpata a *ogni* release a prescindere dal fatto che lo specifico base file sia cambiato. **(A) churn dei file agent/command generati:** `buildMarker()` stampava `base_version=<VERSION>` nel commento `<!-- baldart-generated -->`; a ogni update la VERSION cambiava → la riga del marker cambiava anche su contenuto byte-identico → git vedeva un diff → scattava il commit `post-update reconcile` *a ogni update*. Il campo era puramente informativo (`readMarker` lo leggeva ma nessuno gate-ava la rigenerazione su di esso). **(B) falso-drift degli skill overlay:** `doctor` e `status` confrontavano il pin `base_skill_version` *direttamente* con la VERSION installata (ignorando `base_file_sha`), così ogni bump faceva apparire ogni overlay come "drifted" anche con base SKILL.md identico — mentre `overlay drift` lo faceva già correttamente SHA-first. **MINOR** (cambia il formato del marker generato; nessuna chiave `baldart.config.yml` — la fix riusa `base_file_sha` già presente).
29
+
30
+ > **Why.** Il drift deve segnalare *modifiche reali di contenuto*, non bump di versione. La radice comune era usare la VERSION come proxy di "il base è cambiato": un proxy sempre falso-positivo dato che VERSION sale a ogni release. La fix rende ogni layer **content-addressed** (SHA) e relega la VERSION a metadato informativo: (A) il marker porta solo `base_sha` + `overlay_sha` — entrambi cambiano *solo* su modifica reale → i file generati tornano byte-stabili tra update di sola-versione (il commit di reconcile scatta solo quando base o overlay cambiano davvero); (B) `doctor`/`status` diventano SHA-first come `overlay drift`, col confronto di versione come fallback solo quando manca `base_file_sha`. In più il pin `base_<kind>_version` viene rinfrescato al merge **solo quando lo SHA combacia** (base invariato) — così resta veritiero senza mai mascherare drift reale: se il base è cambiato lo SHA differisce e il pin stale resta un segnale legittimo da riconciliare. Retrocompatibile in lettura: `readMarker` tollera ancora i marker legacy con `base_version=` (i consumer pre-4.11 subiscono un diff one-shot al primo update, poi stabilità).
31
+
32
+ ### Changed
33
+
34
+ - **`src/utils/overlay-merger.js`** — `buildMarker()` non stampa più `base_version` (identità content-addressed: solo `base_sha` + `overlay_sha`); `readMarker()` rende `base_version` opzionale nella regex (legge sia il formato nuovo sia il legacy, `baseVersion: null` quando assente); call-site di `mergeOverlay` semplificato. Nuovo helper `refreshOverlayVersionPin(overlayPath, kind, frameworkVersion, currentBaseSha)` — rinfresca il pin di versione **solo** quando `base_file_sha` combacia col base corrente (no-op su mismatch → drift preservato; no-op su already-current; fail-safe su I/O). `baseVersionTargeted` ora usa `frameworkVersion` come fallback significativo invece di `'unknown'`.
35
+ - **`src/commands/doctor.js`** — `overlayDrift` SHA-first: confronta `base_file_sha` con lo SHA del base SKILL.md corrente; fallback al confronto di versione solo quando `base_file_sha` manca (o il base è illeggibile). Esclude i file `*.example.md`.
36
+ - **`src/commands/status.js`** — display drift SHA-first allineato al contratto di `overlay drift` (mostra `base changed: sha X → Y` su drift reale, `base unchanged` quando lo SHA combacia; fallback di versione solo senza `base_file_sha`).
37
+ - **`src/utils/symlinks.js`** — `_generateOverlayedFile` calcola `baseSha` una volta e, dopo il backfill di `base_file_sha`, chiama `refreshOverlayVersionPin` per tenere truthful il pin informativo quando il base è invariato.
38
+
39
+ ### Added
40
+
41
+ - **`src/utils/__tests__/overlay-merger.test.js`** — 7 test: stabilità byte-identica del merge tra bump di sola-versione (fix Trigger A), assenza di `base_version` nel marker generato, lettura back-compat del marker legacy, e i tre rami di `refreshOverlayVersionPin` (refresh su sha-match, no-op su sha-mismatch con drift preservato, no-op su already-current).
42
+
43
+ ### Fixed
44
+
45
+ - **`framework/.claude/skills/new/SKILL.md`** (Phase 3.7 Step A, card-scoped diff) — risoluzione della backlog card per nome file resa **case-insensitive** (`find -name` → `-iname`). Era l'unico punto in tutte le skill che risolve una card by-name, e `-name` è case-sensitive: i file su disco hanno prefisso maiuscolo (`FEAT-`/`BUG-`/`CHORE-`), ma se `CARD_ID` arriva con case diverso (es. `/new feat-0019`) `find` tornava **vuoto in silenzio** → `CARD_FILE=""` → la card veniva trattata come inesistente (stessa famiglia del case-collision docs/PRD noto). Stesso blocco: il placeholder `${paths.backlog_dir:-../../backlog}` non era bash valido (il punto nel nome variabile → `bad substitution` se eseguito verbatim) — ora emesso già risolto in una variabile `BACKLOG_DIR` (fallback `backlog`).
46
+ - **`framework/.claude/skills/worktree-manager/SKILL.md`** (`/nw` step 3b, sync card non-tracciate) — stesso fix di normalizzazione: il token `${paths.backlog_dir:-backlog}` letterale (bash non valido) sostituito da una variabile `BACKLOG_DIR` risolta, allineando il fallback (`backlog`) tra le due skill.
47
+
8
48
  ## [4.10.0] - 2026-06-04
9
49
 
10
50
  **`/new` ora ha una todo list persistente e una progress bar — durante un batch sai sempre quale fase gira, quale wave del team mode è in volo, e quale gate è risolto o skippato (con motivo).** Finora `/new` era un orchestratore autonomo cieco verso l'utente: l'unica traccia di stato era il tracker file interno `/tmp/batch-tracker-<FIRST-CARD-ID>.md`, che l'utente **non vede**. Risultato: in un run lungo (15 fasi per card, 50+ gate, sequential o team mode a wave) non avevi modo di capire dove fosse il processo. La fix replica il doppio meccanismo che dà a `/prd` la sua todo list amata, adattato alla struttura `wave → card → fase/gate`: (1) un **Task spine nativo** (TaskCreate/TaskUpdate) — il pannello todo sempre visibile della UI — tenuto *coarse*, un task per card etichettato per wave (`Wave 1 · FEAT-0502 — <title>`) più i framing task `Pre-flight`/`Final review`/`Merge & cleanup`; (2) una **Progress Bar markdown** emessa **alle transizioni** (cambio fase/wave/card, decisione gate, ogni `AskUserQuestion`/STOP — non a ogni messaggio, per non fare rumore nei tratti autonomi) con tabella card×wave + un **Gate ledger** che mostra ogni gate `✅ risolto` / `⏭️ skippato (+motivo)`. **MINOR** (capability aggiunta alla skill `/new`; nessuna chiave `baldart.config.yml` — usa solo i Task tool nativi e l'`execution_strategy` già esistente).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.10.0
1
+ 4.12.0
@@ -167,7 +167,7 @@ In team mode the cards of a wave run in parallel: set ALL of a wave's card tasks
167
167
 
168
168
  ### B. Progress Bar — markdown, at transition boundaries
169
169
 
170
- Append this block to your message **at every transition boundary** (NOT on every message — that would be noise across long autonomous runs). A **transition boundary** is any of: a phase change, a wave change, a card change, **any gate decision** (resolved or skipped), and every `AskUserQuestion` / STOP.
170
+ Append this block to your message **at the heavy transition boundaries only** (NOT on every message, and NOT on every intra-card phase change — that accumulates markdown across long autonomous runs; per § "Context economy"). A **heavy transition boundary** is any of: a **card change**, a **wave change**, **any gate decision** (resolved or skipped), and every `AskUserQuestion` / STOP. **Intra-card phase movement** (e.g. Phase 2 → 2.5 → 2.55 within the same card, with no gate decision) is shown via the **Task spine live sub-state** (TaskUpdate, § A above) — NOT by re-emitting this full markdown block each phase.
171
171
 
172
172
  ```
173
173
  ---
@@ -204,6 +204,39 @@ In **team mode** the same gates map to the per-card team sub-steps (D.3a AC-Clos
204
204
 
205
205
  ---
206
206
 
207
+ ## Context economy (MANDATORY)
208
+
209
+ `/new` runs as a single long-lived orchestrator across a whole batch. Every byte the orchestrator
210
+ renders inline (Bash tool-results, agent prompts it composes, files it Reads) stays in its context
211
+ window for the **entire** run — there is no real "purge" (the soft "forget" instruction does not
212
+ reclaim tokens). On long batches the dominant token cost is **bulk content the orchestrator pipes
213
+ through its own inline channel**: gate logs, full `git diff`s, whole-file Reads, concatenated
214
+ baselines. Keep that bulk on disk and pass **paths**, not bodies.
215
+
216
+ > **HARD RULE — bulk content never streams inline.**
217
+ > 1. **Gate-output discipline.** Never run a verification gate (`lint` / `tsc` / `test` / `build`)
218
+ > with its output streaming inline — **never `| tee`, never a bare `npm run …`**. Always
219
+ > `cmd > /tmp/<gate>-<CARD-ID>.txt 2>&1` and surface only the exit code inline (`echo "<gate>:$?"`).
220
+ > On failure, surface a **bounded** extract only (`tail -n 30` or `grep -Ei 'error|fail' … | head -n 30`),
221
+ > never the whole log. Pass any fix agent the **file path**, not the inline blob.
222
+ > 2. **Diffs to disk.** Capture `git diff` to `/tmp/diff-<CARD-ID>.txt` and pass agents that path —
223
+ > never paste a full diff into a prompt or render it inline. (`git diff --name-only` file *lists*
224
+ > are fine — they are small.)
225
+ > 3. **Findings stay file-resident.** Reviewer findings already live in their report files
226
+ > (`/qa/*.md`, `/tmp/codexreview-*.md`). Read a findings file only at the decision point that needs
227
+ > it (merge / domain-route / severity tally) — do not echo finding bodies back into your messages.
228
+ > 4. **Pass baseline/report paths, not concatenations.** When briefing a downstream reviewer
229
+ > (e.g. Codex), give it the **list of baseline/report paths** to read on demand rather than
230
+ > inlining their concatenated contents.
231
+ > 5. **Targeted Reads.** When verifying code (Phase 2.5), Read only the evidence lines (grep with
232
+ > tight context around the `file:line` the completion report names) — not whole files.
233
+ >
234
+ > This rule is purely about the **inline channel**. It does NOT change any agent's return contract,
235
+ > nor what the orchestrator is allowed to read at a decision point — only that bulk arrives via a
236
+ > path the consumer opens itself, not via the orchestrator's own context.
237
+
238
+ ---
239
+
207
240
  ## Phase 0 — Workspace Hygiene Pre-flight (BLOCKING — non-skippable)
208
241
 
209
242
  **Why this exists**: FEAT-0006 incident left the main repo with an unpushed orphan commit (`c9d41f9`) and `develop` diverged from `origin/develop` because `/new` started work on a dirty/diverged repo without surfacing the situation. This phase reconciles the main repo state BEFORE any worktree is created, and snapshots any in-progress user work so Phase 6c can restore it after the batch.
@@ -376,7 +409,7 @@ In **team mode** the same gates map to the per-card team sub-steps (D.3a AC-Clos
376
409
  ```bash
377
410
  AUDIT_FILE="/tmp/codex-crosscard-<FIRST-CARD-ID>-<SESSION-ID>.md" && \
378
411
  CODEX_SCRIPT="$(ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/codex/scripts/codex-companion.mjs ~/.claude/plugins/cache/openai-codex/codex/*/scripts/codex-companion.mjs 2>/dev/null | sort -V | tail -1)" && \
379
- [ -z "$CODEX_SCRIPT" ] && echo "CODEX_NOT_FOUND" | tee "$AUDIT_FILE" && exit 1; \
412
+ [ -z "$CODEX_SCRIPT" ] && echo "CODEX_NOT_FOUND" > "$AUDIT_FILE" && exit 1; \
380
413
  node "$CODEX_SCRIPT" task --wait "
381
414
  Cross-card grounding check for a batch of backlog cards about to be implemented together.
382
415
  Your job is to find conflicts BETWEEN cards that per-card reviewers would miss.
@@ -405,7 +438,7 @@ In **team mode** the same gates map to the per-card team sub-steps (D.3a AC-Clos
405
438
  - **Fix**: concrete recommendation (add depends_on, force sequential, etc.)
406
439
 
407
440
  If no cross-card conflicts: return PASS.
408
- " 2>&1 | tee "$AUDIT_FILE"
441
+ " > "$AUDIT_FILE" 2>&1
409
442
  ```
410
443
 
411
444
  **Variable interpolation**:
@@ -885,20 +918,20 @@ For each card, execute these phases in order:
885
918
  field with the exact file path and line where the TODO comment was written.
886
919
  ```
887
920
 
888
- 8. **Run the verification gates and CAPTURE their output** (so step 9 can pass it to a fix agent). Each is its own gate:
921
+ 8. **Run the verification gates and CAPTURE their output to disk** (so step 9 can pass it to a fix agent) — **redirect, never `tee`/stream inline** (per § "Context economy" → Gate-output discipline). Each is its own gate:
889
922
  ```bash
890
923
  cd <worktree-path>
891
- npm run lint 2>&1 | tee /tmp/lint-<CARD-ID>.txt
924
+ npm run lint > /tmp/lint-<CARD-ID>.txt 2>&1; echo "lint:$?"
892
925
  # tsc is its OWN blocking gate — run it ONLY when stack.language includes "typescript"
893
926
  # (do NOT assume `npm run build` covers tsc — Next.js dev builds and many setups skip it):
894
- npx tsc --noEmit 2>&1 | tee /tmp/tsc-<CARD-ID>.txt # guard: when stack.language includes "typescript"
895
- npm test 2>&1 | tee /tmp/test-<CARD-ID>.txt # if tests exist
896
- npm run build 2>&1 | tee /tmp/build-<CARD-ID>.txt
927
+ npx tsc --noEmit > /tmp/tsc-<CARD-ID>.txt 2>&1; echo "tsc:$?" # guard: when stack.language includes "typescript"
928
+ npm test > /tmp/test-<CARD-ID>.txt 2>&1; echo "test:$?" # if tests exist
929
+ npm run build > /tmp/build-<CARD-ID>.txt 2>&1; echo "build:$?"
897
930
  ```
898
- 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 — do NOT run the gates without capture.
931
+ 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.
899
932
  9. **If any check fails**: categorize the error (`lint | TypeScript | test | build`), log it in the tracker as `retry-cause: <category>`.
900
933
  **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.
901
- 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 error output (from the captured `/tmp/*-<CARD-ID>.txt` files), the error category, the explicit list of files it may edit, and the failing check output. 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.
934
+ 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.
902
935
  10. If still failing after 3 retries (or on a stuck loop), log the failure in `## Issues & Flags` and ask the user before continuing.
903
936
  11. **Update tracker**: phase = "2-implement DONE", log files changed (short list), retry count, retry causes (e.g., `"2 retries: TypeScript x1, lint x1"`), and test results (new + existing test count, pass/fail).
904
937
  11b. **File diff gate** — verify the coder only touched its allowed files. The coder commits its work (per `coder.md`), so a bare `git diff --name-only HEAD` is vacuous (clean tree). Detect what the card's commits actually changed against the trunk, with an uncommitted-fallback:
@@ -950,8 +983,12 @@ Before triggering any review, you MUST verify that the coder agent implemented *
950
983
  | 2 | [acceptance criterion] | Done / Missing / Partial | [file:line or "not found"] |
951
984
  ```
952
985
 
953
- 4. For each item, verify by **reading the actual implementation code** (use Grep/Read).
954
- Do NOT trust the coder agent's completion report alone verify independently.
986
+ 4. For each item, verify by **reading the actual implementation code** — but read **narrowly** (per
987
+ § "Context economy" → Targeted Reads): use the coder completion report's `evidence: file:line` as
988
+ the index, then `Grep` (or `Read` with a tight `offset`/`limit` around that line) for the specific
989
+ symbol/route/field — do NOT Read whole files into context to confirm one item. The report is your
990
+ index of WHERE to look; you still verify independently (do NOT trust its `status` alone), just
991
+ without pulling entire files inline.
955
992
 
956
993
  **Verification depth per requirement type:**
957
994
  - **"Create endpoint/route"** → verify: route file exists, handler has correct HTTP method,
@@ -1023,7 +1060,7 @@ Before triggering any review, you MUST verify that the coder agent implemented *
1023
1060
  - The exact list of unimplemented items (copy the checklist rows)
1024
1061
  - The file-ownership restrictions from `## File Ownership Map`
1025
1062
  - The instruction: "Implement ONLY these missing items. Do not refactor or expand scope."
1026
- - After the fix agent completes, re-run the static gates the fix could have broken — `npm run lint`, `npx tsc --noEmit` (when `stack.language` includes typescript), and `npm test` — not just build + lint (a gap-fix can introduce a type error or break a test that the earlier Phase 2 gate had passed).
1063
+ - After the fix agent completes, re-run the static gates the fix could have broken — `npm run lint`, `npx tsc --noEmit` (when `stack.language` includes typescript), and `npm test` — not just build + lint (a gap-fix can introduce a type error or break a test that the earlier Phase 2 gate had passed). Redirect each to `/tmp/<gate>-<CARD-ID>.txt` per § "Context economy" (never inline).
1027
1064
  - Re-verify each fixed item against the code — do NOT trust the agent's self-report.
1028
1065
  - Repeat this sub-loop up to **2 times** (per-item budget, shared with step 0 — see "Loop-counter scope"). After 2 loops, if items remain Partial or Missing:
1029
1066
  - Log in `## Issues & Flags`: list each unimplemented requirement.
@@ -1238,8 +1275,8 @@ After completeness is verified, clean up the implementation before it reaches re
1238
1275
  **Step-by-step**:
1239
1276
 
1240
1277
  1. **Update tracker**: phase = "2.55-simplify".
1241
- 2. Run `git diff` in the worktree to capture all changes for the current card.
1242
- 3. Launch **three agents in parallel** (single message, all receive the full diff):
1278
+ 2. Capture the current card's changes to disk — `git diff > /tmp/diff-<CARD-ID>.txt` in the worktree (**redirect, not inline** per § "Context economy" → Diffs to disk).
1279
+ 3. Launch **three agents in parallel** (single message). Each receives the **path** `/tmp/diff-<CARD-ID>.txt` and Reads the diff itself — do NOT paste the full diff into the prompts:
1243
1280
 
1244
1281
  - **Reuse agent** — search the codebase for existing utilities/helpers that could replace newly written code. Flag duplicated functionality, inline logic that could use existing utils.
1245
1282
  - **Quality agent** — flag redundant state, parameter sprawl, copy-paste with slight variation, leaky abstractions, stringly-typed code where constants/enums exist, unnecessary JSX nesting, unnecessary comments (WHAT comments, narration, task references).
@@ -1252,7 +1289,7 @@ After completeness is verified, clean up the implementation before it reaches re
1252
1289
 
1253
1290
  **Telemetry (Fix Application Log)** — for EVERY finding (valid OR skipped) append one row to the tracker's `## Fix Application Log` section per the schema above. Use `domain=simplify-{reuse|quality|efficiency}` matching the originating agent. Include the `severity` trailing key. Inline: `decision=inline | applied_by=orchestrator | est_lines=<n> | severity=<HIGH|MEDIUM> | finding=<1-line>`. Delegated (domain-override): `decision=<coder|doc-reviewer> | applied_by=<coder|doc-reviewer> | est_lines=<n> | severity=<...> | finding=<1-line>`. Skipped: `decision=skipped | applied_by=- | est_lines=0 | reason=<false-positive|not-worth-addressing>`.
1254
1291
 
1255
- 5. After all fixes, run `npm run lint` and `npx tsc --noEmit` to confirm nothing broke.
1292
+ 5. After all fixes, run `npm run lint` and `npx tsc --noEmit` to confirm nothing broke (redirect to disk per § "Context economy"; surface only exit code + a bounded extract on failure).
1256
1293
  If either fails, fix the regression (up to **2 retries**). **If it still fails after 2 retries**: do NOT silently continue to Phase 2.6 with a broken tree — log the failure in `## Issues & Flags` as `[SIMPLIFY-REGRESSION]` and invoke `AskUserQuestion` (revert the simplify fixes / keep and have me fix manually / stop the card), mirroring the Phase 3.5 escalation.
1257
1294
 
1258
1295
  6. **Update tracker**: phase = "2.55-simplify DONE", log count of fixes applied (or "clean — 0 fixes").
@@ -1423,7 +1460,7 @@ skill's Phase 1 falls back to deriving Gherkin scenarios from
1423
1460
  Doc-reviewer applies all doc-domain fixes itself. The orchestrator does NOT spawn a coder for doc fixes (since v3.40.0 — `doc` is owned by `doc-reviewer`, see "Domain-Override Domains"). The only doc-reviewer output that leaves this phase unfixed is a **doc-drift→bug finding rooted in CODE** (the implementation contradicts a documented contract). Route it explicitly: if the conflicting code file matches the `security` Domain-Override match rule (`paths.high_risk_modules`) → spawn `coder` with the finding now, in this phase (a security-class code fix is not deferrable to a `light` Phase 3.7); otherwise carry the finding into the Phase 3.7 `/codexreview` input as a known code-drift bug and let the Phase 3.7 fix sub-loop apply it. Either way, append a Fix Application Log row with `domain=codex-correctness` (NOT `doc`) so telemetry attributes it as a code fix. Do NOT leave it accumulating in the tracker with no fix owner.
1424
1461
  14. **Knowledge-corpus sync (OPTIONAL — only if the project ships a corpus-sync agent)**: There is NO shipped `obsidian-sync` agent — do NOT dispatch one (a hard dispatch to a non-existent subagent fails silently). Only when the project provides its own knowledge-corpus sync agent (declared in `.baldart/overlays/new.md`) AND doc-reviewer's findings indicate a corpus impact, invoke that agent with the listed paths after the doc fixes are applied. Otherwise skip with a one-line notice (`knowledge-corpus sync: skipped (no corpus-sync agent configured)`). Non-blocking either way.
1425
1462
  15. **Telemetry** — after doc-reviewer returns, append one row per doc finding to `## Fix Application Log`: `3 | doc | est_lines=<n> | decision=doc-reviewer | applied_by=doc-reviewer | finding=<1-line>`. If 0 findings, append one row: `3 | doc | est_lines=0 | decision=skipped | applied_by=- | reason=no-findings`. **Phase-8 producer (named counter)** — ALSO record the per-card doc-gap counts as a structured line in `## Current Card` (carried into `## Completed Cards` at Phase 5): `doc_gaps: found=<N> fixed=<M>` where `N` = total doc findings doc-reviewer raised and `M` = those it applied. This is the single named producer for Phase 8's `doc_gaps_found` / `doc_gaps_fixed` fields — without it those fields have no upstream write and Phase 8 would hard-code zeros. (D.4a is the team-mode producer of the same counter — see Phase 7 § D.4a.)
1426
- 16. Run `npm run lint` and `npx tsc --noEmit` (when `stack.language` includes typescript) to verify nothing broke. If doc-reviewer touched any source-adjacent file (a `.ts`/`.tsx` helper, a co-located doc export), also run `npm run build`. If any check fails, apply the self-healing retry loop (up to 3 times, no user prompt). **If still failing after 3 retries**: do NOT fall through silently to Phase 3.5 — log `[DOC-PHASE-REGRESSION]` in `## Issues & Flags` and invoke `AskUserQuestion` (revert the doc-phase edits that broke the build / keep and fix manually / stop the card).
1463
+ 16. Run `npm run lint` and `npx tsc --noEmit` (when `stack.language` includes typescript) to verify nothing broke (redirect to disk per § "Context economy"). If doc-reviewer touched any source-adjacent file (a `.ts`/`.tsx` helper, a co-located doc export), also run `npm run build`. If any check fails, apply the self-healing retry loop (up to 3 times, no user prompt). **If still failing after 3 retries**: do NOT fall through silently to Phase 3.5 — log `[DOC-PHASE-REGRESSION]` in `## Issues & Flags` and invoke `AskUserQuestion` (revert the doc-phase edits that broke the build / keep and fix manually / stop the card).
1427
1464
  17. **Telemetry for the step-16 self-heal** — if the retry loop spawned any fix (a code edit to recover from a doc-phase regression), append a Fix Application Log row for it AFTER the loop settles (the step-15 doc telemetry row was written before this loop ran, so it does not capture step-16 fixes). Then update tracker: phase = "3-doc-review DONE", log doc findings count, fixes applied.
1428
1465
  If doc-reviewer found a recurring gap, append 1-line to `## Lessons Learned`:
1429
1466
  `DOC: <pattern>`
@@ -1485,12 +1522,13 @@ skill's Phase 1 falls back to deriving Gherkin scenarios from
1485
1522
 
1486
1523
  Run this exact bash block in the worktree. It is deterministic (grep + path match), not LLM-discretionary.
1487
1524
 
1488
- `$TRUNK` is the trunk branch resolved in Phase 0 (`git.trunk_branch`, autodetected when the key is absent — see Phase 0 step 0). `$HIGH_RISK_RE` is an alternation built from `paths.high_risk_modules` in `baldart.config.yml` (e.g. `path1|path2|withAuth`). **If `paths.high_risk_modules` is absent, the path-based triggers (#1) emit a one-line diagnostic and match nothing — no hardcoded project path is baked in.**
1525
+ `$TRUNK` is the trunk branch resolved in Phase 0 (`git.trunk_branch`, autodetected when the key is absent — see Phase 0 step 0). `$HIGH_RISK_RE` is an alternation built from `paths.high_risk_modules` in `baldart.config.yml` (e.g. `path1|path2|withAuth`). **If `paths.high_risk_modules` is absent, the path-based triggers (#1) emit a one-line diagnostic and match nothing — no hardcoded project path is baked in.** `BACKLOG_DIR` is the value of `paths.backlog_dir` from `baldart.config.yml` (fallback `backlog` when the key is absent — emit it resolved, never the literal `${paths.backlog_dir}` token, which is not valid bash). The card lookup uses `-iname` (case-insensitive) on purpose: card filenames carry uppercase prefixes (`FEAT-`/`BUG-`/`CHORE-`) but `CARD_ID` may arrive lowercased (e.g. `/new feat-0019`), and a case-sensitive `-name` would return empty silently → the card would be treated as non-existent.
1489
1526
 
1490
1527
  ```bash
1491
1528
  cd <worktree-path>
1492
1529
  CARD_ID="<CARD-ID>"
1493
- CARD_FILE="$(find "${paths.backlog_dir:-../../backlog}" -name "${CARD_ID}*.yml" 2>/dev/null | head -1)"
1530
+ BACKLOG_DIR="<value of paths.backlog_dir, or 'backlog' if the key is absent>"
1531
+ CARD_FILE="$(find "$BACKLOG_DIR" -iname "${CARD_ID}*.yml" 2>/dev/null | head -1)"
1494
1532
  # Detect what THIS card's commits changed against the trunk (post-commit), with an uncommitted fallback:
1495
1533
  CHANGED="$(git diff --name-only "$TRUNK...HEAD" 2>/dev/null || git diff --name-only HEAD~1..HEAD)"
1496
1534
  # High-risk path alternation from config (empty when paths.high_risk_modules is unset):
@@ -1644,7 +1682,7 @@ The detector (Step A) is bash + grep — guaranteed to run, no LLM skip. The dow
1644
1682
 
1645
1683
  > Sequential-mode global step numbering resumes here at 26 (Phase 3.5 ended at 25; Phase 3.7 used its own local C.0–C.6 counter). The tracker phase-string `4-commit` therefore maps to step 26, NOT a second step 25.
1646
1684
 
1647
- 26. **Update tracker**: phase = "4-commit". **Entry assertion** — before committing, verify the Phase 3.7 e2e re-run obligation was honored: read the tracker for `e2e-rerun: triggered` / `e2e-rerun: not-needed`. If Phase 3.7 touched UI files but no `e2e-rerun` entry exists, do NOT commit yet — go run the re-run per Phase 3.7 step 6 first. Also confirm Phase 3.5/3.7 fixes did not leave lint/tsc broken: if the Phase 3.7 fix sub-loop applied any patch, run `npm run lint` + `npx tsc --noEmit` (when typescript) once before committing.
1685
+ 26. **Update tracker**: phase = "4-commit". **Entry assertion** — before committing, verify the Phase 3.7 e2e re-run obligation was honored: read the tracker for `e2e-rerun: triggered` / `e2e-rerun: not-needed`. If Phase 3.7 touched UI files but no `e2e-rerun` entry exists, do NOT commit yet — go run the re-run per Phase 3.7 step 6 first. Also confirm Phase 3.5/3.7 fixes did not leave lint/tsc broken: if the Phase 3.7 fix sub-loop applied any patch, run `npm run lint` + `npx tsc --noEmit` (when typescript) once before committing (redirect to disk per § "Context economy").
1648
1686
  27. Stage and commit **all changes together** in the worktree using format `[CARD-ID] Brief description` (MUST per AGENTS.md). Include all relevant files — implementation, review fixes, QA-driven fixes, and doc updates in a single commit. Do NOT merge or push yet — that happens post-batch.
1649
1687
  - **IMPORTANT — explicit staging**: NEVER use `git add -A` or `git add .`. Always stage files by explicit name:
1650
1688
  ```bash
@@ -1688,7 +1726,7 @@ Never block the pipeline indefinitely — recover and continue per the rules abo
1688
1726
 
1689
1727
  ### Phase 5 — Context Clean & Continue
1690
1728
  30. Archive the card from Active Code Context in `${paths.references_dir}/project-status.md`.
1691
- 31. **CONTEXT PURGE**: After updating the tracker, deliberately forget the implementation details of this card. From this point forward, you should NOT reference any code, file contents, or review details from this cardonly the summary in the tracker. If you need to recall what happened, read the tracker file. This keeps your working context lean for the next card.
1729
+ 31. **CONTEXT HYGIENE**: After updating the tracker, stop referencing this card's details. From this point forward do NOT pull this card's code, file contents, or review findings back into your messages — the tracker summary is the only carry-over; if you need a detail, Read the tracker (or the on-disk findings/gate file) at the point you need it. NOTE: this is a *reference-hygiene* instruction, not token reclamation — a prompt cannot make the model "forget" (see § "Context economy"). The real lever is upstream: because the § "Context economy" rules kept this card's bulk (gate logs, diffs, whole files, baselines) OUT of your inline context in the first place, there is little to carry over. If the harness auto-compacts at a card boundary, recover via the tracker per § "Context Tracking".
1692
1730
  32. **Update tracker**: clear `## Current Card`, move to next pending card.
1693
1731
  33. **Move to the next card** — restart the per-card pipeline at **Phase 1 (Claim & Context)** for the next pending card in `## Card Queue`. Do NOT re-run Phase 0 (workspace hygiene runs once per batch) and do NOT re-run Pre-flight worktree setup (the worktree already exists). When all cards in the queue are done, proceed to **Final review**.
1694
1732
 
@@ -1748,14 +1786,16 @@ Once ALL cards are committed in the worktree:
1748
1786
  for c in <ALL-CARD-IDS>; do [ -s "/tmp/arch-baseline-$c.md" ] || MISSING+=("$c"); done
1749
1787
  printf '%s\n' "${MISSING[@]}"
1750
1788
  ```
1751
- - **All present** → do NOT re-spawn the architect for the full batch. Concatenate the per-card
1752
- baselines into `${ARCH_BASELINE}` (they already cover every touched file across the batch) and
1753
- proceed to F.3. Log `f.2-arch: reused N per-card baselines (no re-spawn)` in the tracker. This
1754
- avoids the N+1 architect invocations the per-card persistence was designed to prevent.
1789
+ - **All present** → do NOT re-spawn the architect for the full batch. Set `${ARCH_BASELINE_PATHS}`
1790
+ to the newline-separated **list of `/tmp/arch-baseline-<CARD-ID>.md` paths** (they already cover
1791
+ every touched file across the batch) do NOT read or concatenate their contents inline (per
1792
+ § "Context economy" pass paths, not concatenations; Codex Reads them on demand). Proceed to F.3.
1793
+ Log `f.2-arch: reused N per-card baselines (no re-spawn)` in the tracker. This avoids the N+1
1794
+ architect invocations the per-card persistence was designed to prevent.
1755
1795
  - **Any missing** (a card was added late, or a baseline was purged) → invoke **codebase-architect**
1756
1796
  ONCE over the batch scope to map existing architecture, critical patterns, and high-risk code
1757
- paths for regression. Persist its output to `/tmp/arch-baseline-batch-<FIRST-CARD-ID>.md` and use
1758
- that as `${ARCH_BASELINE}`. Log `f.2-arch: re-spawned (missing baselines: <list>)`. Do NOT spawn
1797
+ paths for regression. Persist its output to `/tmp/arch-baseline-batch-<FIRST-CARD-ID>.md` and set
1798
+ `${ARCH_BASELINE_PATHS}` to that single path. Log `f.2-arch: re-spawned (missing baselines: <list>)`. Do NOT spawn
1759
1799
  the architect more than once for the batch — it is the single grounding context for all
1760
1800
  downstream review agents.
1761
1801
 
@@ -1763,12 +1803,12 @@ Once ALL cards are committed in the worktree:
1763
1803
 
1764
1804
  > **Primary reviewer: Codex (a non-Anthropic frontier model, via `codex-companion.mjs`)** — cross-model validation. This is a SINGLE-MODEL Codex pass over the batch diff (scope + review + self-FP-check in one task) — it is NOT the full multi-agent `/codexreview` command pipeline (no sub-agent spawning, no Step 3.5 CoVe). The full per-card `/codexreview` already ran in Phase 3.7; this final pass is the cross-model batch-wide sweep. Claude agents (doc-reviewer, api-perf-cost-auditor) provide doc review and supplementary checks; qa-sentinel runs gates only.
1765
1805
 
1766
- 6. **Launch Codex code review** via `Bash` with `run_in_background: true` and `timeout: 600000` (10 min). **The companion script is invoked WITHOUT `--wait`** so the Bash call returns immediately and the orchestrator can fire the parallel Claude agents in step 7; poll `$REVIEW_FILE` for completion in F.4 (it is `tee`-d, so it fills as Codex runs and is complete when the background task finishes). Combining `run_in_background: true` with `--wait` would block the shell and defeat the parallelism — do NOT pass `--wait` here.
1806
+ 6. **Launch Codex code review** via `Bash` with `run_in_background: true` and `timeout: 600000` (10 min). **The companion script is invoked WITHOUT `--wait`** so the Bash call returns immediately and the orchestrator can fire the parallel Claude agents in step 7; poll `$REVIEW_FILE` for completion in F.4 (it is redirected to the file with `> "$REVIEW_FILE" 2>&1`, so it fills as Codex runs and is complete when the background task finishes — no inline `tee` copy, per § "Context economy"). Combining `run_in_background: true` with `--wait` would block the shell and defeat the parallelism — do NOT pass `--wait` here.
1767
1807
 
1768
1808
  ```bash
1769
1809
  REVIEW_FILE="/tmp/codexreview-batch-<FIRST-CARD-ID>-<SESSION-ID>.md" && \
1770
1810
  CODEX_SCRIPT="$(ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/codex/scripts/codex-companion.mjs ~/.claude/plugins/cache/openai-codex/codex/*/scripts/codex-companion.mjs 2>/dev/null | sort -V | tail -1)" && \
1771
- [ -z "$CODEX_SCRIPT" ] && echo "CODEX_NOT_FOUND" | tee "$REVIEW_FILE" && exit 1; \
1811
+ [ -z "$CODEX_SCRIPT" ] && echo "CODEX_NOT_FOUND" > "$REVIEW_FILE" && exit 1; \
1772
1812
  node "$CODEX_SCRIPT" task "
1773
1813
  Run a deep multi-agent code review for these backlog cards. This is a post-implementation
1774
1814
  review — the code is already written and committed. Your job is to find bugs, regressions,
@@ -1780,8 +1820,8 @@ Once ALL cards are committed in the worktree:
1780
1820
  Files changed (review ALL of these):
1781
1821
  ${REVIEW_SCOPE_FILES}
1782
1822
 
1783
- Architecture baseline (from codebase-architect):
1784
- ${ARCH_BASELINE}
1823
+ Architecture baseline files (read each — they hold file paths, type signatures, patterns, high-risk paths):
1824
+ ${ARCH_BASELINE_PATHS}
1785
1825
 
1786
1826
  Follow the /codexreview protocol:
1787
1827
  1. For each card, read its backlog YAML for acceptance_criteria and entrypoints.
@@ -1802,13 +1842,13 @@ Once ALL cards are committed in the worktree:
1802
1842
  6. Classify surviving findings as VERIFIED, FALSE_POSITIVE, or NEEDS_MANUAL_CONFIRMATION.
1803
1843
 
1804
1844
  Return ONLY verified findings. If zero verified bugs: state 'No verified bugs found.'
1805
- " 2>&1 | tee "$REVIEW_FILE"
1845
+ " > "$REVIEW_FILE" 2>&1
1806
1846
  ```
1807
1847
 
1808
1848
  **Variable interpolation** (build the command string before execution):
1809
1849
  - `${CARD_PATHS}`: newline-separated list of backlog YAML paths
1810
1850
  - `${REVIEW_SCOPE_FILES}`: newline-separated list from Step F.1
1811
- - `${ARCH_BASELINE}`: key findings from Step F.2 (2-3 paragraphs max)
1851
+ - `${ARCH_BASELINE_PATHS}`: newline-separated list of per-card baseline file paths from Step F.2 (Codex Reads them on demand — do NOT inline their contents)
1812
1852
 
1813
1853
  7. **In parallel with Codex**, launch Claude support agents (single message):
1814
1854
 
@@ -1857,7 +1897,7 @@ Once ALL cards are committed in the worktree:
1857
1897
  - **`security`-domain findings** (path in `paths.high_risk_modules`, or RLS-policy SQL) and **`migration`-domain findings** (SQL under the migrations dir) → route to **coder**, but apply the Sub-agent failure protocol's STOP-on-crash rule for these domains (never inline-fallback on a security/migration fix). These are NOT collapsed into a generic "everything else" bucket.
1858
1898
  - **All remaining findings** (other code, perf, test) → invoke the **coder** agent once to apply them in a single pass.
1859
1899
  Run in the order doc-reviewer → coder (or skip either if its partition is empty). Pass only the verified findings, not false positives.
1860
- 12. Run final build: `npm run lint && npx tsc --noEmit && npm run build`.
1900
+ 12. Run final build: `npm run lint && npx tsc --noEmit && npm run build` (redirect each to `/tmp/final-<gate>.txt` per § "Context economy"; surface only exit code + bounded extract on failure).
1861
1901
  If any check fails, apply self-healing retry loop (up to 3 times).
1862
1902
  13. **Update tracker** with final review results:
1863
1903
  - Review engine: Codex (a non-Anthropic frontier model, resolved at runtime by `codex-companion.mjs`) (primary) | Claude code-reviewer (fallback)
@@ -2537,7 +2577,7 @@ For each completed agent:
2537
2577
 
2538
2578
  After ALL agents in the group complete successfully:
2539
2579
 
2540
- 1. **D.1 — Build verification (group)** — Run `npm run build` in the worktree to verify combined changes compile. If build fails, identify which card's changes broke it (from `git diff` per card), spawn a targeted fix-coder for those files only.
2580
+ 1. **D.1 — Build verification (group)** — Run `npm run build` in the worktree to verify combined changes compile (redirect to `/tmp/build-group.txt` per § "Context economy"; surface only exit code + bounded extract on failure). If build fails, identify which card's changes broke it (from `git diff --name-only` per card), spawn a targeted fix-coder for those files only.
2541
2581
 
2542
2582
  1.5. **D.1.5 — Effective per-card review profile (compute ONCE; drives D.2 + D.4b)** — For EACH card in the group, compute its **effective codex profile** with the SAME deterministic rule the sequential Phase 3.7 Step C uses, so the two paths never disagree:
2543
2583
  - **Floor**: read the card's `review_profile` field (`skip`/`light`/`balanced`/`deep`) per the QA Profile Selector (fallback-computed only for legacy cards lacking the field).
@@ -2558,7 +2598,7 @@ After ALL agents in the group complete successfully:
2558
2598
 
2559
2599
  3a. **D.3a — Phase 2.5b AC-Closure Gate (per-card, BLOCKING — non-skippable)** — For EACH card in the group, **sequentially**, invoke the full Phase 2.5b gate as documented in `### Phase 2.5b — AC-Closure Gate (BLOCKING — Scope Closure Discipline)`. This includes: build the AC Closure Ledger from the card YAML, run the rationalization scan, invoke `AskUserQuestion` one-per-deferred-AC, run the `implementation_notes` deferral audit, and persist the ledger in the tracker. Until EVERY card in the group exits PASS, do NOT proceed to D.3b. Cards exiting with `not_implemented` ACs that the user routes to "Implementa adesso" must finish their fix-coder loop and re-pass the gate before D.3b starts for the next card. Log under `## AC Closure Ledger — <CARD-ID>` per card.
2560
2600
 
2561
- 3b. **D.3b — Phase 2.55 Simplify (per-card, FANNED OUT across the group)** — The Simplify agents are **read-only analysis on file-disjoint per-card diffs** (the orchestrator applies the fixes afterward), so there is NO reason to run them one card at a time. **Spawn the per-card Simplify analysis for ALL eligible cards in PARALLEL** — in a SINGLE message, fire each card's Phase 2.55 trio (Reuse / Quality / Efficiency) scoped to that card's diff (`git diff --name-only` filtered by the card's File Ownership Map). Per-card (not group-aggregate) so findings stay attributable. When all analyses return, **apply fixes per card** (file-disjoint → no write conflict), then re-run `npm run lint` and `npx tsc --noEmit` on the worktree ONCE for the whole group. (Concurrency is capped by the platform; passing N cards is safe — excess agents queue.)
2601
+ 3b. **D.3b — Phase 2.55 Simplify (per-card, FANNED OUT across the group)** — The Simplify agents are **read-only analysis on file-disjoint per-card diffs** (the orchestrator applies the fixes afterward), so there is NO reason to run them one card at a time. **Spawn the per-card Simplify analysis for ALL eligible cards in PARALLEL** — in a SINGLE message, fire each card's Phase 2.55 trio (Reuse / Quality / Efficiency) against that card's diff captured to `/tmp/diff-<CARD-ID>.txt` (per Phase 2.55 step 2 — pass each trio the **path**, scoped to the card's File Ownership Map; never inline the diff). Per-card (not group-aggregate) so findings stay attributable. When all analyses return, **apply fixes per card** (file-disjoint → no write conflict), then re-run `npm run lint` and `npx tsc --noEmit` on the worktree ONCE for the whole group (redirect to disk per § "Context economy"). (Concurrency is capped by the platform; passing N cards is safe — excess agents queue.)
2562
2602
  - **Gate (enumerated, `review_profile`-driven)**: SKIP D.3b for a card whose `review_profile == skip` (trivial / non-implementable card — Simplify is quality-only, so there is no merge-gate coverage to lose, and there is no substantive diff to simplify). Log `simplify: SKIPPED (review_profile=skip)`. For `light`/`balanced`/`deep` cards D.3b runs unchanged. This is the ONLY enumerated skip — never skip D.3b "for time" on a `light`+ card. (Skipped cards are simply omitted from the parallel fan-out.)
2563
2603
 
2564
2604
  3c. **D.3c — Phase 2.6 E2E-Review (per-card)** — First, evaluate the existing Gate table for EVERY card at once (skip when `features.has_e2e_review: false`, backend-only diff per the diff predicate documented in Phase 2.6, or card type in the Phase 2.6 skip set — `backend`/`api`/`db`/`infra`/`docs`/`chore`/`config`). In practice most cards in a group skip this gate (backend/db/api), so the eligible set is usually 0–1. For the cards that PASS the gate, invoke `/e2e-review` in programmatic mode with that card's payload. Each `/e2e-review` keeps its own isolated state dir (`.baldart/e2e-review/<CARD-ID>/`), so multiple runs do not clobber each other's artifacts.
@@ -453,13 +453,17 @@ Backlog cards created during `/prd` sessions may exist as untracked files in the
453
453
  but are NOT on the trunk branch yet. The worktree (branched from the trunk) won't have them.
454
454
 
455
455
  ```bash
456
+ # BACKLOG_DIR = value of paths.backlog_dir from baldart.config.yml (fallback `backlog`
457
+ # when the key is absent — emit it resolved, never the literal ${paths.backlog_dir}
458
+ # token, which is not valid bash). Same resolution as /new's card-scoped diff block.
459
+ BACKLOG_DIR="<value of paths.backlog_dir, or 'backlog' if the key is absent>"
456
460
  MAIN_ROOT="$(git -C "$WORKTREE_PATH" rev-parse --show-superproject-working-tree 2>/dev/null || pwd)"
457
461
 
458
462
  # For each card in the batch, check if its YAML exists in the main repo but not in the worktree
459
- for CARD_FILE in $(ls "$MAIN_ROOT/${paths.backlog_dir:-backlog}"/*.yml 2>/dev/null); do
463
+ for CARD_FILE in $(ls "$MAIN_ROOT/$BACKLOG_DIR"/*.yml 2>/dev/null); do
460
464
  BASENAME=$(basename "$CARD_FILE")
461
- if [ ! -f "$WORKTREE_PATH/${paths.backlog_dir:-backlog}/$BASENAME" ]; then
462
- cp "$CARD_FILE" "$WORKTREE_PATH/${paths.backlog_dir:-backlog}/$BASENAME"
465
+ if [ ! -f "$WORKTREE_PATH/$BACKLOG_DIR/$BASENAME" ]; then
466
+ cp "$CARD_FILE" "$WORKTREE_PATH/$BACKLOG_DIR/$BASENAME"
463
467
  echo "Synced untracked card: $BASENAME"
464
468
  fi
465
469
  done
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.10.0",
3
+ "version": "4.12.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"
@@ -94,19 +94,39 @@ function countOverlays(cwd) {
94
94
  return counts('') + counts('agents') + counts('commands');
95
95
  }
96
96
 
97
+ // SHA-first drift detection for skill overlays, mirroring `overlay drift`
98
+ // (overlay.js): a pinned `base_file_sha` is the source of truth — an overlay is
99
+ // drifted only when the base SKILL.md content actually changed. The version is
100
+ // a fallback ONLY when no `base_file_sha` is present, because VERSION bumps on
101
+ // every release regardless of whether THIS base file changed; comparing it
102
+ // directly flagged every unchanged overlay as drifted after any update (v4.11.0).
97
103
  function overlayDrift(cwd, frameworkVersion) {
98
104
  if (!frameworkVersion) return [];
99
105
  const dir = path.join(cwd, '.baldart', 'overlays');
100
106
  if (!fileExists(dir)) return [];
107
+ let computeBaseFileSha;
108
+ try { ({ computeBaseFileSha } = require('../utils/overlay-merger')); }
109
+ catch (_) { computeBaseFileSha = null; }
101
110
  const drifted = [];
102
111
  try {
103
112
  for (const f of fs.readdirSync(dir)) {
104
- if (!f.endsWith('.md')) continue;
113
+ if (!f.endsWith('.md') || f.endsWith('.example.md')) continue;
105
114
  const txt = fs.readFileSync(path.join(dir, f), 'utf8');
106
115
  const fm = txt.match(/^---\n([\s\S]*?)\n---/);
107
116
  if (!fm) continue;
108
117
  try {
109
118
  const meta = yaml.load(fm[1]) || {};
119
+ const name = f.replace(/\.md$/, '');
120
+ const baseAbs = path.join(cwd, '.framework', 'framework', '.claude', 'skills', name, 'SKILL.md');
121
+ // SHA-first: trust the pinned base_file_sha when we can read the base.
122
+ if (meta.base_file_sha && computeBaseFileSha && fileExists(baseAbs)) {
123
+ const currentSha = computeBaseFileSha(fs.readFileSync(baseAbs, 'utf8'));
124
+ if (String(meta.base_file_sha) !== currentSha) {
125
+ drifted.push({ name: f, target: meta.base_skill_version || 'sha', installed: frameworkVersion });
126
+ }
127
+ continue;
128
+ }
129
+ // Fallback: no base_file_sha (or base unreadable) → version comparison.
110
130
  if (meta.base_skill_version && meta.base_skill_version !== frameworkVersion) {
111
131
  drifted.push({ name: f, target: meta.base_skill_version, installed: frameworkVersion });
112
132
  }
@@ -81,9 +81,16 @@ async function status() {
81
81
  if (overlays.length === 0) {
82
82
  UI.info('No overlays authored. Examples in: .framework/templates/overlays/');
83
83
  } else {
84
- // Best-effort version-drift check
84
+ // Best-effort drift check — SHA-first (mirrors `overlay drift`): the
85
+ // pinned base_file_sha is the source of truth; an overlay is drifted
86
+ // only when the base SKILL.md content actually changed. Version is a
87
+ // fallback ONLY when base_file_sha is absent, since VERSION bumps every
88
+ // release regardless of whether THIS base changed (v4.11.0).
85
89
  const yaml = require('js-yaml');
86
90
  const fwVersion = version;
91
+ let computeBaseFileSha;
92
+ try { ({ computeBaseFileSha } = require('../utils/overlay-merger')); }
93
+ catch (_) { computeBaseFileSha = null; }
87
94
  overlays.forEach((file) => {
88
95
  const full = path.join(overlaysDir, file);
89
96
  let line = `Overlay: ${file}`;
@@ -92,6 +99,21 @@ async function status() {
92
99
  const fmMatch = txt.match(/^---\n([\s\S]*?)\n---/);
93
100
  if (fmMatch) {
94
101
  const fm = yaml.load(fmMatch[1]) || {};
102
+ const name = file.replace(/\.md$/, '');
103
+ const baseAbs = path.join(process.cwd(), '.framework', 'framework', '.claude', 'skills', name, 'SKILL.md');
104
+ // SHA-first when we can read the base file.
105
+ if (fm.base_file_sha && computeBaseFileSha && fs.existsSync(baseAbs)) {
106
+ const currentSha = computeBaseFileSha(fs.readFileSync(baseAbs, 'utf8'));
107
+ if (String(fm.base_file_sha) !== currentSha) {
108
+ line += ` (base changed: sha ${fm.base_file_sha} → ${currentSha} — review for drift)`;
109
+ UI.warning(line);
110
+ return;
111
+ }
112
+ if (fm.base_skill_version) line += ` (v${fm.base_skill_version}, base unchanged)`;
113
+ UI.success(line);
114
+ return;
115
+ }
116
+ // Fallback: no base_file_sha → version comparison.
95
117
  if (fm.base_skill_version && fwVersion && fm.base_skill_version !== fwVersion) {
96
118
  line += ` (targets v${fm.base_skill_version}, installed v${fwVersion} — review for drift)`;
97
119
  UI.warning(line);
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Unit tests for overlay-merger marker stability + version-pin refresh (v4.11.0).
4
+ *
5
+ * Run: node src/utils/__tests__/overlay-merger.test.js
6
+ * Exits 0 on success, non-zero on failure (CI-friendly via node:test).
7
+ *
8
+ * Contract under test:
9
+ * - The generated-file marker is CONTENT-ADDRESSED only (base_sha + overlay_sha)
10
+ * so `mergeOverlay` produces BYTE-IDENTICAL output across version-only bumps —
11
+ * killing the per-update reconcile churn (Trigger A).
12
+ * - `readMarker` still reads legacy markers that carry `base_version=`.
13
+ * - `refreshOverlayVersionPin` only rewrites the pin when the base is unchanged
14
+ * (sha matches), never masking real drift.
15
+ */
16
+ const test = require('node:test');
17
+ const assert = require('node:assert');
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+
22
+ const {
23
+ mergeOverlay, readMarker, isGeneratedFile, computeBaseFileSha,
24
+ refreshOverlayVersionPin, MARKER_PREFIX,
25
+ } = require('../overlay-merger');
26
+
27
+ const BASE = `---
28
+ name: coder
29
+ description: coding agent
30
+ ---
31
+
32
+ Preamble line.
33
+
34
+ ## Role
35
+ You write code.
36
+
37
+ ## Rules
38
+ Be careful.
39
+ `;
40
+
41
+ const OVERLAY = `---
42
+ base_agent: coder
43
+ base_agent_version: 4.9.0
44
+ base_file_sha: "${computeBaseFileSha(BASE)}"
45
+ mode: extend
46
+ ---
47
+
48
+ ## [APPEND] Rules
49
+ Always run the linter.
50
+ `;
51
+
52
+ test('marker is byte-stable across version-only bumps (Trigger A fix)', () => {
53
+ const a = mergeOverlay({ kind: 'agent', name: 'coder', baseContent: BASE, overlayContent: OVERLAY, frameworkVersion: '4.10.0' });
54
+ const b = mergeOverlay({ kind: 'agent', name: 'coder', baseContent: BASE, overlayContent: OVERLAY, frameworkVersion: '4.11.0' });
55
+ assert.strictEqual(a.generated, b.generated, 'generated output must be identical when only frameworkVersion changes');
56
+ });
57
+
58
+ test('generated marker no longer stamps base_version', () => {
59
+ const { generated } = mergeOverlay({ kind: 'agent', name: 'coder', baseContent: BASE, overlayContent: OVERLAY, frameworkVersion: '4.11.0' });
60
+ assert.ok(generated.includes(MARKER_PREFIX), 'must carry the generated marker');
61
+ assert.ok(!/base_version=/.test(generated), 'marker must NOT contain base_version=');
62
+ assert.ok(/base_sha=/.test(generated), 'marker must keep base_sha=');
63
+ assert.ok(/overlay_sha=/.test(generated), 'marker must keep overlay_sha=');
64
+ });
65
+
66
+ test('readMarker reads NEW marker (no base_version)', () => {
67
+ const { generated } = mergeOverlay({ kind: 'agent', name: 'coder', baseContent: BASE, overlayContent: OVERLAY, frameworkVersion: '4.11.0' });
68
+ const m = readMarker(generated);
69
+ assert.ok(m, 'marker must be detected');
70
+ assert.strictEqual(m.kind, 'agent');
71
+ assert.strictEqual(m.name, 'coder');
72
+ assert.strictEqual(m.baseVersion, null, 'new markers have no base_version');
73
+ assert.ok(m.baseSha && m.overlaySha);
74
+ assert.ok(isGeneratedFile(generated));
75
+ });
76
+
77
+ test('readMarker still reads LEGACY marker (with base_version)', () => {
78
+ const legacy = `---\nname: coder\n---\n${MARKER_PREFIX} kind=agent name=coder base_version=4.5.0 base_sha=abc123def456 overlay_sha=deadbeef0000\n DO NOT EDIT. -->\n\n## Role\nx\n`;
79
+ const m = readMarker(legacy);
80
+ assert.ok(m, 'legacy marker must still parse');
81
+ assert.strictEqual(m.baseVersion, '4.5.0');
82
+ assert.strictEqual(m.baseSha, 'abc123def456');
83
+ assert.strictEqual(m.overlaySha, 'deadbeef0000');
84
+ assert.ok(isGeneratedFile(legacy));
85
+ });
86
+
87
+ function withTempOverlay(content, fn) {
88
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-pin-'));
89
+ const p = path.join(dir, 'coder.md');
90
+ fs.writeFileSync(p, content);
91
+ try { return fn(p); } finally { fs.rmSync(dir, { recursive: true, force: true }); }
92
+ }
93
+
94
+ test('refreshOverlayVersionPin: refreshes when sha matches and version differs', () => {
95
+ withTempOverlay(OVERLAY, (p) => {
96
+ const r = refreshOverlayVersionPin(p, 'agent', '4.11.0', computeBaseFileSha(BASE));
97
+ assert.strictEqual(r.changed, true, r.reason);
98
+ assert.match(fs.readFileSync(p, 'utf8'), /base_agent_version:\s*4\.11\.0/);
99
+ });
100
+ });
101
+
102
+ test('refreshOverlayVersionPin: NO-OP when base sha mismatches (drift preserved)', () => {
103
+ withTempOverlay(OVERLAY, (p) => {
104
+ const r = refreshOverlayVersionPin(p, 'agent', '4.11.0', 'feedfacefeed'); // wrong sha → base changed
105
+ assert.strictEqual(r.changed, false);
106
+ assert.match(fs.readFileSync(p, 'utf8'), /base_agent_version:\s*4\.9\.0/, 'stale pin must remain as a drift signal');
107
+ });
108
+ });
109
+
110
+ test('refreshOverlayVersionPin: NO-OP when already current', () => {
111
+ const overlayCurrent = OVERLAY.replace('4.9.0', '4.11.0');
112
+ withTempOverlay(overlayCurrent, (p) => {
113
+ const r = refreshOverlayVersionPin(p, 'agent', '4.11.0', computeBaseFileSha(BASE));
114
+ assert.strictEqual(r.changed, false);
115
+ assert.strictEqual(r.reason, 'already-current');
116
+ });
117
+ });
@@ -81,9 +81,61 @@ function ensureBaseFileShaInOverlay(overlayPath, baseSha) {
81
81
  return { changed: true, reason: 'backfilled' };
82
82
  }
83
83
 
84
- function buildMarker({ kind, name, baseVersion, baseSha, overlaySha }) {
84
+ // The marker identity is CONTENT-ADDRESSED only: `base_sha` + `overlay_sha`,
85
+ // both of which change exclusively when the base file or the overlay actually
86
+ // change. `base_version` is deliberately NOT stamped here — VERSION bumps on
87
+ // every framework release regardless of whether THIS base file changed, so
88
+ // embedding it made every `baldart update` rewrite this line and produce a
89
+ // spurious git diff on byte-identical generated files (→ a "post-update
90
+ // reconcile" commit every single update). The field was informational only
91
+ // (readMarker parsed it but nothing gated regeneration on it), so dropping it
92
+ // buys byte-stability across version-only bumps at zero cost. (v4.11.0)
93
+ /**
94
+ * Refresh the overlay's informational `base_<kind>_version` pin to the installed
95
+ * VERSION — but ONLY when the base content is unchanged (the overlay's
96
+ * `base_file_sha` still matches the current base sha). This keeps the pin
97
+ * truthful (it tracks the version the overlay was last reconciled against)
98
+ * without ever masking real drift: when the base content HAS changed the sha
99
+ * differs, so we leave the stale pin alone and it remains a legitimate
100
+ * "review me" signal for `overlay drift` / `status` / `doctor`.
101
+ *
102
+ * Contract: NO-OP unless `base_file_sha` is present AND equals `currentBaseSha`
103
+ * AND the version field actually differs. Fail-safe: any I/O error returns
104
+ * `changed:false` so it can never block a merge. (v4.11.0)
105
+ *
106
+ * @param {string} overlayPath absolute path to `.baldart/overlays/<…>.md`
107
+ * @param {string} kind 'agent' | 'command' | 'skill'
108
+ * @param {string} frameworkVersion installed framework VERSION
109
+ * @param {string} currentBaseSha computeBaseFileSha(currentBaseContent)
110
+ * @returns {{ changed: boolean, reason: string }}
111
+ */
112
+ function refreshOverlayVersionPin(overlayPath, kind, frameworkVersion, currentBaseSha) {
113
+ if (!frameworkVersion || !currentBaseSha) return { changed: false, reason: 'missing-input' };
114
+ let content;
115
+ try { content = fs.readFileSync(overlayPath, 'utf8'); }
116
+ catch (_) { return { changed: false, reason: 'unreadable' }; }
117
+ const m = content.match(/^(?\s*)---\n([\s\S]*?)\n---(\r?\n|$)/);
118
+ if (!m) return { changed: false, reason: 'no-frontmatter' };
119
+ const fmBody = m[2];
120
+ // Only refresh when the base is provably unchanged — never mask drift.
121
+ const shaMatch = fmBody.match(/^\s*base_file_sha\s*:\s*["']?([0-9a-f]+)["']?\s*$/m);
122
+ if (!shaMatch || shaMatch[1] !== currentBaseSha) return { changed: false, reason: 'sha-mismatch-or-absent' };
123
+ const versionKey = `base_${kind}_version`;
124
+ const verRe = new RegExp(`^(\\s*${versionKey}\\s*:\\s*).*$`, 'm');
125
+ if (!verRe.test(fmBody)) return { changed: false, reason: 'no-version-field' };
126
+ const current = fmBody.match(verRe)[0].split(':').slice(1).join(':').trim();
127
+ if (current === String(frameworkVersion)) return { changed: false, reason: 'already-current' };
128
+ const newFmBody = fmBody.replace(verRe, `$1${frameworkVersion}`);
129
+ const replaced = `${m[1]}---\n${newFmBody}\n---${m[3]}`;
130
+ const updated = content.slice(0, m.index) + replaced + content.slice(m.index + m[0].length);
131
+ try { fs.writeFileSync(overlayPath, updated, 'utf8'); }
132
+ catch (_) { return { changed: false, reason: 'unwritable' }; }
133
+ return { changed: true, reason: 'refreshed' };
134
+ }
135
+
136
+ function buildMarker({ kind, name, baseSha, overlaySha }) {
85
137
  const baseShaField = baseSha ? ` base_sha=${baseSha}` : '';
86
- return `${MARKER_PREFIX} kind=${kind} name=${name} base_version=${baseVersion}${baseShaField} overlay_sha=${overlaySha}
138
+ return `${MARKER_PREFIX} kind=${kind} name=${name}${baseShaField} overlay_sha=${overlaySha}
87
139
  DO NOT EDIT MANUALLY. Edit .baldart/overlays/${kind}s/${name}.md instead;
88
140
  this file is regenerated by \`npx baldart update\`. -->`;
89
141
  }
@@ -98,6 +150,9 @@ function buildMarker({ kind, name, baseVersion, baseSha, overlaySha }) {
98
150
  * - immediately after the closing `---` of the YAML frontmatter (v3.26.1+)
99
151
  *
100
152
  * `base_sha` is optional — older generated files (pre-v3.19.0) won't have it.
153
+ * `base_version` is optional too: v4.11.0+ stops stamping it (see buildMarker),
154
+ * but files generated by older versions still carry it, so the regex tolerates
155
+ * either shape and surfaces `baseVersion` as null when absent.
101
156
  */
102
157
  function readMarker(content) {
103
158
  const idx = content.indexOf(MARKER_PREFIX);
@@ -107,9 +162,9 @@ function readMarker(content) {
107
162
  const block = content.slice(idx, idx + 4000);
108
163
  const end = block.indexOf('-->');
109
164
  if (end === -1) return null;
110
- const m = block.slice(0, end).match(/kind=(\S+)\s+name=(\S+)\s+base_version=(\S+)(?:\s+base_sha=(\S+))?\s+overlay_sha=(\S+)/);
165
+ const m = block.slice(0, end).match(/kind=(\S+)\s+name=(\S+)(?:\s+base_version=(\S+))?(?:\s+base_sha=(\S+))?\s+overlay_sha=(\S+)/);
111
166
  if (!m) return null;
112
- return { kind: m[1], name: m[2], baseVersion: m[3], baseSha: m[4] || null, overlaySha: m[5] };
167
+ return { kind: m[1], name: m[2], baseVersion: m[3] || null, baseSha: m[4] || null, overlaySha: m[5] };
113
168
  }
114
169
 
115
170
  /**
@@ -191,7 +246,8 @@ function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersio
191
246
  warnings.push(`overlay frontmatter parse error (${err.message}) — treating overlay as plain markdown without metadata`);
192
247
  }
193
248
  }
194
- const baseVersionTargeted = String(overlayMeta[`base_${kind}_version`] || '').trim() || 'unknown';
249
+ const baseVersionTargeted = String(overlayMeta[`base_${kind}_version`] || '').trim()
250
+ || (frameworkVersion ? String(frameworkVersion) : 'unknown');
195
251
  const overlayMode = (overlayMeta.mode || 'extend').trim();
196
252
 
197
253
  // Parse the base. Keep its frontmatter verbatim — that's what Claude reads.
@@ -239,13 +295,7 @@ function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersio
239
295
 
240
296
  const overlaySha = shortSha(overlayContent);
241
297
  const baseSha = shortSha(baseContent);
242
- const marker = buildMarker({
243
- kind,
244
- name,
245
- baseVersion: frameworkVersion || baseVersionTargeted,
246
- baseSha,
247
- overlaySha
248
- });
298
+ const marker = buildMarker({ kind, name, baseSha, overlaySha });
249
299
 
250
300
  const fmBlock = baseParsed.frontmatter
251
301
  ? `---\n${baseParsed.frontmatter}\n---\n`
@@ -280,5 +330,6 @@ module.exports = {
280
330
  shortSha,
281
331
  computeBaseFileSha,
282
332
  ensureBaseFileShaInOverlay,
333
+ refreshOverlayVersionPin,
283
334
  MARKER_PREFIX
284
335
  };
@@ -484,16 +484,23 @@ class SymlinkUtils {
484
484
  }
485
485
 
486
486
  _generateOverlayedFile({ kind, name, fwAbsolute, overlayAbs, linkPath, lstat, frameworkVersion, aggregate, adapter, targetRel }) {
487
- const { mergeOverlay, isGeneratedFile, computeBaseFileSha, ensureBaseFileShaInOverlay } = require('./overlay-merger');
487
+ const { mergeOverlay, isGeneratedFile, computeBaseFileSha, ensureBaseFileShaInOverlay, refreshOverlayVersionPin } = require('./overlay-merger');
488
488
  const baseContent = fs.readFileSync(fwAbsolute, 'utf8');
489
+ const baseSha = computeBaseFileSha(baseContent);
489
490
  // Backfill base_file_sha into the overlay frontmatter so `overlay drift` is
490
491
  // deterministic for EVERY overlay, not just freshly-scaffolded ones
491
492
  // (additive-only, never overwrites). Done BEFORE reading overlayContent so
492
493
  // the marker's overlay_sha already reflects it — no one-time regen churn.
493
494
  // (v3.33.0)
494
495
  try {
495
- const bf = ensureBaseFileShaInOverlay(overlayAbs, computeBaseFileSha(baseContent));
496
+ const bf = ensureBaseFileShaInOverlay(overlayAbs, baseSha);
496
497
  if (bf.changed) UI.info(`[${adapter.label}] ${kind} ${name}: backfilled base_file_sha into overlay frontmatter.`);
498
+ // Keep the informational base_<kind>_version pin truthful — but ONLY when
499
+ // the base content is unchanged (sha matches). When the base HAS changed
500
+ // the sha differs and the pin is left stale on purpose, so it stays a
501
+ // legitimate drift signal to reconcile. (v4.11.0)
502
+ const vp = refreshOverlayVersionPin(overlayAbs, kind, frameworkVersion, baseSha);
503
+ if (vp.changed) UI.info(`[${adapter.label}] ${kind} ${name}: refreshed base_${kind}_version pin to v${frameworkVersion} (base unchanged).`);
497
504
  } catch (_) { /* non-fatal — never block a merge on backfill */ }
498
505
  const overlayContent = fs.readFileSync(overlayAbs, 'utf8');
499
506
  const merged = mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion });