baldart 4.53.8 → 4.54.1

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,44 @@ 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.54.1] - 2026-06-19
9
+
10
+ **Two bugs in the Codex relay of the `/new` review workflows — found live as a ~20-minute "freeze".** During a real `/new FEAT-0037` the Codex step of `new-card-review` appeared stuck; on-disk diagnosis (`/tmp/codexreview-wave-FEAT-0037*.md` + `ps`) showed **Codex had actually finished 9 minutes earlier** (full review, zero actionable findings) but the relay couldn't detect it, because of two independent, compounding bugs:
11
+
12
+ 1. **`$$` placeholder in the `/tmp` filenames.** The workflows built `/tmp/codextask-${cardId}-$$.txt` / `/tmp/codexreview-wave-${cardId}-$$.md`. In a JS template string `$$` is the literal two characters — the `Write` tool keeps it verbatim, but **bash expands `$$` to the PID of each subshell**, so the launch, the poll, and the extract Bash calls each referenced a *different* path that never agreed. Observed: the first launch `cat`-ed a PID-named task file that didn't exist → Codex ran with an empty task ("Provide a prompt…", 68 bytes); the relay retried with clean names (Codex then ran for real), but the poll loop kept `grep`-ing a `-$$.md` file with a fresh PID each iteration → never terminal.
13
+ 2. **Codex emits prose, not the sentinel block.** Even with the right file, Codex (GPT-5.x via the companion) wrote its findings as a prose report (`**Findings** No actionable… **Cleared Concerns** …`) instead of between `<<<FINDINGS_JSON>>>`/`<<<END_FINDINGS_JSON>>>`, so the relay's only terminal condition (`grep END_FINDINGS_JSON`) never matched → it spun to the 10-minute window before falling back to `code-reviewer`.
14
+
15
+ Nothing broke downstream (the fallback is wired), but every occurrence cost ~12 wasted minutes + a redundant full `code-reviewer` re-run. Fix:
16
+ - **Stable per-wave/batch filenames (no `$$`)** — `cardId`/`firstCardId` is already unique per wave/batch and the files are `/tmp` scratch truncated by `>` on launch, so the launch/poll/extract calls now agree on one literal path.
17
+ - **Robust terminal detection** — the relay also polls for the companion's reliable `Turn completed.` marker. New return logic: a non-empty awk extraction ⇒ `codexAvailable:true` + parsed findings; `Turn completed.`/`CODEX_NOT_FOUND` seen **but** the awk output empty (Codex finished with prose, no machine-readable block) ⇒ `codexAvailable:false` + **exit immediately** (route to the `code-reviewer` fallback — never parse findings out of prose, which would risk dropping a BLOCKER); only a full 10-minute window with no marker ⇒ false. So a prose-only completion now falls back at Codex-done time, not +10 min.
18
+ - **Task-prompt hardening (best-effort)** — Codex is told its FINAL message must be ONLY the sentinel block (no `Findings`/`Cleared Concerns`/`Validation` prose; cleared concerns go as `requires_action:false` entries inside the JSON), reducing how often the prose-fallback path is hit.
19
+
20
+ Verified by parsing both workflows (`node -c`), confirming zero `$$` remain, and running the exact `awk` + marker logic against the real incident fixture (prose-only → empty awk + `Turn completed.` → `false`/fallback) and a synthetic sentinel file (→ valid JSON, `true`). Scope is the two review workflows only; `new2.js`'s Codex usage is a different shape (a `$FILE` var, no sentinel-poll dependency; a synchronous `--wait` per-card call) and inherits the fix at the Final via its delegation to `new-final-review`. **PATCH** (relay bugfix; no new agent/skill/command, no `baldart.config.yml` key, no install/layout change).
21
+
22
+ ### Fixed
23
+
24
+ - **`framework/.claude/workflows/new-card-review.js`** + **`new-final-review.js`** — Codex relay: (1) dropped the `$$` placeholder from the `/tmp` task/review filenames (stable per-wave/batch names); (2) added `Turn completed.` as a poll terminal + a deterministic 3-way return rule (non-empty JSON ⇒ available; finished-without-block ⇒ unavailable, immediate fallback, never prose-parse; full-window-no-marker ⇒ unavailable); (3) task prompt now demands the FINAL message be ONLY the sentinel block.
25
+
26
+ ## [4.54.0] - 2026-06-19
27
+
28
+ **The i18n anti-hardcoded gate is now DIFF-SCOPED in every per-change context, and the two UI-authoring surfaces (`ui-expert`, `ui-design`) finally honor the i18n layer.** Surfaced by a real `/new FEAT-0037` run on a consumer (`mayo`) that is *partially* i18n-adopted: the coder had to manually triage 25 eslint-i18n errors + 4 parity failures and discovered they were **pre-existing baseline debt in files the card never touched** (`products/edit`, `products/new`, `sw-register`, …). Root cause: the i18n gate ran **whole-repo** (`npx eslint --config eslint.i18n.config.mjs .`) in every per-change consumer, while the *normal* lint gate was already diff-scoped — so on any mid-adoption project every card failed the i18n gate on unrelated baseline debt. The coder did the right thing (distrusted its own report, verified independently) but should never have had to. (The parity failure and the `Cannot find module '@/lib/i18n'` tsc note in that run were project-side, not framework — handled soundly by the coder.)
29
+
30
+ 1. **Diff-scoped per-change gate.** `/new` per-card (Phase 2 step 8), the `new2` Phase-2 gate, the Final review batch gate, and `qa-sentinel` now strip the trailing ` .` full-sweep target from the resolved command and lint **only the change's changed `*.{ts,tsx,js,jsx,mjs,cjs}` files** (the card's diff per-card, the batch's diff at the Final review). Pre-existing debt in untouched files no longer fails an unrelated change; no changed JS/TS files → SKIP. A diff-scoped failure is therefore unambiguously a string the change introduced — `implement.md` step 9 now says so (no more "is this mine?" triage). The string transform (`${GATE% .}` + changed-file append) was verified on flat/legacy/path-less commands and end-to-end on a real git repo (baseline file in HEAD but outside the diff is correctly excluded from the eslint args).
31
+ 2. **`i18n.lint_command` semantics unchanged** = the canonical **full-sweep** command. The full-sweep consumers (`/i18n` audit, `/i18n-adopt` migration, `i18n-align` routine) keep running it whole-repo by design (they reconcile the entire codebase). No config key change → the schema-change propagation rule does not apply.
32
+ 3. **`ui-expert` + `ui-design` honor i18n.** `ui-expert` (which WRITES UI code) gains a `coder.md` STEP-9 mirror gated on `features.has_i18n` — no hardcoded user-facing strings, registry stub population, `paths.i18n_registry` in its Project Context, two new i18n red flags. `ui-design` (which produces throwaway mockups) gets a lighter note: mockup copy is realistic placeholder, the UI Element Inventory must list user-facing strings for the implementer to externalize, no registry population.
33
+
34
+ **MINOR** (corrects gate scoping behaviour + extends the capability of existing agents/skills; no new agent/skill/command, no `baldart.config.yml` key, no install/layout change).
35
+
36
+ ### Fixed / Changed
37
+
38
+ - **`framework/.claude/skills/new/references/implement.md`** — Phase 2 step 8 i18n gate replaced with the diff-scoped recipe (compute changed JS/TS files via `git diff --name-only "$TRUNK...HEAD"`, strip the trailing ` .`, lint only those files, SKIP when none); step 9 states a diff-scoped failure is always the card's own string.
39
+ - **`framework/.claude/workflows/new2.js`** + **`new-final-review.js`** — Phase-2 / Final batch i18n gate prompts now instruct diff-scoping to the card's / batch's changed files.
40
+ - **`framework/.claude/agents/qa-sentinel.md`** — i18n gate prose: diff-scope to the current review scope (card diff per-card, batch diff at Final), consistent with the normal lint gate; whole-repo reserved for the full-sweep consumers.
41
+ - **`framework/agents/i18n-protocol.md`** — SSOT: new "Scope — diff-scoped in per-change contexts, whole-repo only in full-sweep contexts" paragraph.
42
+ - **`framework/templates/baldart.config.template.yml`** + **`src/utils/i18n-gate.js`** — doc/comment clarifying `lint_command` is the full-sweep command and the per-change gates diff-scope it (no functional change).
43
+ - **`framework/.claude/agents/ui-expert.md`** — `paths.i18n_registry` + `features.has_i18n` gating in Project Context; new "i18n — No Hardcoded Strings" BLOCKING section mirroring `coder.md` STEP 9; two i18n entries in the Internationalization red flags.
44
+ - **`framework/.claude/skills/ui-design/SKILL.md`** — `features.has_i18n` note in Project Context; Step G inventory must list user-facing strings (placeholder copy, no registry population).
45
+
8
46
  ## [4.53.8] - 2026-06-19
