baldart 4.53.7 → 4.54.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.54.0] - 2026-06-19
9
+
10
+ **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.)
11
+
12
+ 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).
13
+ 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.
14
+ 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.
15
+
16
+ **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).
17
+
18
+ ### Fixed / Changed
19
+
20
+ - **`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.
21
+ - **`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.
22
+ - **`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.
23
+ - **`framework/agents/i18n-protocol.md`** — SSOT: new "Scope — diff-scoped in per-change contexts, whole-repo only in full-sweep contexts" paragraph.
24
+ - **`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).
25
+ - **`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.
26
+ - **`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).
27
+
28
+ ## [4.53.8] - 2026-06-19
29
+
30
+ **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.
31
+
32
+ 1. **Empty-result gate: the "read the rested agent's LAST event" step is now executable for background teammates.** v4.53.5 told the orchestrator to classify rate-limit-death vs genuine-empty-result by reading the dead teammate's last event — but a background teammate killed mid-flight delivers *nothing* to the orchestrator, so "no report + no diff" is the identical observable for both causes. In this run the orchestrator therefore classified the two rate-limit deaths (03, 07) as *generic empty-result* and re-fired both within 16s (same burst), the exact anti-pattern v4.53.5 forbids. The gate now spells out HOW: the failure signature lives ONLY in the teammate's on-disk transcript (`<session-subagents-dir>/agent-*.jsonl`, matched by label via the sibling `*.meta.json`) — locate + `tail` it, look for `isApiErrorMessage`/`Rate limited`/`Server is temporarily limiting requests`/`(not your usage limit)`, and STATE the classification + evidence before choosing the remedy. (`new2` is unaffected — its F-019 workflow runtime surfaces transient errors to the script directly, no disk dig needed.)
33
+ 2. **No more SendMessage-to-fetch-a-report.** Same gate locator is reused for report retrieval: when a teammate rests without its final message captured, `SendMessage`-ing it forces a full-context **resume** (~10–15M tokens reloaded just to re-emit a report) — observed twice on the L0 coders in this run, then ignored. Step C now forbids it: the completion-report is already the teammate's last transcript message; read it from disk for free.
34
+ 3. **Per-wave fix-agent scope discipline.** The wave-3 "apply ALL verified findings" coder ran away to 215 turns / ~32M tokens (the single most expensive agent in the run) by re-surveying the architecture. `new-card-review.js` `applyFixPass` brief now mandates going DIRECTLY to each finding's cited `evidence` file:line, finding-by-finding, no re-mapping the codebase (a verified finding is a precise instruction, not a research prompt).
35
+ 4. **grep binary-heuristic false-empty added to the grep-verification discipline.** The orchestrator burned ~6 turns when plain `grep` treated an 8000-line accented `data.ts` as *binary* and returned 0 matches for present tokens. The discipline (completeness.md) gains a third failure mode: prefer the already-mandated `rg`; if you must use `grep`, `grep -a`; a 0-match from plain `grep` on a known-modified file is almost always this artifact.
36
+ 5. **Codex companion path resolved once per run, passed via workflow args.** Both review workflows spawned a Haiku resolver agent per invocation (4× in this run). They now accept `args.codexScriptPath` (resolved once by the orchestrator, threaded at all three delegation sites) and **fall back to the in-workflow resolver when absent** — fully backward-compatible.
37
+
38
+ **PATCH** (refinements to `/new` orchestration + one optional, fallback-guarded workflow arg; no new agent/skill/command, no `baldart.config.yml` key, no install/layout change).
39
+
40
+ ### Fixed / Changed
41
+
42
+ - **`framework/.claude/skills/new/references/team-mode.md`** — empty-result gate: concrete on-disk transcript locator + rate-limit signatures + mandate to state the classification (Int 1 of the pass); Step C forbids `SendMessage`-to-fetch-report and points at the same locator (Int 2); Step-D delegation args gain `codexScriptPath` with a resolve-once glob (Int 5).
43
+ - **`framework/.claude/skills/new/references/completeness.md`** — Grep-verification discipline: "Two failure modes" → "Three", adds the binary-heuristic false-empty bullet (`rg`/`grep -a`).
44
+ - **`framework/.claude/workflows/new-card-review.js`** — `fixBrief` gains a SCOPE DISCIPLINE clause (go to the cited file:line, no re-survey); accepts `args.codexScriptPath` (caller-provided) with Haiku-resolver fallback.
45
+ - **`framework/.claude/workflows/new-final-review.js`** — accepts `args.codexScriptPath` with Haiku-resolver fallback.
46
+ - **`framework/.claude/skills/new/references/review-cycle.md`** + **`final-review.md`** — the two remaining `Workflow({...})` delegation sites pass `codexScriptPath` (resolve-once, omit-if-empty).
47
+
8
48
  ## [4.53.7] - 2026-06-19
