baldart 4.53.8 → 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,26 @@ 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
+
8
28
  ## [4.53.8] - 2026-06-19
9
29
 
10
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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.53.8
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
 
@@ -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
 
@@ -219,7 +219,7 @@ const apiPrompt =
219
219
  `Return findings with domain in {code, perf, migration, security}. Use the finding schema fields.`
220
220
 
221
221
  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')}`
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')}`
223
223
 
224
224
  // F-041 — single-card batch: a per-card review already covered these exact files and a
225
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.8",
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