9
47
 
10
48
  **Five `/new` refinements from a deep cost/correctness pass over the same `/new FEAT-0035` epic run that produced v4.53.4–6** (re-reading the orchestrator + all subagent + workflow transcripts of session `e502c890`). The v4.53.5 gate prose was present in this very run yet still not executed — the analysis isolated *why*, plus four other concrete wastes. Recurring theme: in two places the orchestrator ignored a discipline that already existed — so these make existing rules **executable/forcing**, they do not add new policy.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.53.8
1
+ 4.54.1
@@ -36,10 +36,18 @@ the dedicated anti-hardcoded command **in addition** to the normal lint gate.
36
36
  Resolve it: `i18n.lint_command` if set, ELSE the BALDART-owned standalone config by
37
37
  convention when `eslint.i18n.config.mjs` exists at the repo root
38
38
  (`npx eslint --config eslint.i18n.config.mjs .`). This is a separate ESLint run
39
- just for this gate, so it works even on a Biome-only toolchain. A non-zero exit is
40
- a real FAIL (a hardcoded user-facing string slipped through). If NEITHER resolves,
41
- report `i18n-lint: SKIP (no anti-hardcoded gate wired run \`baldart doctor\`)` as
42
- an informational note, never a pass do not claim the gate ran when it did not.
39
+ just for this gate, so it works even on a Biome-only toolchain. **DIFF-SCOPE it to
40
+ the current review scope** (exactly like the normal lint gate #1 below, which runs
41
+ over `<changed source files>`): strip the trailing ` .` full-sweep target from the
42
+ resolved command and run it over only the changed `*.{ts,tsx,js,jsx,mjs,cjs}` files
43
+ in scope (the card's diff per-card, the batch diff at the Final review), so
44
+ pre-existing baseline debt in untouched files never fails an unrelated change. No
45
+ changed JS/TS files in scope → SKIP. Whole-repo `.` is reserved for the full-sweep
46
+ consumers (`/i18n` audit, `/i18n-adopt`, the `i18n-align` routine) — see
47
+ `agents/i18n-protocol.md`. A non-zero exit is a real FAIL (a hardcoded user-facing
48
+ string slipped through). If NEITHER resolves, report `i18n-lint: SKIP (no
49
+ anti-hardcoded gate wired — run \`baldart doctor\`)` as an informational note,
50
+ never a pass — do not claim the gate ran when it did not.
43
51
 
44
52
  Default pre-commit gates (MUST pass before any commit verdict) — used when no
45
53
  `toolchain.commands.*` is configured:
@@ -19,12 +19,15 @@ REGISTRY "Can Edit Code: Yes" capability for ui-expert.
19
19
 
20
20
  **Reads from `baldart.config.yml`:**
21
21
  `paths.design_system`, `paths.ui_guidelines`, `paths.components_primitives`,
22
- `paths.components_root`, `paths.global_styles`,
22
+ `paths.components_root`, `paths.global_styles`, `paths.i18n_registry`,
23
23
  `identity.design_philosophy`, `identity.language`, `identity.audience_segments`.
24
24
 
25
25
  **Gated by features:** `features.has_design_system` (when `true`, the registry
26
26
  reads below are BLOCKING; when `false`, only `${paths.ui_guidelines}` is
27
- required).
27
+ required). `features.has_i18n` (when `true`, the "i18n — no hardcoded strings"
28
+ HARD RULE below is BLOCKING for any UI you write — every user-facing string goes
29
+ through `t()` and its key is registered; see `framework/agents/i18n-protocol.md`.
30
+ When `false`, that section is a no-op).
28
31
 