9
49
 
10
50
  **`setup-worktree.sh` no longer turns an empty `toolchain.commands.build` into a spurious `baseline: fail`.** Observed on a real `/new FEAT-0037` run in a consumer (`mayo`) whose `baldart.config.yml` declares `build: ''` (single-quoted empty scalar — the project's build is not a separate toolchain gate; per `toolchain-protocol.md` an empty command must fall back to the default `npm run build`). The deterministic SSOT worktree script (v4.53.0) read that value with a sed that stripped only DOUBLE quotes (`\"?([^\"#]*)\"?`), so `build: ''` came through as the **literal 2-char string `''`** — non-empty — so the `[ -n "$TC_BUILD" ]` fallback never fired and the script ran `bash -c "''"`, an empty *quoted* command → `bash: : command not found` (exit 127) → `baseline: fail`. tsc + biome had actually passed and the worktree was green on disk; the failure was entirely the unresolved empty scalar. Fix: (1) `_tc()` now resolves a YAML scalar to its true value — `build:`, `build: ''`, `build: ""`, `build: ~`, `build: null` all collapse to empty so the default fallback fires (strips one layer of matching single OR double quotes + YAML null markers + inline `# comment`); (2) the script now tracks build provenance and implements the protocol's **SKIP tier** — the *fallback* `npm run build` on a project with no `build` npm script is a no-build library → SKIP (baseline stays pass), never a spurious fail, while a *configured* (non-empty) build command is always run as a real gate. Verified by executing the resolver against every empty-scalar form + real values, the fallback/SKIP decision on a has-build vs no-build `package.json`, and reproducing the exact `bash: : command not found` exit-127 from the old sed. The prose inline fallbacks (`setup.md`, `worktree-manager/SKILL.md`, `new2.js`) are model-driven and never had the sed bug — no parallel fix needed. **PATCH** (deterministic-script bugfix; correctly-configured consumers see no behaviour change; no new agent/skill/command/config key, no install change).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.53.7
1
+ 4.54.0
@@ -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
 
@@ -6,9 +6,10 @@
6
6
 
7
7
  Before triggering any review, you MUST verify that the coder agent implemented **every requirement and acceptance criterion** in the backlog card. This is a blocking gate — do NOT proceed to Phase 3 with unimplemented items.
8
8
 
9
- > **Grep-verification discipline (MUST — applies to every grep check in this phase: step 0, step 4, the `data_fields` checks).** The completeness checks below confirm an item by grepping the changed files for a token. Two failure modes turn a *false* "not found" into a *false* "Missing/Partial":
9
+ > **Grep-verification discipline (MUST — applies to every grep check in this phase: step 0, step 4, the `data_fields` checks).** The completeness checks below confirm an item by grepping the changed files for a token. Three failure modes turn a *false* "not found" into a *false* "Missing/Partial":
10
10
  > 1. **Quote-broken pattern.** Code-shaped tokens contain shell/regex metacharacters — a TS union literal `mode:'catalog'|'picker'`, an object key, anything with `'` `"` `|` `[]` `?` `(`. Embedding such a pattern in an `echo "=== … ===" && grep …` block or an unquoted `-E` arg breaks the shell/regex parse → the grep matches **nothing even though the code is there**. **Search literals with `rg -F '<exact string>'` (fixed-string, no regex), single-quote the WHOLE pattern, and run ONE pattern per `rg` call** — never fold the search token into a quoted header/echo composition.
11
11
  > 2. **`0 matches` ≠ absent.** A zero-match result is ambiguous (true miss vs. broken pattern / wrong file / wrong escaping). Do NOT classify an item `Missing`/`Partial` on a 0-match alone. **If the 0-match CONTRADICTS the coder completion report's claim that the item is implemented, treat it as a likely broken grep — re-confirm by reading the report's cited `file:line` (or a corrected `rg -F`) BEFORE flagging or spawning a gap-fix agent.** Never re-spawn a coder on a 0-match alone. When unsure, a targeted `Read` at the report's `file:line` is more reliable than a hand-rolled grep.
12
+ > 3. **Binary-heuristic false-empty.** Plain `grep` decides a file is *binary* when it sees a byte it reads as non-text (a non-UTF-8 byte, an accented char under some locales) or very long lines, and then prints `Binary file … matches` / **nothing** instead of the line — so a perfectly valid source file (e.g. an 8000-line `data.ts` with accented strings) silently returns 0 matches for tokens that ARE present. The mandated `rg` does NOT have this failure mode (it searches UTF-8 and long lines fine). **Always reach for `rg -F` first; if you must use `grep`, force text mode with `grep -a`.** A 0-match from plain `grep` on a file you KNOW was just modified is almost always this artifact — switch to `rg`/`grep -a` and re-check before concluding the code is missing.
12
13
 
13
14
  **Step-by-step**:
14
15
 
@@ -92,7 +92,8 @@ that is a **gate violation**: log it as
92
92
  config, // the parsed baldart.config.yml
93
93
  singleCard: cardPaths.length === 1, // N=1 batch — enables the F-041 per-finder slim
94
94
  slimDoc: cardPaths.length === 1 && <Phase-3 doc-review RAN for this card, NOT deferred>,
95
- slimApi: false // api-perf NEVER runs per-card in classic /new → final is its ONLY run; never slim it
95
+ slimApi: false, // api-perf NEVER runs per-card in classic /new → final is its ONLY run; never slim it
96
+ codexScriptPath // resolved ONCE per run (glob in team-mode.md § Step D); omit if empty → workflow self-resolves
96
97
  }})