29
32
  **On missing/empty keys:** ask the user; do not assume defaults. See
30
33
  `framework/agents/project-context.md` § 3.
@@ -57,6 +60,37 @@ visible drift (token hardcoding, duplicate primitives across pages).
57
60
  decision from the registry + tokens-reference. If a case is not covered,
58
61
  propose a registry addition rather than inventing local rules.
59
62
 
63
+ ## i18n — No Hardcoded Strings (BLOCKING when `features.has_i18n: true`)
64
+
65
+ When you WRITE UI code (the implementer role above) and `features.has_i18n: true`,
66
+ you follow the same discipline as `coder.md` STEP 9, scoped to your UI lane. This
67
+ is your responsibility — do NOT assume a linter will catch it (the deterministic
68
+ gate runs downstream, but it is diff-scoped and may not be wired in every project).
69
+ See `framework/agents/i18n-protocol.md`.
70
+
71
+ 1. **HARD RULE (enforcement)**: NEVER write a user-facing string hardcoded in the
72
+ UI — not in JSX/template text, not in `label`/`placeholder`/`title`/`alt`/
73
+ `aria-label` props, not in toast/error/empty-state messages shown to the user.
74
+ Every such string goes through the stack's translation function
75
+ (`t('namespace.domain.key')` / `<FormattedMessage>` / equivalent). Never
76
+ concatenate translated fragments — use **ICU MessageFormat** for plurals/gender/
77
+ select. (Excluded: dev-facing errors, test fixtures, enum/const keys,
78
+ `className`, internal identifiers, URLs, technical values.)
79
+ 2. Add the new key to the source-language native locale file (the `source` value).
80
+ 3. Add a **stub entry** to the registry (`${paths.i18n_registry}`): `source`, a
81
+ **hyper-brief `context`** (what the label is for / where it appears — one line),
82
+ and `domain` (the feature area). Set `char_limit`/`icu` when relevant. The
83
+ QUALITY of `context` is curated by `doc-reviewer`; your job is to never leave the
84
+ entry missing.
85
+ 4. Follow the naming convention (`namespace.domain.key`, semantic not literal,
86
+ `global.*` for genuinely reused strings) unless an overlay overrides it.
87
+ 5. Do NOT translate into other languages (that is `i18n-translator` / the `/i18n`
88
+ skill). A hardcoded user-facing string, or a key referenced in `t()` with no
89
+ registry entry (`I18N_REGISTRY_DRIFT`), fails review — the cost of a hardcoded
90
+ label caught downstream is a redesign cycle, so externalize it as you write it.
91
+
92
+ When `features.has_i18n: false`, this section is a no-op.
93
+
60
94
  ## Your Expertise
61
95
 
62
96
  You bring deep knowledge of:
@@ -486,6 +520,11 @@ Use the Task tool to launch the `motion-expert` agent with a clear brief:
486
520
  - Fixed-width container on text content (will break DE +35% expansion)
487
521
  - String concatenation for sentence construction (breaks word order in
488
522
  many locales — use templated full sentences)
523
+ - *(when `features.has_i18n: true`)* Hardcoded user-facing string not routed
524
+ through `t()` → **HIGH** (externalize it + register the key — see the i18n
525
+ section above)
526
+ - *(when `features.has_i18n: true`)* A `t('...')` key introduced with no entry
527
+ in `${paths.i18n_registry}` → `I18N_REGISTRY_DRIFT`
489
528
 
490
529
  ## Communication Style
491
530
 
@@ -353,13 +353,25 @@
353
353
  npx tsc --noEmit > /tmp/tsc-<CARD-ID>.txt 2>&1; echo "tsc:$?" # guard: when stack.language includes "typescript"
354
354
  npm test > /tmp/test-<CARD-ID>.txt 2>&1; echo "test:$?" # if tests exist
355
355
  npm run build > /tmp/build-<CARD-ID>.txt 2>&1; echo "build:$?"
356
- # i18n anti-hardcoded gate — ONLY when features.has_i18n: true. Run i18n.lint_command
357
- # if set, else the standalone config by convention; a non-zero exit is a real FAIL
358
- # (a hardcoded user-facing string). Skip the line entirely when has_i18n is false.
359
- ${i18n.lint_command:-npx eslint --config eslint.i18n.config.mjs .} > /tmp/i18n-<CARD-ID>.txt 2>&1; echo "i18n:$?" # guard: when features.has_i18n: true AND a gate config/command exists
356
+ # i18n anti-hardcoded gate — ONLY when features.has_i18n: true. DIFF-SCOPED: lint only
357
+ # THIS card's changed *.{ts,tsx,js,jsx,mjs,cjs} files, NOT the whole repo, so pre-existing
358
+ # baseline debt in untouched files never fails this card's gate (the normal lint gate above
359
+ # is likewise diff-scoped). Whole-repo `.` is reserved for the FULL-SWEEP consumers
360
+ # (/i18n audit, /i18n-adopt, the i18n-align routine) — see agents/i18n-protocol.md.
361
+ # `$TRUNK` = the Phase-0 trunk branch. Skip the whole block when has_i18n is false.
362
+ I18N_FILES="$(git diff --name-only "$TRUNK...HEAD" 2>/dev/null || git diff --name-only HEAD~1..HEAD)"
363
+ I18N_FILES="$(printf '%s\n' "$I18N_FILES" | grep -E '\.(ts|tsx|js|jsx|mjs|cjs)$' || true)"
364
+ if [ -n "$I18N_FILES" ]; then
365
+ # Resolve i18n.lint_command (else the standalone config by convention) — the FULL-SWEEP
366
+ # command — then strip its trailing ' .' target so eslint scopes to the changed files:
367
+ I18N_GATE="${i18n.lint_command:-npx eslint --config eslint.i18n.config.mjs .}"; I18N_GATE="${I18N_GATE% .}"
368
+ printf '%s\n' "$I18N_FILES" | xargs $I18N_GATE > /tmp/i18n-<CARD-ID>.txt 2>&1; echo "i18n:$?"
369
+ else
370
+ echo "i18n:SKIP (no changed JS/TS files in this card)" # guard: features.has_i18n: true
371
+ fi
360
372
  ```
361
373
  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.
362
- 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.
374
+ 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.
363
375
  **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.
364
376
  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.
365
377
  10. If still failing after 3 retries (or on a stuck loop), log the failure in `## Issues & Flags` and ask the user before continuing.
@@ -25,6 +25,7 @@ own mockups — separating generation from scoring keeps the quality gate honest
25
25
 
26
26
  **Gated by features:**
27
27
  `features.has_design_system` (when `true`, design-system reads are BLOCKING; when `false`, skip step 1 of Prerequisites and rely on `paths.ui_guidelines` alone).
28
+ `features.has_i18n` (when `true`, the copy in your mockups is realistic **placeholder, not final** — see the i18n note in the workflow below; ref `framework/agents/i18n-protocol.md`).
28
29
 
29
30
  **Overlay:** loads `.baldart/overlays/ui-design.md` if present — extends base by default; sections marked `## [OVERRIDE] <topic>` replace base sections. See `framework/agents/project-context.md` § 5.
30
31
 
@@ -191,7 +192,7 @@ Read [references/inventory.md](references/inventory.md).
191
192
  location. (The user may override via an overlay-declared `paths.designs_dir`, but the default is
192
193
  deterministic.)
193
194
  **When invoked from `/prd` (since v3.22.0), prefix the chosen relative path with `WORKING_DIRECTORY` — see the "Working Directory" section near the top of this file.** Concrete absolute target (prd-workflow case): `<WORKING_DIRECTORY>/<paths.prd_dir>/<slug>/design.html`.
194
- 2. Extract UI Element Inventory (sprint contract).
195
+ 2. Extract UI Element Inventory (sprint contract). **When `features.has_i18n: true`**, the inventory MUST list every user-facing string the design introduces, so whoever implements it (`coder` / `ui-expert`) externalizes each via `t()` + a registry stub per `coder.md` STEP 9 / `framework/agents/i18n-protocol.md`. The mockup HTML itself is a throwaway prototype — its hardcoded copy is realistic placeholder, never the final shipped string, and this skill does NOT populate the registry (mockups are not shipped code).
195
196
  3. Update state file with chosen option, design decisions, component lists.
196
197
  4. Clean up `/tmp/prd-design-<slug>/`.
197
198
 
@@ -229,13 +229,17 @@ const codexReviewTask =
229
229
  `Run a deep code review over this wave's committed diff. The code is already written and committed — find bugs, regressions, security issues, and quality problems, per the protocol in ${protocolRef} (Phase 3.7).\n\n` +