97
98
  ```
98
99
 
@@ -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.
@@ -46,9 +46,10 @@ so it surfaces in telemetry.
46
46
  ```
47
47
  Workflow({ name: 'new-card-review', args: {
48
48
  cards: [{ cardId, cardPath, scopeFiles, editableFiles, qaTier, hasSecurityFiles, runSimplify, archBaselinePath }],
49
- worktreePath, baseBranch, config // from Phase 0 / tracker
49
+ worktreePath, baseBranch, config, codexScriptPath // from Phase 0 / tracker
50
50
  }})
51
51
  ```
52
+ `codexScriptPath` — resolve the Codex companion path ONCE per `/new` run (glob in `references/team-mode.md` § Step D delegation) and reuse it here + at the Final, so the workflow skips its internal Haiku resolver; omit when stdout is empty (the workflow self-resolves as fallback).
52
53
 
53
54
  The workflow runs Simplify + Codex (agent-launched, code-reviewer fallback) + qa-sentinel + security,
54
55
  FP-checks each specialist's own findings, then the **domain writer applies its VERIFIED findings**
@@ -138,7 +138,7 @@ The orchestrator waits for ALL background agents in the group to complete. It wi
138
138
  > **Do NOT poll with `sleep N; echo "waiting..."` loops.** Background agents (and background `Bash` commands) re-invoke the orchestrator automatically when they finish — issuing `sleep`/`echo` "waiting" turns burns orchestrator turns and tokens for zero progress. Simply end your turn after spawning the background work; you will be woken on completion. This applies to EVERY barrier in Step D (D.1 build, D.2 reviewers, D.3/D.3b agents, D.4 qa-sentinel, D.4a doc-reviewer, D.4b `/codexreview`), not just Step C.
139
139
 
140
140
  For each completed agent:
141
- 1. Read the completion report from the agent's output.
141
+ 1. Read the completion report from the agent's returned output. **If the output did not arrive as content (the teammate went idle / "rested" without you capturing its final message), do NOT `SendMessage` the teammate to ask for the report** — messaging a rested background teammate forces a full-context **resume** that reprocesses its entire transcript (~10–15M tokens reloaded) just to re-emit a report you can read for free. The completion-report block is already the teammate's last message in its on-disk transcript: read it with the SAME locator as the empty-result gate below (`tail`/`grep 'completion-report'` the `<session-subagents-dir>/agent-*.jsonl` that matches the label). Disk is the source of truth either way.
142
142
  2. Log to tracker: card ID, status, files changed, build/lint status.
143
143
  3. Do NOT read or store implementation details — only the verdict.
144
144
 
@@ -146,8 +146,11 @@ For each completed agent:
146
146
  - **A Step-7 completion report was emitted.** A teammate that rested with **no completion report = empty-result failure.** (Do not infer "done" from the rest event alone.)
147
147
  - **Its `files_changed` actually changed in the worktree** — `git status --porcelain` intersected with that card's File Ownership Map is non-empty and covers the claimed paths (use the grep-verification discipline above — never a quote-broken check). **Zero changes in the card's ownership files = empty-result failure; report claims files that are not on disk = fabrication.**
148
148
  - A genuinely legitimate no-op card (its AC turned out already satisfied) is the rare exception: confirm it from the report's per-requirement `evidence` pointing at PRE-EXISTING code, and log it explicitly as `no-op (AC pre-satisfied)` — never assume zero-diff means success.
149
- **Classify the CAUSE before re-spawning — read the rested agent's LAST event:**
150
- - **Transient infra failure (the common cause of a silent empty rest in a WIDE parallel wave).** The agent's last output is an API error / `Rate limited` / overload / `(not your usage limit)` i.e. the background teammate was **killed mid-flight by API rate-limiting** (it had done real work — reads, a plan — then died), NOT a model fabrication or "decided nothing to do". The runtime does NOT auto-resume a dead teammate, so it rests with no report + no diff. **Re-spawn it, but STAGGERED with backoff — do NOT re-fire several transient-failed agents in the same parallel burst** (that re-saturates the API and kills them again). Space them out (one at a time, or after a brief pause). This still costs a fresh run (the killed agent's reads are lost — the runtime cannot resume it), so **prevention beats recovery**: if a wave repeatedly hits rate limits, NARROW the parallel fan-out width for that wave (spawn fewer coders at once) rather than re-firing the full wave. A transient failure does NOT consume the Step-B genuine-failure budget.
149
+ **Classify the CAUSE before re-spawning — you MUST read the rested agent's LAST event, never infer the cause from "no report" alone.** A background teammate killed mid-flight dies WITHOUT delivering anything to you, so its failure signature lives ONLY in its on-disk transcript — "no report + no diff" is the SAME observable for a rate-limit death and a genuine empty-result, and they need OPPOSITE remedies. Before choosing one, locate and tail that transcript:
150
+ - Subagent transcripts live at `<session-subagents-dir>/agent-*.jsonl`, each with a sibling `agent-*.meta.json` whose `agentType` equals the teammate label. Resolve the file and read its tail, e.g.:
151
+ `D="$(ls -dt ~/.claude/projects/*/${CLAUDE_SESSION_ID:-*}/subagents 2>/dev/null | head -1)"; f="$(grep -l '"<teammate-label>"' "$D"/*.meta.json 2>/dev/null | head -1 | sed 's/\.meta\.json$/.jsonl/')"; tail -c 4000 "$f"`
152
+ (if `$CLAUDE_SESSION_ID` is unset, the most recent `subagents/` dir under this project is the live session's.) If the transcript truly cannot be located, fall back to whatever the `Agent` tool result surfaced — but do NOT default to "genuine empty-result" without having looked. **State the classification you reached + the evidence in your narration** (e.g. "03 last event = `Rate limited` → transient").
153
+ - **Transient infra failure (the common cause of a silent empty rest in a WIDE parallel wave).** The agent's last event is an API error — signatures: `isApiErrorMessage:true`, `Rate limited`, `Server is temporarily limiting requests`, `overload`, `(not your usage limit)` — i.e. the background teammate was **killed mid-flight by API rate-limiting** (it had done real work — reads, a plan — then died), NOT a model fabrication or "decided nothing to do". The runtime does NOT auto-resume a dead teammate, so it rests with no report + no diff. **Re-spawn it, but STAGGERED with backoff — do NOT re-fire several transient-failed agents in the same parallel burst** (that re-saturates the API and kills them again). Space them out (one at a time, or after a brief pause). This still costs a fresh run (the killed agent's reads are lost — the runtime cannot resume it), so **prevention beats recovery**: if a wave repeatedly hits rate limits, NARROW the parallel fan-out width for that wave (spawn fewer coders at once) rather than re-firing the full wave. A transient failure does NOT consume the Step-B genuine-failure budget.
151
154
  - **Genuine empty-result / fabrication.** The agent rested CLEANLY (no error in its last event), with no completion report and no diff. THIS is the model-fault case: take the one Step-B re-spawn below (same cap), re-briefing the coder with an explicit "write your ownership files and confirm them on disk before reporting done" mandate. A second empty result → `AskUserQuestion` (skip/abandon) — do not re-spawn a third time.
152
155
 
153
156
  **If an agent fails** (status: failed after 3 retries — the central repair cap):
@@ -204,9 +207,13 @@ After ALL agents in the group complete successfully:
204
207
  ```