230
230
  `${waveBrief}\n\n${baselineBrief}\n\n` +
231
231
  `Run the mandatory false-positive check on every finding and suppress the unconvincing ones. Actionability: set requires_action:false on a VERIFIED observation that needs NO change (a cleared concern — MED/LOW only; a BLOCKER/HIGH is always requires_action:true).\n\n` +
232
- `Emit your findings as your FINAL message, between these EXACT sentinels and nothing else after the opening sentinel:\n` +
232
+ `Your FINAL message MUST be ONLY the sentinel block below — NOTHING else. Do NOT write a prose report, do NOT add "Findings" / "Cleared Concerns" / "Validation" headings before or after it. Put every cleared concern as a \`requires_action:false\` entry INSIDE the JSON array. Emit EXACTLY:\n` +
233
233
  `<<<FINDINGS_JSON>>>\n` +
234
234
  `[{"finding_id":"<CARD-ID>-F###","title":"...","severity":"BLOCKER|HIGH|MEDIUM|LOW","confidence":<0-100>,"evidence":"exact file:line + code quote","minimal_fix_direction":"...","domain":"doc|security|migration|code|perf|test","requires_action":true}]\n` +
235
235
  `<<<END_FINDINGS_JSON>>>\n` +
236
236
  `Use an empty array [] between the sentinels if there are no real bugs.`
237
- const codexTaskFile = `/tmp/codextask-${cards[0].cardId}-$$.txt`
238
- const codexReviewFile = `/tmp/codexreview-wave-${cards[0].cardId}-$$.md`
237
+ // Stable per-wave names — NO `$$`: in a JS template string `$$` is the literal two chars, which the
238
+ // Write tool keeps verbatim but bash expands to a per-subshell PID → the launch/poll/extract Bash calls
239
+ // each saw a DIFFERENT path and never agreed (observed live: poll grepped a nonexistent PID-named file
240
+ // forever). cardId is already unique per wave; the file is /tmp scratch truncated by `>` on launch.
241
+ const codexTaskFile = `/tmp/codextask-${cards[0].cardId}.txt`
242
+ const codexReviewFile = `/tmp/codexreview-wave-${cards[0].cardId}.md`
239
243
  const codexPrompt =
240
244
  `You are a THIN RELAY around the Codex companion — do NOT review or investigate code yourself, do NOT grep/sed/Read source to "confirm" findings (Codex's findings are authoritative and already FP-validated; re-verifying them is wasted work). Your ONLY job: launch Codex, wait, extract its JSON, return it.\n\n` +
241
245
  `The companion script is ALREADY CONFIRMED PRESENT at:\n ${codexScriptPath}\n\n` +
@@ -243,11 +247,17 @@ const codexPrompt =
243
247
  ` 1. Write the task text (everything after "TASK PROMPT:" at the end of this message) to ${codexTaskFile} using the Write tool — do NOT inline it into the shell command (avoids quote breakage).\n` +
244
248
  ` 2. Launch Codex in the BACKGROUND (run_in_background:true — a sync run hits the Bash timeout):\n` +
245
249
  ` node "${codexScriptPath}" task "$(cat ${codexTaskFile})" --cwd "${a.worktreePath || '.'}" > ${codexReviewFile} 2>&1\n` +
246
- ` 3. POLL ${codexReviewFile} (BashOutput / repeated reads) until it contains "<<<END_FINDINGS_JSON>>>" (terminal) OR "CODEX_NOT_FOUND" OR the full 10-minute window has elapsed.\n` +
250
+ ` 3. POLL ${codexReviewFile} (BashOutput / repeated reads) until it contains ANY of these — whichever appears FIRST ends the poll (do NOT keep waiting the full window once one is present):\n` +
251
+ ` • "<<<END_FINDINGS_JSON>>>" → Codex emitted the JSON block (success path)\n` +
252
+ ` • "Turn completed." → the companion's reliable end-of-run marker — Codex FINISHED even if it wrote prose instead of the block\n` +
253
+ ` • "CODEX_NOT_FOUND" → companion missing\n` +
254
+ ` • the full 10-minute window has elapsed\n` +
247
255
  ` 4. Extract the findings with this EXACT command (deterministic — skips the [codex] trace + truncated echo, takes the LAST complete sentinel pair):\n` +
248
256
  ` awk '/<<<FINDINGS_JSON>>>/{c=1;b="";next}/<<<END_FINDINGS_JSON>>>/{c=0;last=b;next}c{b=b $0 ORS}END{printf "%s",last}' ${codexReviewFile}\n` +
249
- ` Parse that output as JSON and return it verbatim as \`findings\` (its keys already match the schema). Do NOT re-verify, re-grep, or Read any source file.\n\n` +
250
- `Set codexAvailable:false ONLY if ${codexReviewFile} contains "CODEX_NOT_FOUND" or has no "<<<END_FINDINGS_JSON>>>" after the FULL 10-minute window NEVER because a single poll returned slowly. Otherwise codexAvailable:true.\n\n` +
257
+ ` 5. Decide the return do NOT re-verify, re-grep, or Read any source file:\n` +
258
+ ` awk output is a NON-EMPTY JSON array parse it and return it verbatim as \`findings\`, codexAvailable:true (its keys already match the schema).\n` +
259
+ ` • awk output is EMPTY but the file has "Turn completed." (or "CODEX_NOT_FOUND") → Codex finished WITHOUT the sentinel block (e.g. it wrote a prose report). Return codexAvailable:false, findings:[] and EXIT NOW — do NOT wait the rest of the window, and do NOT parse findings out of the prose (a prose-only completion routes to the code-reviewer fallback; never risk dropping a real finding by guessing).\n` +
260
+ ` • none of the markers ever appeared after the FULL 10-minute window → codexAvailable:false. NEVER set false because a single poll returned slowly.\n\n` +
251
261
  `TASK PROMPT:\n${codexReviewTask}`
252
262
 
253
263
  const tcGateLines = [
@@ -185,13 +185,17 @@ const codexReviewTask =
185
185
  `Run a deep code review over this batch diff, per the /codexreview protocol in ${protocolRef} (Step F.3). The code is already written and committed — find bugs, regressions, security issues, and quality problems.\n\n` +
186
186
  `${scopeBrief}\n\n${baselineBrief}\n\n` +
187
187
  `Run the mandatory false-positive check on every finding and suppress the unconvincing ones. Actionability: set requires_action:false on a VERIFIED observation that needs NO change (a cleared concern — MED/LOW only; a BLOCKER/HIGH is always requires_action:true).\n\n` +
188
- `Emit your findings as your FINAL message, between these EXACT sentinels and nothing else after the opening sentinel:\n` +
188
+ `Your FINAL message MUST be ONLY the sentinel block below — NOTHING else. Do NOT write a prose report, do NOT add "Findings" / "Cleared Concerns" / "Validation" headings before or after it. Put every cleared concern as a \`requires_action:false\` entry INSIDE the JSON array. Emit EXACTLY:\n` +
189
189
  `<<<FINDINGS_JSON>>>\n` +
190
190
  `[{"finding_id":"<CARD-ID>-F###","title":"...","severity":"BLOCKER|HIGH|MEDIUM|LOW","confidence":<0-100>,"evidence":"exact file:line + code quote","minimal_fix_direction":"...","domain":"doc|security|migration|code|perf|test","requires_action":true}]\n` +
191
191
  `<<<END_FINDINGS_JSON>>>\n` +
192
192
  `Use an empty array [] between the sentinels if there are no real bugs.`
193
- const codexTaskFile = `/tmp/codextask-batch-${a.firstCardId || 'batch'}-$$.txt`
194
- const codexReviewFile = `/tmp/codexreview-batch-${a.firstCardId || 'batch'}-$$.md`
193
+ // Stable per-batch names — NO `$$`: in a JS template string `$$` is the literal two chars, which the
194
+ // Write tool keeps verbatim but bash expands to a per-subshell PID → the launch/poll/extract Bash calls
195
+ // each saw a DIFFERENT path and never agreed (observed live: poll grepped a nonexistent PID-named file
196
+ // forever). firstCardId is unique per batch; the file is /tmp scratch truncated by `>` on launch.
197
+ const codexTaskFile = `/tmp/codextask-batch-${a.firstCardId || 'batch'}.txt`
198
+ const codexReviewFile = `/tmp/codexreview-batch-${a.firstCardId || 'batch'}.md`
195
199
  const codexPrompt =
196
200
  `You are a THIN RELAY around the Codex companion — do NOT review or investigate code yourself, do NOT grep/sed/Read source to "confirm" findings (Codex's findings are authoritative and already FP-validated). Your ONLY job: launch Codex, wait, extract its JSON, return it.\n\n` +
197
201
  `The companion script is ALREADY CONFIRMED PRESENT at:\n ${codexScriptPath}\n\n` +
@@ -199,11 +203,17 @@ const codexPrompt =
199
203
  ` 1. Write the task text (everything after "TASK PROMPT:" at the end of this message) to ${codexTaskFile} using the Write tool — do NOT inline it into the shell command (avoids quote breakage).\n` +
200
204
  ` 2. Launch Codex in the BACKGROUND (run_in_background:true — a sync run hits the Bash timeout):\n` +
201
205
  ` node "${codexScriptPath}" task "$(cat ${codexTaskFile})" --cwd "${a.worktreePath || '.'}" > ${codexReviewFile} 2>&1\n` +
202
- ` 3. POLL ${codexReviewFile} (BashOutput / repeated reads) until it contains "<<<END_FINDINGS_JSON>>>" (terminal) OR "CODEX_NOT_FOUND" OR the full 10-minute window has elapsed.\n` +
206
+ ` 3. POLL ${codexReviewFile} (BashOutput / repeated reads) until it contains ANY of these — whichever appears FIRST ends the poll (do NOT keep waiting the full window once one is present):\n` +
207
+ ` • "<<<END_FINDINGS_JSON>>>" → Codex emitted the JSON block (success path)\n` +
208
+ ` • "Turn completed." → the companion's reliable end-of-run marker — Codex FINISHED even if it wrote prose instead of the block\n` +
209
+ ` • "CODEX_NOT_FOUND" → companion missing\n` +
210
+ ` • the full 10-minute window has elapsed\n` +
203
211
  ` 4. Extract the findings with this EXACT command (deterministic — skips the [codex] trace + truncated echo, takes the LAST complete sentinel pair):\n` +
204
212
  ` awk '/<<<FINDINGS_JSON>>>/{c=1;b="";next}/<<<END_FINDINGS_JSON>>>/{c=0;last=b;next}c{b=b $0 ORS}END{printf "%s",last}' ${codexReviewFile}\n` +
205
- ` Parse that output as JSON and return it verbatim as \`findings\` (its keys already match the schema). Do NOT re-verify, re-grep, or Read any source file.\n\n` +
206
- `Set codexAvailable:false ONLY if ${codexReviewFile} contains "CODEX_NOT_FOUND" or has no "<<<END_FINDINGS_JSON>>>" after the FULL 10-minute window NEVER because a single poll returned slowly. Otherwise codexAvailable:true.\n\n` +
213
+ ` 5. Decide the return do NOT re-verify, re-grep, or Read any source file:\n` +
214
+ ` awk output is a NON-EMPTY JSON array parse it and return it verbatim as \`findings\`, codexAvailable:true (its keys already match the schema).\n` +
215
+ ` • awk output is EMPTY but the file has "Turn completed." (or "CODEX_NOT_FOUND") → Codex finished WITHOUT the sentinel block (e.g. it wrote a prose report). Return codexAvailable:false, findings:[] and EXIT NOW — do NOT wait the rest of the window, and do NOT parse findings out of the prose (a prose-only completion routes to the code-reviewer fallback; never risk dropping a real finding by guessing).\n` +
216
+ ` • none of the markers ever appeared after the FULL 10-minute window → codexAvailable:false. NEVER set false because a single poll returned slowly.\n\n` +
207
217
  `TASK PROMPT:\n${codexReviewTask}`
208
218
 
209
219
  const docPrompt =
@@ -219,7 +229,7 @@ const apiPrompt =
219
229
  `Return findings with domain in {code, perf, migration, security}. Use the finding schema fields.`
220
230
 
221
231
  const qaPrompt =
222
- `Run MECHANICAL GATES ONLY over the batch scope, per ${protocolRef} Step F.3 (qa-sentinel row): lint, type-check, the full test suite, build, dependency audit, markdownlint, AND — when features.has_i18n is true — the i18n anti-hardcoded gate (resolve i18n.lint_command, else the standalone eslint.i18n.config.mjs by convention; non-zero exit = FAIL; neither present = SKIP, never a silent pass) as applicable to this project. Do NOT read source for code findings, do NOT emit severities — return only a PASS/FAIL/SKIP gate table.\n\nWorktree: ${a.worktreePath || '(cwd)'}\nChanged files:\n${scope.join('\n')}`
232
+ `Run MECHANICAL GATES ONLY over the batch scope, per ${protocolRef} Step F.3 (qa-sentinel row): lint, type-check, the full test suite, build, dependency audit, markdownlint, AND — when features.has_i18n is true — the i18n anti-hardcoded gate (resolve i18n.lint_command, else the standalone eslint.i18n.config.mjs by convention; non-zero exit = FAIL; neither present = SKIP, never a silent pass) as applicable to this project. DIFF-SCOPE the i18n gate to the batch's changed files listed below: strip the trailing ' .' full-sweep target from the resolved command and run it over only the *.{ts,tsx,js,jsx,mjs,cjs} files in the Changed files list, so pre-existing baseline debt in untouched files never fails the batch (whole-repo \`.\` is only for /i18n, /i18n-adopt, and the i18n-align routine). Do NOT read source for code findings, do NOT emit severities — return only a PASS/FAIL/SKIP gate table.\n\nWorktree: ${a.worktreePath || '(cwd)'}\nChanged files:\n${scope.join('\n')}`
223
233
 
224
234
  // F-041 — single-card batch: a per-card review already covered these exact files and a
225
235
  // 1-card batch has NO cross-card conflict to surface, so the duplicate Claude finders can be
@@ -238,7 +238,7 @@ const projectBrief = [
238
238
  ? `Toolchain commands (run THESE verbatim for the baseline + any gate, per agents/toolchain-protocol.md; empty → project default): ${JSON.stringify(cfg.toolchain.commands)}.`
239
239
  : '',
240
240
  features.has_i18n
241
- ? `i18n (features.has_i18n=true, per agents/i18n-protocol.md): the coder MUST NOT hardcode any user-facing string — route every label through the stack t() function and apply coder.md STEP 9 (add the key + a hyper-brief context to the registry at ${paths.i18n_registry || 'paths.i18n_registry'}). The Phase-2 gate MUST run the anti-hardcoded gate IN ADDITION to lint: \`${(cfg.i18n && cfg.i18n.lint_command) || 'npx eslint --config eslint.i18n.config.mjs .'}\` (resolve i18n.lint_command, else the standalone eslint.i18n.config.mjs by convention; a non-zero exit FAILS the gate; if neither exists report i18n-lint SKIP, never a silent pass).`
241
+ ? `i18n (features.has_i18n=true, per agents/i18n-protocol.md): the coder MUST NOT hardcode any user-facing string — route every label through the stack t() function and apply coder.md STEP 9 (add the key + a hyper-brief context to the registry at ${paths.i18n_registry || 'paths.i18n_registry'}). The Phase-2 gate MUST run the anti-hardcoded gate IN ADDITION to lint: resolve i18n.lint_command (else the standalone eslint.i18n.config.mjs by convention) — base command \`${(cfg.i18n && cfg.i18n.lint_command) || 'npx eslint --config eslint.i18n.config.mjs .'}\` but DIFF-SCOPED: strip the trailing ' .' full-sweep target and run it over ONLY this card's changed *.{ts,tsx,js,jsx,mjs,cjs} files (\`git diff --name-only TRUNK...HEAD | grep -E '\\.(ts|tsx|js|jsx|mjs|cjs)$'\`), so pre-existing baseline debt in untouched files never fails this card. No changed JS/TS files → SKIP. A non-zero exit FAILS the gate (a string THIS card introduced); if neither command nor config exists report i18n-lint SKIP, never a silent pass. Whole-repo \`.\` is only for /i18n, /i18n-adopt, and the i18n-align routine.`
242
242
  : '',
243
243
  FLAGS.effort ? `Reasoning effort for this run: ${FLAGS.effort}.` : '',
244
244
  ].filter(Boolean).join('\n')
@@ -81,6 +81,21 @@ Translations are NOT stored here — they live in the native locale files.
81
81
  genuine, 0 technical-attribute false positives). Runners: `qa-sentinel` (for `/new`, `/qa`),