205
208
  Workflow({ name: 'new-card-review', args: {
206
209
  cards: [ …one entry per non-trivial card in the wave… ],
207
- worktreePath, baseBranch, config
210
+ worktreePath, baseBranch, config, codexScriptPath
208
211
  }})
209
212
  ```
213
+ `codexScriptPath` — resolve the Codex companion path ONCE per `/new` run and reuse the cached value
214
+ for every wave + the Final (so the workflow skips its internal Haiku resolver). Resolve with:
215
+ `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`
216
+ (empty stdout → omit the arg; the workflow falls back to its own resolver — never block on this).
210
217
  The workflow fans out the finders per card, runs ONE Codex pass + ONE qa-sentinel (group max tier)
211
218
  over the union, and the **domain writer applies all VERIFIED fixes for the whole group** (canonical
212
219
  writer map v4.26.1: `security` → `security-reviewer`, then `code`/`perf`/`migration`/`test`/`simplify`
@@ -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
 
@@ -197,15 +197,23 @@ const PREFLIGHT_SCHEMA = {
197
197
  properties: { codexScriptPath: { type: 'string', description: 'absolute path to codex-companion.mjs, or "" if none' } },
198
198
  }
199
199
  let codexScriptPath = ''
200
- try {
201
- const pf = await agent(
202
- `Resolve the Codex companion script path. Run EXACTLY this one Bash command and nothing else:\n` +
203
- " 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\n" +
204
- `Return codexScriptPath = the trimmed stdout (a single absolute path), or "" if the command printed nothing. Do NOT run Codex, do NOT read any other file, do NOT reason about availability — just report the command's stdout.`,
205
- { label: 'codex-preflight', phase: 'Discovery', model: 'haiku', schema: PREFLIGHT_SCHEMA }
206
- )
207
- codexScriptPath = (pf && typeof pf.codexScriptPath === 'string') ? pf.codexScriptPath.trim() : ''
208
- } catch (_) { codexScriptPath = '' }
200
+ // Caller-provided path (F-???: the orchestrator resolves it ONCE and passes it in args, so we don't
201
+ // spawn a Haiku resolver per workflow invocation). Falls back to the in-workflow resolver when absent,
202
+ // so consumers/skills that don't pass it yet keep working unchanged.
203
+ if (typeof a.codexScriptPath === 'string' && /codex-companion\.mjs$/.test(a.codexScriptPath.trim())) {
204
+ codexScriptPath = a.codexScriptPath.trim()
205
+ log('Codex companion path received from caller args (resolver agent skipped).')
206
+ } else {
207
+ try {
208
+ const pf = await agent(
209
+ `Resolve the Codex companion script path. Run EXACTLY this one Bash command and nothing else:\n` +
210
+ " 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\n" +
211
+ `Return codexScriptPath = the trimmed stdout (a single absolute path), or "" if the command printed nothing. Do NOT run Codex, do NOT read any other file, do NOT reason about availability — just report the command's stdout.`,
212
+ { label: 'codex-preflight', phase: 'Discovery', model: 'haiku', schema: PREFLIGHT_SCHEMA }
213
+ )
214
+ codexScriptPath = (pf && typeof pf.codexScriptPath === 'string') ? pf.codexScriptPath.trim() : ''
215
+ } catch (_) { codexScriptPath = '' }
216
+ }
209
217
  const codexResolved = /codex-companion\.mjs$/.test(codexScriptPath)