82
82
  the classic `/new` Phase-2 gate, the `new2` Phase-2 gate, and the `i18n-align`
83
83
  routine (pre-commit). This is the HARD enforcement for JSX-rendered strings.
84
+
85
+ **Scope — diff-scoped in per-change contexts, whole-repo only in full-sweep
86
+ contexts.** `i18n.lint_command` (with its trailing ` .`) is the canonical
87
+ **full-sweep** command. The **per-change gates** — `/new` per-card (Phase 2 step 8),
88
+ the `new2` Phase-2 gate, the Final review batch gate, and `qa-sentinel` — run it
89
+ **DIFF-SCOPED**: they strip the trailing ` .` target and lint only the changed
90
+ `*.{ts,tsx,js,jsx,mjs,cjs}` files in the change's diff (the card's diff per-card,
91
+ the batch's diff at the Final review), exactly as the normal lint gate is already
92
+ diff-scoped. This way pre-existing baseline debt in files an unrelated change did
93
+ not touch never fails that change's gate; the Final batch gate (scoped to the
94
+ union of the batch's changed files) is the merge backstop. The **full-sweep
95
+ contexts** — the `/i18n` audit, the `/i18n-adopt` migration, and the `i18n-align`
96
+ routine — run whole-repo (`.`) by design, because they reconcile the entire
97
+ codebase, not a single change. A diff-scoped gate failure is therefore always a
98
+ string the change itself introduced — never baseline noise to triage.
84
99
  2. **Semantic backstop (review)** — non-JSX imperative strings are OUTSIDE the
85
100
  linter's deterministic reach, so `code-reviewer` flags them as **HIGH**,
86
101
  **syntactically scoped**:
@@ -375,9 +375,13 @@ i18n:
375
375
  # doctor` set this to a STANDALONE ESLint run BALDART owns
376
376
  # (`npx eslint --config eslint.i18n.config.mjs .`) — it works on Biome projects
377
377
  # too (a dedicated ESLint invocation just for this gate, never touching your main
378
- # lint config). The gate runners (qa-sentinel, /new2, the i18n-align routine) run
379
- # it verbatim; a non-zero exit FAILS the gate. Empty + no config file present →
380
- # the gate reports SKIP (never a silent pass).
378
+ # lint config). This is the FULL-SWEEP command: the full-sweep runners (/i18n,
379
+ # /i18n-adopt, the i18n-align routine) run it verbatim over the whole repo, while
380
+ # the per-change gates (/new per-card, /new2 Phase-2, the Final review) DIFF-SCOPE
381
+ # it — they strip the trailing ` .` and lint only the change's changed JS/TS files,
382
+ # so pre-existing baseline debt never fails an unrelated card. A non-zero exit
383
+ # FAILS the gate. Empty + no config file present → the gate reports SKIP (never a
384
+ # silent pass).
381
385
  lint_command: ""
382
386
 
383
387
  # ─── GIT ─────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.53.8",
3
+ "version": "4.54.1",
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"
@@ -31,6 +31,12 @@ const FLAT_CONFIG = 'eslint.i18n.config.mjs';
31
31
  const LEGACY_CONFIG = '.eslintrc.i18n.json';
32
32
  const MODE = 'jsx-only';
33
33
 
34
+ // These are the FULL-SWEEP commands (trailing ` .` = whole repo). The full-sweep
35
+ // consumers (/i18n, /i18n-adopt, the i18n-align routine) run them verbatim. The
36
+ // per-change gates (/new per-card, /new2 Phase-2, the Final review, qa-sentinel)
37
+ // DIFF-SCOPE them at runtime — they strip the trailing ` .` and append only the
38
+ // change's changed JS/TS files — so pre-existing baseline debt never fails an
39
+ // unrelated card (see agents/i18n-protocol.md § "BLOCKING anti-hardcoded cascade").
34
40
  const FLAT_COMMAND = `npx eslint --config ${FLAT_CONFIG} .`;
35
41
  const LEGACY_COMMAND = `npx eslint --no-eslintrc -c ${LEGACY_CONFIG} --ext .js,.jsx,.ts,.tsx .`;
36
42
  // Default advertised command (flat / ESLint 9). `resolveLintCommand` picks the