210
218
  log(codexResolved
211
219
  ? `Codex companion resolved deterministically: ${codexScriptPath}`
@@ -422,6 +430,7 @@ async function applyFixPass(findings, writer, label, role) {
422
430
  `You MAY edit ONLY these files (ownership map — touching anything else is a violation):\n${unionEditable.join('\n')}\n\n` +
423
431
  `Findings to fix (fix the code, not the tests unless a test itself is wrong; do NOT expand scope beyond the finding):\n` +
424
432
  findings.map((f) => `- [${f.finding_id}] (${f.card || '?'} / ${f.domain} / ${f.severity}) ${f.title}\n evidence: ${f.evidence}\n direction: ${f.minimal_fix_direction}`).join('\n') +
433
+ `\n\nSCOPE DISCIPLINE (do not run away — every finding already cites the exact site to fix): go DIRECTLY to each finding's \`evidence\` file:line and apply its \`direction\`. Do NOT re-map the architecture, do NOT re-survey the codebase, do NOT read files beyond the cited evidence + their immediate dependencies. Work finding-by-finding (and card-by-card when findings span several cards) — a verified finding is a precise instruction, not a research prompt. Open only what each fix needs.` +
425
434
  `\n\nAfter applying: run \`${tc('lint', 'npm run lint')}\` and (when the project uses typescript) \`${tc('typecheck', 'npx tsc --noEmit')}\` and \`${tc('build', 'npm run build')}\` in the worktree. If a check fails because of an edit you made, fix the regression — at most 2 retries — staying within the allowed files. ` +
426
435
  `Do NOT commit. Do NOT git stash (refs/stash is shared across worktrees). ` +
427
436
  `Return: applied (finding_ids you fixed — list a finding here if you MADE the edit, even when the build stays red for reasons OUTSIDE this finding / your ownership, e.g. a known cross-wave cascade; the build status is reported separately in checks), unresolved (finding_ids you could NOT fix within the allowed files / 2 retries — i.e. the EDIT itself could not be made), and checks (PASS/FAIL/SKIP for lint, tsc, build).`
@@ -153,15 +153,23 @@ const PREFLIGHT_SCHEMA = {
153
153
  properties: { codexScriptPath: { type: 'string', description: 'absolute path to codex-companion.mjs, or "" if none' } },
154
154
  }
155
155
  let codexScriptPath = ''
156
- try {
157
- const pf = await agent(
158
- `Resolve the Codex companion script path. Run EXACTLY this one Bash command and nothing else:\n` +
159
- " 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\n" +
160
- `Return codexScriptPath = the trimmed stdout (a single absolute path), or "" if the command printed nothing. Do NOT run Codex, do NOT read any other file, do NOT reason about availability — just report the command's stdout.`,
161
- { label: 'codex-preflight', phase: 'Review', model: 'haiku', schema: PREFLIGHT_SCHEMA }
162
- )
163
- codexScriptPath = (pf && typeof pf.codexScriptPath === 'string') ? pf.codexScriptPath.trim() : ''
164
- } catch (_) { codexScriptPath = '' }
156
+ // Caller-provided path: the orchestrator resolves codex-companion.mjs ONCE and passes it in args, so we
157
+ // skip the per-invocation Haiku resolver. Falls back to the in-workflow resolver when absent, so
158
+ // consumers/skills that don't pass it yet keep working unchanged.
159
+ if (typeof a.codexScriptPath === 'string' && /codex-companion\.mjs$/.test(a.codexScriptPath.trim())) {
160
+ codexScriptPath = a.codexScriptPath.trim()
161
+ log('Codex companion path received from caller args (resolver agent skipped).')
162
+ } else {
163
+ try {
164
+ const pf = await agent(
165
+ `Resolve the Codex companion script path. Run EXACTLY this one Bash command and nothing else:\n` +
166
+ " 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\n" +
167
+ `Return codexScriptPath = the trimmed stdout (a single absolute path), or "" if the command printed nothing. Do NOT run Codex, do NOT read any other file, do NOT reason about availability — just report the command's stdout.`,
168
+ { label: 'codex-preflight', phase: 'Review', model: 'haiku', schema: PREFLIGHT_SCHEMA }
169
+ )
170
+ codexScriptPath = (pf && typeof pf.codexScriptPath === 'string') ? pf.codexScriptPath.trim() : ''
171
+ } catch (_) { codexScriptPath = '' }
172
+ }
165
173
  const codexResolved = /codex-companion\.mjs$/.test(codexScriptPath)
166
174
  log(codexResolved
167
175
  ? `Codex companion resolved deterministically: ${codexScriptPath}`
@@ -211,7 +219,7 @@ const apiPrompt =
211
219
  `Return findings with domain in {code, perf, migration, security}. Use the finding schema fields.`
212
220
 
213
221
  const qaPrompt =
214
- `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')}`
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. 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')}`
215
223
 
216
224
  // F-041 — single-card batch: a per-card review already covered these exact files and a
217
225
  // 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.7",
3
+ "version": "4.54.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"
@@ -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