baldart 4.43.1 → 4.46.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,50 @@ 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.46.0] - 2026-06-15
9
+
10
+ **Worktree env-file copy is unified onto one stack-agnostic config key, `stack.env_files` — closing the v4.42.0-deferred divergence (`/nw` copied `.env.local`+`.env`; new2's pre-flight copied `env/.env.local/.env.example/supabase/.temp`) WITHOUT the superset the adversarial pass had refuted.** A worktree is a fresh checkout, so the gitignored env artifacts a build needs must be copied from main — but the SET was hard-coded and divergent across paths. This release makes the set a single SSOT list (`stack.env_files`, default `['.env.local', '.env']`) read identically by `worktree-manager` (`/nw`), `/new`, and `new2`. A 3-skeptic **adversarial review before implementation** shaped the design: it (1) confirmed `framework/agents/runbook.md:36` (`cp .env.example .env`) is a documentation-template onboarding idiom — a DIFFERENT context — and **excluded** it; (2) refuted folding `supabase/.temp` into the copy set (it carries the remote project-ref → every copying worktree auto-links to the shared remote, the exact footgun `stack.schema_deploy_from_trunk_only` exists to prevent, and worse unattended in `/new` than in manual `/nw`; it is not even a build input); and (3) found the bash bug class fixed below. Crucially, the layer is **stack-agnostic**: the generic skill/installer/template never name Supabase or any stack — copying a tool's local-state directory is a per-project opt-in the user adds to their own `stack.env_files` / overlay, never a framework default. **MINOR** (additive: a new `baldart.config.yml` key, propagated end-to-end per the schema-change rule; no removed surface).
11
+
12
+ ### Added
13
+
14
+ - **`stack.env_files` config key** (`framework/templates/baldart.config.template.yml`) — list of gitignored env artifacts (files OR dirs) copied into each worktree, default `['.env.local', '.env']`. Files are copied with `cp` (dereferences symlinks — never `cp -P`, which would dangle a relative env symlink inside `.worktrees/`), directories with `cp -r` (mirrored: stale files removed on a `/nw` resume). A missing FILE is WARNed at copy time (never `exit 1` — that would strand `/new`'s programmatic path; the build gate is the real enforcement); nothing-copied escalates to a loud WARN. Replaces the old `cp … 2>/dev/null || true` that silently swallowed a missing critical env → cryptic later build failure.
15
+
16
+ ### Changed
17
+
18
+ - **`framework/.claude/skills/worktree-manager/SKILL.md` — `/nw` step 4 copy loop reads `stack.env_files`** (file/dir-aware, WARN-not-fatal). The dev-server PORT is now written to the first FILE actually copied (not `ENV_FILES[0]`, which could be a directory — `echo >> dir` errors), falling back to `.env.local`. The env-sync staleness check (`/mw` step 2) compares the NEWEST mtime among all `stack.env_files` (portable `stat -f %m || stat -c %Y`, recursive for dirs) instead of only `.env.local`; the `/lw` status line, the port-reuse rule, the docs-mode "forbidden" list, and the Project-Context header are reconciled to the list. The skill stays **stack-agnostic** — it iterates the list and never names a stack.
19
+ - **`framework/.claude/workflows/new2.js` + `framework/.claude/skills/new/references/setup.md`** — the pre-flight worktree briefing stops hard-coding `env/.env.local/.env.example/supabase/.temp` and copies `stack.env_files` instead (new2 interpolates the list resolved from `args.config`). `.env.example` is dropped (tracked → already in the checkout).
20
+ - **`src/commands/configure.js`** — autodetects `stack.env_files` from the universal gitignored env conventions (`.env.local`/`.env`) only (stack-agnostic — no per-database special-casing), a comma-separated interactive prompt, and a summary-box line.
21
+ - **`src/commands/update.js`** — the `missingStack` detector now also catches top-level ARRAY stack keys (`stack.env_files` is `typeof 'object'`, so it needs `Array.isArray`); sub-object keys (`charting`/`animation`/`testing`) stay excluded (`Array.isArray({}) === false`). Without this the new key would silently never be asked on update.
22
+ - **`framework/docs/PROJECT-CONFIGURATION.md`** — documents `stack.env_files` (§4.4) as a stack-agnostic list.
23
+
24
+ ## [4.45.0] - 2026-06-15
25
+
26
+ **The main-repo-root (`$MAIN`) resolution across `worktree-manager` + `/prd` + `/new` is unified onto one correct, `separate-git-dir`-safe canonical — fixing two latent bugs while *refuting* the obvious "unify on `--git-common-dir`" fix the deferred plan proposed.** v4.42.0 deferred the `$MAIN` cleanup after an adversarial pass flagged it as a likely trap; this release does it properly. The root resolution lived in ~5 forms across three genuine execution contexts (main-checkout cwd, worktree cwd, the `allocate-id.sh` startup), and the deferred plan wanted to collapse them onto `git rev-parse --git-common-dir` + `/..` (the recipe already in `allocate-id.sh` `resolve_main()`). A 3-skeptic adversarial review **before implementation** demolished that plan with git experiments: (1) `--git-common-dir` + parent returns the *parent of the git dir*, which is the WRONG directory under `git init --separate-git-dir` (the git dir lives outside the working tree) — so the "canonical" recipe was itself fragile, and `resolve_main()` only worked because BALDART repos use in-tree `.git`; (2) the alleged `prd/SKILL.md` Step-1 bug ("`--show-toplevel` from inside a worktree") does **not** exist — Step 1 runs only at fresh kickoff on the main checkout, and resume reads the persisted `main_path`, so `--show-toplevel` is correct there *and* is the only form that survives `separate-git-dir`; (3) `git worktree list` head is also not `separate-git-dir`-safe (returns the git dir). The corrected design keeps the task's **context classification** but fixes the **primitive**: every site resolves the root through `--show-toplevel` (the true working-tree root in all cases), using `git -C .. rev-parse --show-toplevel` from a worktree (its parent `.worktrees/` lives inside the main repo). `resolve_main()` is rewritten as the canonical reference (detects a linked worktree via `git-dir != git-common-dir`, walks one level up) — **byte-identical output to the old form for in-tree repos** (verified), correct for `separate-git-dir`, and fails cleanly (`return 1`) instead of emitting a garbage path. Two real fragilities fixed along the way: the `/mw` `$MAIN` fallback (`--git-common-dir` + `/..` under `git -C`, relative-base hazard + `separate-git-dir` break) and the `3b` card-sync (`--show-superproject-working-tree || pwd`, which returned the SUPERPROJECT for a real submodule and only worked otherwise by a cwd accident). All recipes dogfooded across normal + `separate-git-dir` repos in every cwd context. **MINOR** (hardening + bug fixes across skills/agents; no removed surface, **no new `baldart.config.yml` key** ⇒ schema-change propagation rule N/A).
27
+
28
+ ### Fixed
29
+
30
+ - **`framework/.claude/skills/worktree-manager/scripts/allocate-id.sh` — `resolve_main()` rewritten `--show-toplevel`-based.** Now resolves the working-tree root (correct under `git init --separate-git-dir`, where `--git-common-dir` + parent pointed at the wrong directory), detects a linked worktree (`git-dir != git-common-dir`) and walks one level up. Identical output to the prior form for in-tree repos; returns non-zero instead of a garbage path when not in a git repo.
31
+ - **`framework/.claude/skills/worktree-manager/SKILL.md` — `/mw` step 4c `$MAIN` fallback** no longer derives the root via `git -C "$WORKTREE_PATH" rev-parse --git-common-dir` + `/..` (parent-of-git-dir, wrong under a separate git dir; appending `/..` to a possibly-relative `--git-common-dir` under `git -C` resolves against the wrong base). It `cd`s into the worktree then `git -C .. rev-parse --show-toplevel`, with a loud guard replacing the silent failure path.
32
+ - **`framework/.claude/skills/worktree-manager/SKILL.md` — `3b. Sync untracked cards`** drops the `git -C "$WORKTREE_PATH" --show-superproject-working-tree || pwd` form (returned the *superproject* for a real git submodule, where the cards do not live; only resolved otherwise by relying on cwd). At step 3b cwd is still the main checkout, so it now uses `git rev-parse --show-toplevel` — correct for normal, submodule, and `separate-git-dir` repos.
33
+
34
+ ### Changed
35
+
36
+ - **`framework/.claude/skills/worktree-manager/SKILL.md` — new `## Resolving the main repo root` section** documenting the three execution contexts, the correct primitive per context, and an explicit "do NOT use `--git-common-dir` + `/..`" rule (codifying the adversarial finding so the next maintainer doesn't re-attempt the refuted unification). The `/nw` step-1, nw-docs step-0, and `/nw` env-copy sites gain cross-ref comments; env-copy keeps `git -C .. --show-toplevel` but replaces its silent `|| echo '../..'` fallback with a loud guard.
37
+ - **`framework/.claude/skills/prd/references/validation-phase.md` + `framework/.claude/agents/{prd,prd-card-writer}.md`** — the legacy `$MAIN` fallback and the descriptive `$MAIN` comments switch from `--git-common-dir` to the canonical `--show-toplevel` resolution (and point at the new section). `prd/SKILL.md` Step 1 is deliberately **unchanged** — `--show-toplevel` there is correct (refuted "bug").
38
+ - **`framework/.claude/skills/new/references/setup.md` — `$MAIN` resolution corrected**: from a worktree-invocation, resolve via the worktree's parent toplevel rather than `--show-superproject-working-tree` (a submodule primitive) or `git worktree list` (not `separate-git-dir`-safe).
39
+
40
+ ## [4.44.0] - 2026-06-15
41
+
42
+ **The toolchain layer (v4.41.0) now reaches the *execution-side* gates too: the worktree baseline/merge builds and the classic `/new` reference suite stop hard-coding `npx tsc`/`npx eslint`/`npm run build` and run the consumer's configured `toolchain.commands.*` instead.** v4.41.0 wired the *review* gates (`qa-sentinel`, `coder`, the `/new`+`/new2` review workflows) through `toolchain.commands.*`, but a dozen gates that actually run a build/lint/typecheck were still hard-coded — so a Biome/Vitest consumer's worktree baseline silently ran `eslint`, and the classic `/new` per-card + final gates ran `npm run build` regardless of config. This release makes those gates toolchain-aware while preserving each site's existing **severity** policy (a configured command failing is the same STOP/continue verdict the default produced — the protocol's no-fallback rule governs only *which* command runs, never what to do with its exit code). The mechanism follows the **execution context**: a markdown **prose note** where a full model writes the gate command (the `/new` references, `/bug`, `/simplify`), and an inline **shell resolver** in `worktree-manager` — which has no `args.config` and whose baseline runs in a weak/background subagent, so it resolves `toolchain.commands.*` from `baldart.config.yml` on disk the same way it already resolves `git.trunk_branch`, rather than relying on a prose note a weak model could skip. The design was adversarially reviewed before implementation (3 skeptics), which corrected the initial plan on three points: (1) the `/mw` best-effort `npm run test 2>/dev/null || true` keeps its `|| true` swallow (never a STOP trigger); (2) the `/mw` changed-files lint scope is preserved as the default (a configured whole-tree command like `biome check .` runs by the consumer's contract); (3) two missed twins — `/bug` and `/simplify` verify steps — were added to scope. **Declared debt (intentionally untouched):** `npm install` stays hard-coded (there is no `toolchain.commands.install` key — adding one would be a 5-layer schema change; keeping this MINOR), `markdownlint` is not in the curated map, and the descriptive `(tsc+lint+build)` labels in `new2.js`/`setup.md` are not load-bearing (the `new2` projectBrief already injects the verbatim instruction — refuted by review). **MINOR** (additive: more gates honor the existing `features.has_toolchain` + `toolchain.commands.*`; no removed surface, **no new `baldart.config.yml` key** ⇒ schema-change propagation rule N/A).
43
+
44
+ ### Changed
45
+
46
+ - **`framework/.claude/skills/worktree-manager/SKILL.md` — gates toolchain-aware via an inline shell resolver.** New `## Toolchain-aware gates` convention + a Project-Context dependency on `features.has_toolchain` + `toolchain.commands.{typecheck,lint,build,test}`. Each gate block (`/nw` step 5 baseline, `/mw` step 2 pre-merge, step 4b rebase build, step 5 post-merge) inlines a `_tc()` resolver that reads the config from disk (mirroring the existing `git.trunk_branch`/`git.merge_strategy` grep idiom) and runs the configured command via `eval "${VAR:-<default>}"`, falling back to the hard-coded default when unset/flag-off. Three carve-outs documented: `npm install` is not a gate key, severity is unchanged, and the `/mw` best-effort `test` line keeps its `|| true` swallow.
47
+ - **`framework/.claude/skills/new/SKILL.md` — new `## Toolchain gates` core invariant** (cited as `§ "Toolchain gates"` by the reference modules, same pattern as `§ "Context economy"`): when `has_toolchain`, the mechanical gates use `toolchain.commands.<gate>` verbatim, a non-zero configured command is a real FAIL (no masking fallback), and `npm install`/`markdownlint` are not toolchain keys.
48
+ - **`framework/.claude/skills/new/references/{implement,review-cycle,completeness,final-review,commit,team-mode}.md` — gate sites cite `§ "Toolchain gates"`** so the classic `/new` per-card (Phase 1 verify, Phase 2.55/2.5x re-runs, Phase 3 doc re-verify, completeness gap re-run, commit pre-check), team-mode (per-card coder briefing, group build, group simplify re-run), and final-review build all resolve `toolchain.commands.*` verbatim when the flag is on.
49
+ - **`framework/.claude/skills/{bug,simplify}/SKILL.md` — standalone verify steps toolchain-aware** (the two twins surfaced by the adversarial scope review): `/bug` PHASE 5 regression checks and `/simplify` Step 5 verify gain a self-contained Toolchain note + a Project-Context dependency on `features.has_toolchain` + `toolchain.commands.{lint,typecheck,test}`.
50
+ - **`framework/agents/toolchain-protocol.md` — Consumers section updated** to record the execution-side gates and the two resolution mechanisms (prose note vs on-disk shell resolver), and to restate the `npm install`/`markdownlint` carve-outs.
51
+
8
52
  ## [4.43.1] - 2026-06-15
9
53
 
10
54
  **The npm publish workflow is now race-safe: pushing several `v*.*.*` tags close together can no longer leave `latest` on a lower version.** Publishing v4.41.0/4.42.0/4.43.0 together exposed the bug — the three tag-push workflow runs executed in **parallel**, and `npm publish` (with no explicit dist-tag) points `latest` at whichever version finishes **last chronologically**, not the highest. Real outcome: `latest` landed on **4.42.0**, so `npx baldart` installed a non-latest version (fixed by hand with `npm dist-tag add baldart@4.43.0 latest`). This release closes the race two ways: (1) a workflow-level **`concurrency` group** (`group: publish-npm`, `cancel-in-progress: false`) serializes all publish runs so they never race on the dist-tag and a publish is never cancelled; (2) a new final step **reconciles `latest` to the highest published version** — it computes the max stable semver across `npm view baldart versions` (unioned with the just-published `VERSION` to survive registry propagation lag, numeric per-segment compare so `4.10.0 > 4.9.0`) and repoints `latest` only when it has drifted. Because the runs are serialized, the last run always leaves `latest` on the maximum regardless of push order. The existing "Verify tag matches VERSION" guard is untouched. **PATCH** (CI/release-machinery bugfix, no change to installed surface or `baldart.config.yml` ⇒ schema-change propagation rule N/A).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.43.1
1
+ 4.46.0
@@ -311,7 +311,8 @@ applies even when N=1.
311
311
  worktree on this machine (lock + shared high-water mark under `.worktrees/`):
312
312
 
313
313
  ```bash
314
- # $MAIN is the main repo root: dirname of `git rev-parse --git-common-dir`.
314
+ # $MAIN is the persisted main repo root (see worktree-manager § "Resolving the
315
+ # main repo root" — via --show-toplevel, NOT --git-common-dir).
315
316
  ALLOC="$MAIN/.claude/skills/worktree-manager/scripts/allocate-id.sh"
316
317
  if [ -x "$ALLOC" ]; then
317
318
  N="$("$ALLOC" reserve FEAT "<slug>")" # e.g. prints 0024
@@ -504,7 +504,9 @@ parallelism (concurrent sessions on sibling worktrees both pick the same "next"
504
504
  integer and conflict at merge):
505
505
 
506
506
  ```bash
507
- # $MAIN = dirname of `git rev-parse --git-common-dir` (shared across worktrees).
507
+ # $MAIN = the persisted main repo root (the same value SKILL.md Step 1 wrote; see
508
+ # worktree-manager § "Resolving the main repo root" — resolved via --show-toplevel,
509
+ # NOT --git-common-dir, which breaks under git init --separate-git-dir).
508
510
  ALLOC="$MAIN/.claude/skills/worktree-manager/scripts/allocate-id.sh"
509
511
  [ -x "$ALLOC" ] && N="$("$ALLOC" reserve FEAT <slug>") # also BUG | UI | DOC | PERF
510
512
  ```
@@ -17,7 +17,7 @@ Argument: optional bug description (e.g., `/bug feature X is not saving`).
17
17
 
18
18
  ## Project Context
19
19
 
20
- **Reads from `baldart.config.yml`:** `paths.design_system`, `paths.references_dir`, `paths.wiki_dir` (Phase 0 `rg` lookup target).
20
+ **Reads from `baldart.config.yml`:** `paths.design_system`, `paths.references_dir`, `paths.wiki_dir` (Phase 0 `rg` lookup target), `features.has_toolchain` + `toolchain.commands.{lint,typecheck,test}` (the PHASE 5 regression gates run those verbatim when the flag is on).
21
21
  **Gated by features:** `features.has_design_system` (when `true`, Phase 4's design-system reads become BLOCKING for UI-touching bugs).
22
22
  **Overlay:** loads `.baldart/overlays/bug.md` if present — project-specific debug entry points (e.g. SWR debug switches, env summary helpers, error-code modules). The base skill stays generic; project-specific code paths live in the overlay.
23
23
  **On missing/empty keys:** ask the user; do not assume defaults. See `framework/agents/project-context.md` § 3.
@@ -202,6 +202,8 @@ If the project exposes an env-summary helper (listed in `.baldart/overlays/bug.m
202
202
 
203
203
  ## PHASE 5: VERIFY & CLEAN UP
204
204
 
205
+ > **Toolchain:** when `features.has_toolchain: true` in `baldart.config.yml`, run `toolchain.commands.{typecheck,lint,test}` **verbatim** instead of the defaults below — per `framework/agents/toolchain-protocol.md`. Empty/absent key (or flag off) → the default. A configured command that exits non-zero is a real FAIL (do not fall back).
206
+
205
207
  1. Reproduce original scenario — confirm fix
206
208
  2. Check regressions: `npx tsc --noEmit` + `npx eslint --max-warnings=0 <changed-files>`
207
209
  3. If tests exist: `npm run test`
@@ -243,6 +243,24 @@ baselines. Keep that bulk on disk and pass **paths**, not bodies.
243
243
 
244
244
  ---
245
245
 
246
+ ## Toolchain gates
247
+
248
+ When `features.has_toolchain: true` in `baldart.config.yml`, every mechanical gate
249
+ this skill runs (`lint`, `typecheck`, `test`, `build`) uses the consumer's
250
+ configured command from `toolchain.commands.<gate>` **verbatim** instead of the
251
+ `npm`/`npx` default shown at the call site — per `framework/agents/toolchain-protocol.md`.
252
+ Resolve per gate: a non-empty `toolchain.commands.{lint,typecheck,test,build}` wins;
253
+ empty/absent (or the flag off/missing) → the default at the call site, identical to
254
+ pre-toolchain behavior. A configured command that exits non-zero is a real gate
255
+ **FAIL** — do NOT then run the default (that would mask the failure); fall back only
256
+ when the key is **unset**. `npm install` and `markdownlint` are NOT toolchain keys
257
+ (there is no `commands.install`, and markdownlint is not in the curated map) — they
258
+ stay as written. The gate-output discipline (§ "Context economy" → redirect to disk,
259
+ surface only the exit code) applies to the resolved command exactly as to the default.
260
+ Reference modules cite this section as `§ "Toolchain gates"`.
261
+
262
+ ---
263
+
246
264
 
247
265
  ## Routing — il per-card pipeline e i moduli on-demand
248
266
 
@@ -250,9 +268,9 @@ baselines. Keep that bulk on disk and pass **paths**, not bodies.
250
268
  sistema, ri-letto a OGNI turno. Per non pagare 60k+ token di istruzioni di fase a
251
269
  ogni turno, il dettaglio passo-passo di ogni fase vive in un **modulo `references/<x>.md`**
252
270
  caricato on-demand. Questo file (il core) tiene solo gli invarianti cross-fase
253
- (Context Tracking, Progress Visibility, § "Context economy", Agent Routing, QA
254
- Profile, Trivial fast-lane, Risk-signal detector, Fix Application Log) + questa
255
- mappa di navigazione.
271
+ (Context Tracking, Progress Visibility, § "Context economy", § "Toolchain gates",
272
+ Agent Routing, QA Profile, Trivial fast-lane, Risk-signal detector, Fix Application
273
+ Log) + questa mappa di navigazione.
256
274
 
257
275
  > **HARD RULE — leggi il modulo PRIMA di eseguire la fase.** Quando entri in una
258
276
  > fase, **Read** il suo modulo `references/<x>.md` e poi eseguilo. Eseguire una
@@ -260,9 +278,9 @@ mappa di navigazione.
260
278
  > compaction che l'ha evacuato) è una violazione di protocollo: ricaricalo. Registra
261
279
  > nel tracker, sotto `## Current Card`, il campo `phase_module_loaded: <modulo>` al
262
280
  > caricamento, così la § "Context recovery protocol" sa cosa ri-leggere dopo una
263
- > compaction. I `§ "..."` citati dai moduli (Context economy, Context Tracking,
264
- > Trivial-card fast-lane, Risk-signal detector, Fix Application Log) puntano a sezioni
265
- > che vivono **qui nel core** → risolvono sempre.
281
+ > compaction. I `§ "..."` citati dai moduli (Context economy, Toolchain gates, Context
282
+ > Tracking, Trivial-card fast-lane, Risk-signal detector, Fix Application Log) puntano a
283
+ > sezioni che vivono **qui nel core** → risolvono sempre.
266
284
 
267
285
  **Sequenza (per ogni card, in ordine — i moduli per-card sono caricati per traversata):**
268
286
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  > Sequential-mode global step numbering resumes here at 26 (Phase 3.5 ended at 25; Phase 3.7 used its own local C.0–C.6 counter). The tracker phase-string `4-commit` therefore maps to step 26, NOT a second step 25.
8
8
 
9
- 26. **Update tracker**: phase = "4-commit". **Entry assertion** — before committing, verify the Phase 3.7 e2e re-run obligation was honored: read the tracker for `e2e-rerun: triggered` / `e2e-rerun: not-needed`. If Phase 3.7 touched UI files but no `e2e-rerun` entry exists, do NOT commit yet — go run the re-run per Phase 3.7 step 6 first. Also confirm Phase 3.5/3.7 fixes did not leave lint/tsc broken: if the Phase 3.7 fix sub-loop applied any patch, run `npm run lint` + `npx tsc --noEmit` (when typescript) once before committing (redirect to disk per § "Context economy").
9
+ 26. **Update tracker**: phase = "4-commit". **Entry assertion** — before committing, verify the Phase 3.7 e2e re-run obligation was honored: read the tracker for `e2e-rerun: triggered` / `e2e-rerun: not-needed`. If Phase 3.7 touched UI files but no `e2e-rerun` entry exists, do NOT commit yet — go run the re-run per Phase 3.7 step 6 first. Also confirm Phase 3.5/3.7 fixes did not leave lint/tsc broken: if the Phase 3.7 fix sub-loop applied any patch, run `npm run lint` + `npx tsc --noEmit` (when typescript) once before committing — when `has_toolchain`, the configured `toolchain.commands.{lint,typecheck}` verbatim (§ "Toolchain gates") — (redirect to disk per § "Context economy").
10
10
  27. Stage and commit **all changes together** in the worktree using format `[CARD-ID] Brief description` (MUST per AGENTS.md). Include all relevant files — implementation, review fixes, QA-driven fixes, and doc updates in a single commit. Do NOT merge or push yet — that happens post-batch.
11
11
  - **IMPORTANT — explicit staging**: NEVER use `git add -A` or `git add .`. Always stage files by explicit name:
12
12
  ```bash
@@ -118,7 +118,7 @@ Before triggering any review, you MUST verify that the coder agent implemented *
118
118
  - The exact list of unimplemented items (copy the checklist rows)
119
119
  - The file-ownership restrictions from `## File Ownership Map`
120
120
  - The instruction: "Implement ONLY these missing items. Do not refactor or expand scope."
121
- - After the fix agent completes, re-run the static gates the fix could have broken — `npm run lint`, `npx tsc --noEmit` (when `stack.language` includes typescript), and `npm test` — not just build + lint (a gap-fix can introduce a type error or break a test that the earlier Phase 2 gate had passed). Redirect each to `/tmp/<gate>-<CARD-ID>.txt` per § "Context economy" (never inline).
121
+ - After the fix agent completes, re-run the static gates the fix could have broken — `npm run lint`, `npx tsc --noEmit` (when `stack.language` includes typescript), and `npm test` (when `has_toolchain`, the configured `toolchain.commands.{lint,typecheck,test}` verbatim § "Toolchain gates") — not just build + lint (a gap-fix can introduce a type error or break a test that the earlier Phase 2 gate had passed). Redirect each to `/tmp/<gate>-<CARD-ID>.txt` per § "Context economy" (never inline).
122
122
  - Re-verify each fixed item against the code — do NOT trust the agent's self-report.
123
123
  - Repeat this sub-loop up to **2 times** (per-item budget, shared with step 0 — see "Loop-counter scope"). After 2 loops, if items remain Partial or Missing:
124
124
  - Log in `## Issues & Flags`: list each unimplemented requirement.
@@ -258,7 +258,7 @@ that is a **gate violation**: log it as
258
258
  - **`security`-domain findings** (path in `paths.high_risk_modules`, or RLS-policy SQL) → route to **security-reviewer** in write mode (canonical writer map v4.26.1 — it owns the security-invariant contract a coder lacks; NEVER route security fixes to coder). **`migration`-domain findings** (SQL under the migrations dir) → route to **coder**. For both, apply the Sub-agent failure protocol's STOP-on-crash rule (never inline-fallback on a security/migration fix). These are NOT collapsed into a generic "everything else" bucket.
259
259
  - **All remaining findings** (other code, perf, test) → invoke the **coder** agent once to apply them in a single pass.
260
260
  Run in the order doc-reviewer → security-reviewer → coder (skip any whose partition is empty). Pass only the verified findings, not false positives.
261
- 12. Run final build: `npm run lint && npx tsc --noEmit && npm run build` (redirect each to `/tmp/final-<gate>.txt` per § "Context economy"; surface only exit code + bounded extract on failure).
261
+ 12. Run final build: `npm run lint && npx tsc --noEmit && npm run build` (when `has_toolchain`, the configured `toolchain.commands.{lint,typecheck,build}` verbatim — § "Toolchain gates"; redirect each to `/tmp/final-<gate>.txt` per § "Context economy"; surface only exit code + bounded extract on failure).
262
262
  If any check fails, apply self-healing retry loop (up to 3 times).
263
263
  13. **Update tracker** with final review results:
264
264
  - Review engine: Codex (a non-Anthropic frontier model, resolved at runtime by `codex-companion.mjs`) (primary) | Claude code-reviewer (fallback)
@@ -335,6 +335,7 @@
335
335
  ```
336
336
 
337
337
  8. **Run the verification gates and CAPTURE their output to disk** (so step 9 can pass it to a fix agent) — **redirect, never `tee`/stream inline** (per § "Context economy" → Gate-output discipline). Each is its own gate:
338
+ When `features.has_toolchain: true`, substitute each command below with `toolchain.commands.{lint,typecheck,test,build}` run verbatim (§ "Toolchain gates"; defaults shown are the fallback when a key is unset).
338
339
  ```bash
339
340
  cd <worktree-path>
340
341
  npm run lint > /tmp/lint-<CARD-ID>.txt 2>&1; echo "lint:$?"
@@ -117,7 +117,7 @@ After completeness is verified, clean up the implementation before it reaches re
117
117
 
118
118
  **Telemetry (Fix Application Log)** — for EVERY finding (valid OR skipped) append one row to the tracker's `## Fix Application Log` section per the schema above. Use `domain=simplify-{reuse|quality|efficiency}` matching the originating agent. Include the `severity` trailing key. Inline: `decision=inline | applied_by=orchestrator | est_lines=<n> | severity=<HIGH|MEDIUM> | finding=<1-line>`. Delegated (domain-override): `decision=<coder|doc-reviewer> | applied_by=<coder|doc-reviewer> | est_lines=<n> | severity=<...> | finding=<1-line>`. Skipped: `decision=skipped | applied_by=- | est_lines=0 | reason=<false-positive|not-worth-addressing>`.
119
119
 
120
- 5. After all fixes, run `npm run lint` and `npx tsc --noEmit` to confirm nothing broke (redirect to disk per § "Context economy"; surface only exit code + a bounded extract on failure).
120
+ 5. After all fixes, run `npm run lint` and `npx tsc --noEmit` (when `has_toolchain`, the configured `toolchain.commands.{lint,typecheck}` verbatim — § "Toolchain gates") to confirm nothing broke (redirect to disk per § "Context economy"; surface only exit code + a bounded extract on failure).
121
121
  If either fails, fix the regression (up to **2 retries**). **If it still fails after 2 retries**: do NOT silently continue to Phase 2.6 with a broken tree — log the failure in `## Issues & Flags` as `[SIMPLIFY-REGRESSION]` and invoke `AskUserQuestion` (revert the simplify fixes / keep and have me fix manually / stop the card), mirroring the Phase 3.5 escalation.
122
122
 
123
123
  6. **Update tracker**: phase = "2.55-simplify DONE", log count of fixes applied (or "clean — 0 fixes").
@@ -288,7 +288,7 @@ skill's Phase 1 falls back to deriving Gherkin scenarios from
288
288
  Doc-reviewer applies all doc-domain fixes itself. The orchestrator does NOT spawn a coder for doc fixes (since v3.40.0 — `doc` is owned by `doc-reviewer`, see "Domain-Override Domains"). The only doc-reviewer output that leaves this phase unfixed is a **doc-drift→bug finding rooted in CODE** (the implementation contradicts a documented contract). Route it explicitly: if the conflicting code file matches the `security` Domain-Override match rule (`paths.high_risk_modules`) → spawn `security-reviewer` with the finding now, in this phase (a security-class code fix is not deferrable to a `light` Phase 3.7, and security is owned by `security-reviewer` — never a coder); otherwise carry the finding into the Phase 3.7 `/codexreview` input as a known code-drift bug and let the Phase 3.7 fix sub-loop apply it. Either way, append a Fix Application Log row with `domain=codex-correctness` (NOT `doc`) so telemetry attributes it as a code fix. Do NOT leave it accumulating in the tracker with no fix owner.
289
289
  14. **Knowledge-corpus sync (OPTIONAL — only if the project ships a corpus-sync agent)**: There is NO shipped `obsidian-sync` agent — do NOT dispatch one (a hard dispatch to a non-existent subagent fails silently). Only when the project provides its own knowledge-corpus sync agent (declared in `.baldart/overlays/new.md`) AND doc-reviewer's findings indicate a corpus impact, invoke that agent with the listed paths after the doc fixes are applied. Otherwise skip with a one-line notice (`knowledge-corpus sync: skipped (no corpus-sync agent configured)`). Non-blocking either way.
290
290
  15. **Telemetry** — after doc-reviewer returns, append one row per doc finding to `## Fix Application Log`: `3 | doc | est_lines=<n> | decision=doc-reviewer | applied_by=doc-reviewer | finding=<1-line>`. If 0 findings, append one row: `3 | doc | est_lines=0 | decision=skipped | applied_by=- | reason=no-findings`. **Phase-8 producer (named counter)** — ALSO record the per-card doc-gap counts as a structured line in `## Current Card` (carried into `## Completed Cards` at Phase 5): `doc_gaps: found=<N> fixed=<M>` where `N` = total doc findings doc-reviewer raised and `M` = those it applied. This is the single named producer for Phase 8's `doc_gaps_found` / `doc_gaps_fixed` fields — without it those fields have no upstream write and Phase 8 would hard-code zeros. (D.4a is the team-mode producer of the same counter — see Phase 7 § D.4a.)
291
- 16. Run `npm run lint` and `npx tsc --noEmit` (when `stack.language` includes typescript) to verify nothing broke (redirect to disk per § "Context economy"). If doc-reviewer touched any source-adjacent file (a `.ts`/`.tsx` helper, a co-located doc export), also run `npm run build`. If any check fails, apply the self-healing retry loop (up to 3 times, no user prompt). **If still failing after 3 retries**: do NOT fall through silently to Phase 3.5 — log `[DOC-PHASE-REGRESSION]` in `## Issues & Flags` and invoke `AskUserQuestion` (revert the doc-phase edits that broke the build / keep and fix manually / stop the card).
291
+ 16. Run `npm run lint` and `npx tsc --noEmit` (when `stack.language` includes typescript) — when `has_toolchain`, the configured `toolchain.commands.{lint,typecheck,build}` verbatim (§ "Toolchain gates") — to verify nothing broke (redirect to disk per § "Context economy"). If doc-reviewer touched any source-adjacent file (a `.ts`/`.tsx` helper, a co-located doc export), also run `npm run build`. If any check fails, apply the self-healing retry loop (up to 3 times, no user prompt). **If still failing after 3 retries**: do NOT fall through silently to Phase 3.5 — log `[DOC-PHASE-REGRESSION]` in `## Issues & Flags` and invoke `AskUserQuestion` (revert the doc-phase edits that broke the build / keep and fix manually / stop the card).
292
292
  17. **Telemetry for the step-16 self-heal** — if the retry loop spawned any fix (a code edit to recover from a doc-phase regression), append a Fix Application Log row for it AFTER the loop settles (the step-15 doc telemetry row was written before this loop ran, so it does not capture step-16 fixes). Then update tracker: phase = "3-doc-review DONE", log doc findings count, fixes applied.
293
293
  If doc-reviewer found a recurring gap, append 1-line to `## Lessons Learned`:
294
294
  `DOC: <pattern>`
@@ -15,7 +15,7 @@
15
15
  - Resolve `$TRUNK` = `git.trunk_branch` from `baldart.config.yml`. **When the key is absent** (consumer updated to ≥4.0.0 without re-running `configure`), do NOT hard-assume `develop` — autodetect the repo's real default branch exactly as worktree-manager does, so `/new` and `nw` agree on the base: `git -C "$MAIN" symbolic-ref --quiet refs/remotes/origin/HEAD` (strip `refs/remotes/origin/`), else the first existing local branch among `develop` / `main` / `master`. A `main`-trunk repo defaulted to `develop` here would diverge from the worktree base `nw` picks and break every `git diff "$TRUNK...HEAD"` gate. Only HALT ("Trunk branch unresolved — run `npx baldart configure`") if nothing resolves. Persist the resolved value as `Trunk branch:` in the tracker `## Worktree` section. **Every later Phase 0 / Phase 6c bash snippet that references the integration trunk MUST use `$TRUNK`, never a baked-in `develop`.** Begin every later consumer with a guard: if `$TRUNK` is empty → HALT with "Trunk branch unresolved — re-read `git.trunk_branch` from the tracker".
16
16
  - Resolve `$METRICS` = `paths.metrics` from `baldart.config.yml` (default `docs/metrics`). This is the framework-owned telemetry directory written by Phase 8 (tracker archive, `skill-runs.jsonl`, `sessions/`). The dirty-tree gate (step 3) reads `$METRICS` to recognise — and never surface — its own telemetry output. Persist it as `Metrics dir:` in the tracker `## Worktree` section.
17
17
 
18
- 1. **Resolve `$MAIN`** — the absolute path of the main repo (not a worktree). If `/new` was invoked from inside a worktree, walk up to the parent repo via `git rev-parse --show-superproject-working-tree` or `git worktree list` until you find the non-worktree root. Persist as `Main repo:` in the tracker `## Worktree` section. **Write `$MAIN` to the tracker the moment it is computed** — every later consumer (Phase 6c, Phase 6b) MUST re-read it from the tracker and HALT with "`$MAIN` absent from tracker" if the field is missing or empty, never silently use an undefined `$MAIN` (it does not survive context compaction).
18
+ 1. **Resolve `$MAIN`** — the absolute path of the main repo (not a worktree). Use `git rev-parse --show-toplevel` when cwd is the main checkout. If `/new` was invoked from **inside a worktree**, that returns the worktree, so resolve the main root from the worktree's parent instead: `cd "$(git rev-parse --show-toplevel)/.." && git rev-parse --show-toplevel` (the worktree lives at `<main>/.worktrees/<name>`). Do **not** use `--git-common-dir` + `/..` (wrong under `git init --separate-git-dir`) or `git worktree list` (its main entry is the git dir, not the working tree, under a separate git dir). This is the same contract as worktree-manager § "Resolving the main repo root". Persist as `Main repo:` in the tracker `## Worktree` section. **Write `$MAIN` to the tracker the moment it is computed** — every later consumer (Phase 6c, Phase 6b) MUST re-read it from the tracker and HALT with "`$MAIN` absent from tracker" if the field is missing or empty, never silently use an undefined `$MAIN` (it does not survive context compaction).
19
19
 
20
20
  1b. **Migration Gate (BLOCKING only when a migration is *declared* — else a silent no-op)** — resolve DB migrations interactively **before** the worktree exists, so the schema is live before any card builds against it. *Why this exists*: a migration applied to a shared/remote DB is owner-gated, so without this gate it is deferred to the END of the batch — and every downstream card in the batch is then built and verified against a schema that is not yet live (`validation_commands` / QA / E2E / DB-generated `tsc` types fail falsely → those cards cascade into deferral/blocked). Front-loading the migration removes that root cause. **The declaration lives in the EPIC card** (`migration_plan` block — project-specific, authored by the user, typically via the `.baldart/overlays/new.md` overlay). Steps:
21
21
 
@@ -287,7 +287,9 @@
287
287
  { cards: [<all card IDs>], groupParent: <PARENT-ID|null>, slug: "<slug>" }
288
288
  Let it: group cards, derive the branch from git_strategy.branch (fallback
289
289
  feat/<PARENT-ID>-<slug>), create the worktree in .worktrees/, install deps,
290
- copy env files, assign a free port, update .worktrees/registry.json (all card
290
+ copy the gitignored env artifacts in stack.env_files (files via cp, dirs via
291
+ cp -r; missing file → WARN, never abort), assign a free port, update
292
+ .worktrees/registry.json (all card
291
293
  IDs in the `cards` field), and run the baseline (tsc + lint + build)
292
294
  UNDER A HARD TIMEOUT — wrap the build step in `timeout 600 <build-cmd>`
293
295
  (10 min) so a hung or interactive build cannot stall the pre-flight barrier.
@@ -105,6 +105,7 @@ Agent tool call:
105
105
  a) Print the numbered requirements checklist (anti-skip measure)
106
106
  b) Implement ALL requirements
107
107
  c) Run: npx tsc --noEmit && npx eslint --max-warnings=0 <your-files>
108
+ (when toolchain.commands.{typecheck,lint} are configured, run those verbatim instead — per agents/toolchain-protocol.md)
108
109
  d) Self-heal up to 3 times if checks fail
109
110
  e) Verify completeness: for each requirement, confirm code exists (read it)
110
111
  f) If any requirement is missing after implementation, implement it now
@@ -151,7 +152,7 @@ For each completed agent:
151
152
 
152
153
  After ALL agents in the group complete successfully:
153
154
 
154
- 1. **D.1 — Build verification (group)** — Run `npm run build` in the worktree to verify combined changes compile (redirect to `/tmp/build-group.txt` per § "Context economy"; surface only exit code + bounded extract on failure). If build fails, identify which card's changes broke it (from `git diff --name-only` per card), spawn a targeted fix-coder for those files only.
155
+ 1. **D.1 — Build verification (group)** — Run `npm run build` (when `has_toolchain`, the configured `toolchain.commands.build` verbatim — § "Toolchain gates") in the worktree to verify combined changes compile (redirect to `/tmp/build-group.txt` per § "Context economy"; surface only exit code + bounded extract on failure). If build fails, identify which card's changes broke it (from `git diff --name-only` per card), spawn a targeted fix-coder for those files only.
155
156
 
156
157
  1.5. **D.1.5 — Effective per-card review profile (compute ONCE; drives D.2 + D.4b)** — For EACH card in the group, compute its **effective codex profile** with the SAME deterministic rule the sequential Phase 3.7 Step C uses, so the two paths never disagree:
157
158
  - **Floor**: read the card's `review_profile` field (`skip`/`light`/`balanced`/`deep`) per the QA Profile Selector (fallback-computed only for legacy cards lacking the field).
@@ -242,7 +243,7 @@ After ALL agents in the group complete successfully:
242
243
 
243
244
  3a. **D.3a — Phase 2.5b AC-Closure Gate (per-card, BLOCKING — non-skippable)** — For EACH card in the group, **sequentially**, invoke the full Phase 2.5b gate as documented in `### Phase 2.5b — AC-Closure Gate (BLOCKING — Scope Closure Discipline)`. This includes: build the AC Closure Ledger from the card YAML, run the rationalization scan, invoke `AskUserQuestion` one-per-deferred-AC, run the `implementation_notes` deferral audit, and persist the ledger in the tracker. Until EVERY card in the group exits PASS, do NOT proceed to D.3b. Cards exiting with `not_implemented` ACs that the user routes to "Implementa adesso" must finish their fix-coder loop and re-pass the gate before D.3b starts for the next card. Log under `## AC Closure Ledger — <CARD-ID>` per card.
244
245
 
245
- 3b. **D.3b — Phase 2.55 Simplify (per-card, FANNED OUT across the group)** — The Simplify agents are **read-only analysis on file-disjoint per-card diffs** (the orchestrator applies the fixes afterward), so there is NO reason to run them one card at a time. **Spawn the per-card Simplify analysis for ALL eligible cards in PARALLEL** — in a SINGLE message, fire each card's Phase 2.55 trio (Reuse / Quality / Efficiency) against that card's diff captured to `/tmp/diff-<CARD-ID>.txt` (per Phase 2.55 step 2 — pass each trio the **path**, scoped to the card's File Ownership Map; never inline the diff). Per-card (not group-aggregate) so findings stay attributable. When all analyses return, **apply fixes per card** (file-disjoint → no write conflict), then re-run `npm run lint` and `npx tsc --noEmit` on the worktree ONCE for the whole group (redirect to disk per § "Context economy"). (Concurrency is capped by the platform; passing N cards is safe — excess agents queue.)
246
+ 3b. **D.3b — Phase 2.55 Simplify (per-card, FANNED OUT across the group)** — The Simplify agents are **read-only analysis on file-disjoint per-card diffs** (the orchestrator applies the fixes afterward), so there is NO reason to run them one card at a time. **Spawn the per-card Simplify analysis for ALL eligible cards in PARALLEL** — in a SINGLE message, fire each card's Phase 2.55 trio (Reuse / Quality / Efficiency) against that card's diff captured to `/tmp/diff-<CARD-ID>.txt` (per Phase 2.55 step 2 — pass each trio the **path**, scoped to the card's File Ownership Map; never inline the diff). Per-card (not group-aggregate) so findings stay attributable. When all analyses return, **apply fixes per card** (file-disjoint → no write conflict), then re-run `npm run lint` and `npx tsc --noEmit` (when `has_toolchain`, the configured `toolchain.commands.{lint,typecheck}` verbatim — § "Toolchain gates") on the worktree ONCE for the whole group (redirect to disk per § "Context economy"). (Concurrency is capped by the platform; passing N cards is safe — excess agents queue.)
246
247
  - **Gate (enumerated, `TRIVIAL_CARDS`-driven)**: SKIP D.3b for a card in **`TRIVIAL_CARDS`** (the set already computed at D.1.5 — `review_profile == skip` AND 0 Step-A triggers AND **non-source diff**), aligning team mode with sequential Phase 2.55's `IS_TRIVIAL` re-confirmation on the ACTUAL diff and with team-mode's own D.1.5 SSOT. A trivial card has no substantive diff to simplify, and Simplify is quality-only (no merge-gate coverage to lose). Log `simplify: SKIPPED (trivial — non-source diff)`. **A card with `review_profile == skip` whose committed diff DID touch a source file is NOT in `TRIVIAL_CARDS` → run D.3b for it** (exactly as sequential 2.55 does — `skip` is the floor, the real diff is the deciding check). For `light`/`balanced`/`deep` cards D.3b runs unchanged. This is the ONLY enumerated skip — never skip D.3b "for time" on a non-trivial card. (Skipped cards are simply omitted from the parallel fan-out.)
247
248
 
248
249
  3c. **D.3c — Phase 2.6 E2E-Review (per-card)** — First, evaluate the existing Gate table for EVERY card at once (skip when `features.has_e2e_review: false`, backend-only diff per the diff predicate documented in Phase 2.6, or card type in the Phase 2.6 skip set — `backend`/`api`/`db`/`infra`/`docs`/`chore`/`config`). In practice most cards in a group skip this gate (backend/db/api), so the eligible set is usually 0–1. For the cards that PASS the gate, invoke `/e2e-review` in programmatic mode with that card's payload. Each `/e2e-review` keeps its own isolated state dir (`.baldart/e2e-review/<CARD-ID>/`), so multiple runs do not clobber each other's artifacts.
@@ -66,7 +66,7 @@ created by Step 1 (HARD RULE 17). `$WORKTREE_PATH` is set in the state file.
66
66
 
67
67
  **Variable guard (R6).** Before executing any item in Step 7, resolve and verify:
68
68
  - `$WORKTREE_PATH` — read from the state file `## Worktree / path:`. HALT with `"ABORT: WORKTREE_PATH not set in state file — cannot commit or merge."` if absent or empty.
69
- - `$MAIN` — the absolute path of the main (non-worktree) git repository root. **READ the persisted value from the state file `## Worktree / main_path:`** (written at SKILL.md Step 1, the R6 persist-then-read pattern `/new` uses). HALT with `"ABORT: MAIN absent from state — re-run from a session whose Step 1 persisted main_path."` if the field is absent or empty. Only when the field is genuinely missing (legacy state file) FALL BACK to deriving it once via `git -C "$WORKTREE_PATH" rev-parse --git-common-dir` (strips `/.git`). Every subsequent `git` command that runs outside the worktree MUST use `git -C "$MAIN" …`; never use a bare `git` command whose cwd is undefined. (This is the same variable name and resolution contract `/new` uses — the two skills must agree.)
69
+ - `$MAIN` — the absolute path of the main (non-worktree) git repository root. **READ the persisted value from the state file `## Worktree / main_path:`** (written at SKILL.md Step 1, the R6 persist-then-read pattern `/new` uses). HALT with `"ABORT: MAIN absent from state — re-run from a session whose Step 1 persisted main_path."` if the field is absent or empty. Only when the field is genuinely missing (legacy state file) FALL BACK to deriving it once from the worktree's parent toplevel: `MAIN="$(cd "$WORKTREE_PATH" && git -C .. rev-parse --show-toplevel)"`. (Do **not** use `git rev-parse --git-common-dir` + `/..` — that returns the parent of the git dir, which is wrong under `git init --separate-git-dir`; `--show-toplevel` is the true working-tree root. This mirrors the canonical resolution in worktree-manager § "Resolving the main repo root".) Every subsequent `git` command that runs outside the worktree MUST use `git -C "$MAIN" …`; never use a bare `git` command whose cwd is undefined. (This is the same variable name and resolution contract `/new` uses — the two skills must agree.)
70
70
 
71
71
  ### Resolve remaining items
72
72
 
@@ -8,7 +8,7 @@ description: Review changed code for reuse, quality, and efficiency, then fix an
8
8
 
9
9
  ## Project Context
10
10
 
11
- **Reads from `baldart.config.yml`:** `paths.references_dir`, `paths.components_primitives`, `paths.components_root`, `paths.design_system`.
11
+ **Reads from `baldart.config.yml`:** `paths.references_dir`, `paths.components_primitives`, `paths.components_root`, `paths.design_system`, `features.has_toolchain` + `toolchain.commands.{lint,typecheck,test}` (the Step 5 verify gates run those verbatim when the flag is on — see Step 5).
12
12
  **Gated by features:** `features.has_design_system` (Design System check section is BLOCKING when `true`).
13
13
  **Overlay:** loads `.baldart/overlays/simplify.md` if present — project-specific utility module paths, hook conventions.
14
14
  **On missing/empty keys:** ask the user; do not assume defaults. See `framework/agents/project-context.md` § 3.
@@ -142,6 +142,12 @@ When extracting shared code, write to the SAME `${paths.*}` keys the review phas
142
142
  These static checks run AFTER Step 4 has mutated code — a pre-fix pass would not cover the post-fix
143
143
  code, so re-running them here is mandatory whenever Step 4 changed anything.
144
144
 
145
+ > **Toolchain:** when `features.has_toolchain: true` in `baldart.config.yml`, run the command from
146
+ > `toolchain.commands.{lint,typecheck,test}` **verbatim** instead of the default shown below — per
147
+ > `framework/agents/toolchain-protocol.md`. Per gate: a non-empty config command wins; empty/absent
148
+ > (or the flag off) falls back to the default. A configured command that exits non-zero is a real
149
+ > FAIL (do not fall back to the default).
150
+
145
151
  Run the linter (use the project's lint command):
146
152
 
147
153
  ```
@@ -20,6 +20,8 @@ description: >
20
20
  - `git.merge_strategy` — `pr` (default, GitHub PR via `gh`) or `local-push` (direct fast-forward to `origin/<git.trunk_branch>`). Drives `/mw` step 4c.
21
21
  - `paths.backlog_dir` — used for syncing untracked backlog cards (`/nw` step 3b).
22
22
  - `paths.metrics` — JSONL telemetry dir (default `docs/metrics`) used by the rebase conflict-resolution table.
23
+ - `stack.env_files` — list of gitignored env artifacts (files OR dirs) copied into each worktree (`/nw` step 4). Default `['.env.local', '.env']`. The SAME list drives /nw, /new, and new2 — no per-path divergence. NEVER list a tracked file (already in the checkout). The skill is stack-agnostic — it just iterates the list; any project-specific entry (e.g. a tool's local-state dir) is the project's own opt-in in its config/overlay.
24
+ - `features.has_toolchain` + `toolchain.commands.{typecheck,lint,build,test}` — when the flag is `true`, the mechanical gates below run the consumer's configured commands verbatim instead of the hardcoded `npx tsc`/`npx eslint`/`npm run build` defaults (see "## Toolchain-aware gates"). Absent/`false` → defaults, identical to pre-toolchain behavior.
23
25
  - Protocol reference: `framework/agents/project-context.md`. Skills must ASK when a needed key is missing — never assume.
24
26
 
25
27
  ## Effort
@@ -30,6 +32,47 @@ reasoning depth for this run — detect it once at kickoff and strip the token
30
32
  before consuming user input. Level→behavior mapping, parsing contract, and
31
33
  precedence caveats: `framework/agents/effort-protocol.md`.
32
34
 
35
+ ## Toolchain-aware gates
36
+
37
+ When `features.has_toolchain: true` in `baldart.config.yml`, every mechanical
38
+ gate in this skill (type-check, lint, build, test) runs the command from
39
+ `toolchain.commands.<gate>` **verbatim** instead of the hardcoded default — per
40
+ `framework/agents/toolchain-protocol.md`. Resolution is done **in shell**, the
41
+ same way this skill already resolves `git.trunk_branch` / `git.merge_strategy`
42
+ from disk (a background `/nw` baseline runner is a weak/background model — a prose
43
+ "please substitute" note would be silently skipped while the hardcoded command
44
+ ran anyway; the shell resolver is model-independent). Each gate block inlines this
45
+ compact resolver (self-contained, since each runs as its own `Bash` call):
46
+
47
+ ```bash
48
+ # Resolve baldart.config.yml from disk; missing/unreadable → empty → hardcoded default.
49
+ CFG="baldart.config.yml"; [ -f "$CFG" ] || CFG="$(git rev-parse --show-toplevel 2>/dev/null)/baldart.config.yml"
50
+ _tc() { grep -E '^[[:space:]]*has_toolchain:[[:space:]]*true' "$CFG" >/dev/null 2>&1 || return 0
51
+ grep -A20 '^toolchain:' "$CFG" 2>/dev/null | grep -A15 '^[[:space:]]*commands:' \
52
+ | grep -E "^[[:space:]]+$1:" | head -1 \
53
+ | sed -E "s/.*$1:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" | sed -E 's/[[:space:]]+$//'; }
54
+ ```
55
+
56
+ `eval "${VAR:-<default>}"` then runs the configured command or falls back.
57
+
58
+ **Three carve-outs specific to this skill:**
59
+ - **`npm install` is NOT a toolchain gate** — there is no `toolchain.commands.install`
60
+ key (the command map is lint/format/typecheck/test/test_related/build/audit).
61
+ Dependency installation stays as-is; do **not** route it through the resolver.
62
+ - **Severity is unchanged.** The resolver picks *which* command runs, never *what
63
+ to do with its exit code*. The protocol's "a configured command that exits
64
+ non-zero is a real FAIL — do not then run the default" governs only resolution
65
+ (never mask a failure by running the default after the configured one failed);
66
+ it does **not** change each block's STOP/continue policy: a `/nw` baseline
67
+ type-check/lint failure is still "report but continue", a build failure is still
68
+ "STOP", exactly as written at each block.
69
+ - **The `/mw` best-effort test line stays best-effort.** `npm run test 2>/dev/null
70
+ || true` (pre-merge step 2) is intentionally non-blocking; the resolved command
71
+ keeps the `|| true` swallow, so `test` is never a `/mw` STOP trigger.
72
+
73
+ A configured **lint** command may be whole-tree (e.g. `npx biome check .`) where a
74
+ default is changed-files-scoped (`/mw` pre-merge) — that wider scope is the
75
+ consumer's configured contract, applied only when `has_toolchain` is on.
33
76
 
34
77
  **IMMEDIATE EXECUTION**: When invoked via `/nw`, `/mw`, `/lw`, or `/cw`, do NOT explain the process. Start executing the matching command flow immediately:
35
78
 
@@ -66,7 +109,7 @@ backlog cards, design references — and never touch source code.
66
109
  What docs mode SKIPS vs the standard flow:
67
110
  - No `npm install` (no `node_modules/` inside the worktree)
68
111
  - No port allocation (no dev server)
69
- - No `.env`/`.env.local` copy
112
+ - No env-artifact (`stack.env_files`) copy
70
113
  - No `npm run build` baseline verification
71
114
  - No lint / tsc baseline checks
72
115
  - No `--dev` server bootstrap
@@ -112,6 +155,9 @@ Output:
112
155
  # Without this, the worktree IS git-tracked: `git status` on the main
113
156
  # repo explodes with every file inside the worktree, exactly defeating
114
157
  # the parallel-isolation purpose of the docs-mode.
158
+ # Main-checkout context (the docs worktree is created below, cwd is still the
159
+ # main repo) → `--show-toplevel` is the main root, separate-git-dir-safe. See
160
+ # § "Resolving the main repo root".
115
161
  MAIN_ROOT="$(git rev-parse --show-toplevel)"
116
162
  if [ ! -f "$MAIN_ROOT/.gitignore" ] || ! grep -qE '^\.worktrees/?$' "$MAIN_ROOT/.gitignore"; then
117
163
  echo "ERROR: .worktrees/ is not in $MAIN_ROOT/.gitignore" >&2
@@ -195,7 +241,7 @@ git -C "$MAIN_ROOT" worktree add "$WORKTREE_REL" -b "$BRANCH" "origin/$TRUNK"
195
241
  }
196
242
  ```
197
243
 
198
- **Forbidden in docs mode**: `npm install`, port scan, `.env*` copy, `npm run build`,
244
+ **Forbidden in docs mode**: `npm install`, port scan, `.env*`/`stack.env_files` copy, `npm run build`,
199
245
  `npx tsc`, `npx eslint`, dev server start, `git checkout`/`switch`/`branch` on main.
200
246
 
201
247
  ### mw-docs programmatic
@@ -367,7 +413,8 @@ unmerged sibling branch, invisible to both the local backlog and the trunk
367
413
  merge-base. They collide at rebase/merge time.
368
414
 
369
415
  `scripts/allocate-id.sh` closes that race **on the same machine**. Every worktree
370
- shares one main repo root (resolved from `git rev-parse --git-common-dir`), and
416
+ shares one main repo root (resolved by the canonical `resolve_main()` helper in
417
+ that script — see § "Resolving the main repo root" below), and
371
418
  `.worktrees/` lives there and is gitignored — so it is the natural shared
372
419
  coordination point, exactly like `registry.json`. The allocator anchors a lock
373
420
  and a per-prefix high-water-mark there:
@@ -381,6 +428,36 @@ and a per-prefix high-water-mark there:
381
428
  .claude/skills/worktree-manager/scripts/allocate-id.sh release <worktree-path>
382
429
  ```
383
430
 
431
+ ---
432
+
433
+ ## Resolving the main repo root
434
+
435
+ Several steps need `$MAIN` — the absolute path of the **main repo working tree**
436
+ (never a worktree). It is resolved in **three distinct execution contexts**, and
437
+ each genuinely needs a different primitive — this is NOT entropy, and "unifying"
438
+ them onto one naked primitive regresses real cases (verified):
439
+
440
+ 1. **Main-checkout cwd** (a worktree is being *created*, or cards synced before
441
+ any `cd`): `git rev-parse --show-toplevel`. This is the true working-tree root
442
+ even when `.git` lives elsewhere (`git init --separate-git-dir`). Sites:
443
+ `/nw` step 1, nw-docs step 0, `3b. Sync untracked cards` (cwd is still main
444
+ there), and `/prd` SKILL.md Step 1.
445
+ 2. **Worktree cwd** (after `cd "$WORKTREE_PATH"`): `git -C .. rev-parse --show-toplevel`.
446
+ The worktree's parent is `.worktrees/`, which lives inside the main repo, so
447
+ its toplevel is the main root — correct under a separate git dir too. Sites:
448
+ `/nw` step 4 env-copy, `/mw` step 4c `$MAIN` fallback.
449
+ 3. **Arbitrary cwd, main-or-worktree** (the `allocate-id.sh` startup, called from
450
+ either): the canonical `resolve_main()` in `scripts/allocate-id.sh` — it uses
451
+ `--show-toplevel`, then detects a linked worktree (`git-dir != git-common-dir`)
452
+ and walks one level up.
453
+
454
+ **Do NOT resolve `$MAIN` via `git rev-parse --git-common-dir` + `/..`.** That
455
+ returns the parent of the *git dir*, which equals the working-tree root only when
456
+ `.git` is in-tree; under `git init --separate-git-dir` it points at the wrong
457
+ directory. Always derive the root through `--show-toplevel`. `resolve_main()` is
458
+ the reference implementation; the SKILL snippets mirror it inline because the
459
+ script is not on disk inside a freshly-checked-out worktree.
460
+
384
461
  Shared files, all under `$MAIN/.worktrees/` (already gitignored):
385
462
  - `.id-alloc.lock/` — cross-process mutex (atomic `mkdir`, stale-stolen after 30s
386
463
  via the dir's own mtime; it is a directory, NOT a git ref, so it does not touch
@@ -422,6 +499,9 @@ Supports three modes:
422
499
  # Resolve $MAIN (main repo root) and $TRUNK (git.trunk_branch) up front, and
423
500
  # persist both onto the registry entry created in step 6 (R6). Every later step
424
501
  # reads them from the registry with a presence guard — never from in-context state.
502
+ # Main-checkout context: cwd is the main repo (the orchestrator invokes /nw from
503
+ # $MAIN — setup.md resolves $MAIN before spawning), so `--show-toplevel` is the
504
+ # main root and is separate-git-dir-safe. See § "Resolving the main repo root".
425
505
  MAIN="$(git rev-parse --show-toplevel)"
426
506
  # .gitignore safety (auto-heal, NON-blocking — code-mode differs from nw-docs, which
427
507
  # hard-aborts). Without `.worktrees/` ignored the worktree is git-tracked and `git status`
@@ -520,7 +600,14 @@ but are NOT on the trunk branch yet. The worktree (branched from the trunk) won'
520
600
  # when the key is absent — emit it resolved, never the literal ${paths.backlog_dir}
521
601
  # token, which is not valid bash). Same resolution as /new's card-scoped diff block.
522
602
  BACKLOG_DIR="<value of paths.backlog_dir, or 'backlog' if the key is absent>"
523
- MAIN_ROOT="$(git -C "$WORKTREE_PATH" rev-parse --show-superproject-working-tree 2>/dev/null || pwd)"
603
+ # At this point cwd is still the MAIN checkout (the `cd "$WORKTREE_PATH"` happens
604
+ # in step 4 below), so `--show-toplevel` IS the main repo root — correct for a
605
+ # normal repo, a git submodule, and a `--separate-git-dir` repo alike. (The old
606
+ # `git -C "$WORKTREE_PATH" --show-superproject-working-tree || pwd` was wrong for a
607
+ # real submodule — it returned the SUPERPROJECT, not this repo's root where the
608
+ # cards live — and only resolved otherwise by relying on this same cwd accident.
609
+ # See § "Resolving the main repo root".)
610
+ MAIN_ROOT="$(git rev-parse --show-toplevel)"
524
611
 
525
612
  # For each card in the batch, check if its YAML exists in the main repo but not in the worktree
526
613
  for CARD_FILE in $(ls "$MAIN_ROOT/$BACKLOG_DIR"/*.yml 2>/dev/null); do
@@ -547,11 +634,62 @@ npm install
547
634
  # 2. Own .next cache — already isolated since each worktree has its own
548
635
  # directory tree. The default .next inside the worktree is sufficient.
549
636
 
550
- # 3. Copy environment files from main repo root
551
- # Build requires Firebase env vars this copy is critical.
552
- MAIN_ROOT="$(git -C .. rev-parse --show-toplevel 2>/dev/null || echo '../..')"
553
- cp "$MAIN_ROOT/.env.local" .env.local 2>/dev/null || true
554
- cp "$MAIN_ROOT/.env" .env 2>/dev/null || true
637
+ # 3. Copy environment artifacts from main repo root.
638
+ # The build typically requires gitignored env secrets, so this copy is
639
+ # critical but the SET is project-specific and lives in `stack.env_files`
640
+ # (baldart.config.yml). Resolve that list and emit it LITERALLY below as a bash
641
+ # array (default ('.env.local' '.env') when the key is absent). List ONLY
642
+ # gitignored artifacts: a tracked file (e.g. an example env committed to git)
643
+ # is already in the worktree checkout — copying it is redundant. Entries may be
644
+ # FILES (cp) or DIRECTORIES (cp -r). The SAME list drives /nw, /new, and new2 —
645
+ # no per-path divergence. (A directory entry that carries remote-link/credential
646
+ # state makes the worktree act on the same remote as main; that is a per-project
647
+ # opt-in the user adds to stack.env_files / their overlay — the skill itself is
648
+ # stack-agnostic and just iterates the list.)
649
+ #
650
+ # cwd is the worktree; its parent (`.worktrees/`) lives inside the main repo,
651
+ # so `git -C ..` resolves the MAIN repo's toplevel — correct under a
652
+ # `--separate-git-dir` repo too (unlike `--git-common-dir`+parent). The old
653
+ # silent `|| echo '../..'` fallback masked a resolution failure with a relative
654
+ # guess; fail loud instead. See § "Resolving the main repo root".
655
+ MAIN_ROOT="$(git -C .. rev-parse --show-toplevel 2>/dev/null)"
656
+ [ -n "$MAIN_ROOT" ] || { echo "ERROR: cannot resolve main repo root from worktree $(pwd)" >&2; exit 1; }
657
+
658
+ # <resolve stack.env_files and emit each entry quoted; default if the key is absent>
659
+ ENV_FILES=( ".env.local" ".env" )
660
+ copied_any=0
661
+ primary_env="" # first FILE actually copied — PORT target (step 4)
662
+ for ef in "${ENV_FILES[@]}"; do
663
+ src="$MAIN_ROOT/$ef"
664
+ dest_dir="$(dirname "$ef")"
665
+ if [ -d "$src" ]; then
666
+ # Directory artifact. cp -r MERGES into an existing
667
+ # dest, so mirror it: remove first to avoid stale files lingering on a /nw
668
+ # resume. Judge success by dest existence, NOT cp's exit code (BSD `cp -r`
669
+ # returns non-zero on a broken inner symlink while still copying the rest).
670
+ mkdir -p "$dest_dir"
671
+ rm -rf "$ef"
672
+ cp -R "$src" "$dest_dir"/ 2>/dev/null || true
673
+ [ -e "$ef" ] && copied_any=1 || echo "WARN: env dir '$ef' (stack.env_files) present in main but copy failed — worktree may be incomplete." >&2
674
+ elif [ -f "$src" ]; then
675
+ # File artifact. PLAIN cp (NEVER cp -P): it dereferences a symlinked source,
676
+ # so an `.env.local -> ../shared/.env` symlink yields real content here — a
677
+ # preserved relative symlink would dangle inside `.worktrees/feat-X/`.
678
+ mkdir -p "$dest_dir"
679
+ if cp "$src" "$ef"; then copied_any=1; [ -z "$primary_env" ] && primary_env="$ef"; fi
680
+ else
681
+ # Absent in main. A missing FILE is the critical case (build likely needs it):
682
+ # WARN loudly so a later cryptic build failure is diagnosable — but NEVER exit 1
683
+ # (this runs on /new's programmatic path; aborting would strand the batch — the
684
+ # build gate in step 5 is the real enforcement). A missing DIR is best-effort
685
+ # (the worktree just isn't linked, which is safe) — we cannot stat a nonexistent
686
+ # path to know it was a dir, so a single neutral WARN per entry covers both.
687
+ echo "WARN: env artifact '$ef' (stack.env_files) not found in $MAIN_ROOT — the worktree build may fail if it is required." >&2
688
+ fi
689
+ done
690
+ if [ "$copied_any" = 0 ] && [ "${#ENV_FILES[@]}" -gt 0 ]; then
691
+ echo "WARN: NO env artifacts copied into the worktree (none of: ${ENV_FILES[*]} exist in $MAIN_ROOT). If the build needs env vars it WILL fail — add the file(s) to the main repo or fix stack.env_files." >&2
692
+ fi
555
693
 
556
694
  # 4. Pick a free dev server port (avoid 3000 used by main)
557
695
  PORT=3001
@@ -559,11 +697,15 @@ while lsof -i :$PORT -sTCP:LISTEN >/dev/null 2>&1; do
559
697
  PORT=$((PORT + 1))
560
698
  if [ $PORT -gt 3099 ]; then echo "ERROR: No free port in 3001-3099" >&2; exit 1; fi
561
699
  done
562
- # Write PORT to .env.local (append or replace existing PORT line)
563
- if grep -q "^PORT=" .env.local 2>/dev/null; then
564
- sed -i '' "s/^PORT=.*/PORT=$PORT/" .env.local
700
+ # Write PORT to the project's PRIMARY env file the first FILE that was actually
701
+ # copied (NOT ENV_FILES[0], which could be a directory entry, where
702
+ # `echo >> dir` / `grep dir` error out). Fall back to .env.local when nothing was
703
+ # copied so a port is still persisted somewhere the dev server can read.
704
+ PORT_ENV_FILE="${primary_env:-.env.local}"
705
+ if grep -q "^PORT=" "$PORT_ENV_FILE" 2>/dev/null; then
706
+ sed -i '' "s/^PORT=.*/PORT=$PORT/" "$PORT_ENV_FILE"
565
707
  else
566
- echo "PORT=$PORT" >> .env.local
708
+ echo "PORT=$PORT" >> "$PORT_ENV_FILE"
567
709
  fi
568
710
 
569
711
  # 5. ASSERT git hooks are ACTIVE — not just readable.
@@ -597,16 +739,27 @@ fi
597
739
  ### 5. Verify baseline
598
740
 
599
741
  ```bash
742
+ # Toolchain-aware (§ "Toolchain-aware gates"): when features.has_toolchain: true,
743
+ # run toolchain.commands.{typecheck,lint,build} verbatim; else the defaults below.
744
+ CFG="baldart.config.yml"; [ -f "$CFG" ] || CFG="$(git rev-parse --show-toplevel 2>/dev/null)/baldart.config.yml"
745
+ _tc() { grep -E '^[[:space:]]*has_toolchain:[[:space:]]*true' "$CFG" >/dev/null 2>&1 || return 0
746
+ grep -A20 '^toolchain:' "$CFG" 2>/dev/null | grep -A15 '^[[:space:]]*commands:' \
747
+ | grep -E "^[[:space:]]+$1:" | head -1 \
748
+ | sed -E "s/.*$1:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" | sed -E 's/[[:space:]]+$//'; }
749
+
750
+ TC_TC=$(_tc typecheck); TC_LINT=$(_tc lint); TC_BUILD=$(_tc build)
751
+
600
752
  # TypeScript + lint (fast)
601
- npx tsc --noEmit
602
- npx eslint --max-warnings=0 src/
753
+ eval "${TC_TC:-npx tsc --noEmit}"
754
+ eval "${TC_LINT:-npx eslint --max-warnings=0 src/}"
603
755
 
604
756
  # Full build verification (required — confirms worktree is functional)
605
- npm run build
757
+ eval "${TC_BUILD:-npm run build}"
606
758
  ```
607
759
 
608
760
  If build fails → STOP and report. Do NOT continue — the worktree is broken.
609
761
  If only tsc/lint fails → report but continue (the trunk branch should be clean, may be a transient issue).
762
+ (Severity is by-gate as stated here; the toolchain resolver only changes *which* command runs — § "Toolchain-aware gates".)
610
763
 
611
764
  ### 6. Update registry
612
765
 
@@ -673,12 +826,32 @@ fi
673
826
 
674
827
  ### 2. Env sync check
675
828
 
676
- Compare main repo `.env.local` modification time with registry `envSyncedAt`.
677
- If main `.env.local` is newer, WARN:
829
+ Compare the NEWEST modification time among the `stack.env_files` artifacts in the
830
+ main repo against the registry `envSyncedAt`. If any is newer, WARN. Use a
831
+ portable mtime read (`stat -f %m` is BSD/macOS, `stat -c %Y` is GNU/Linux — the
832
+ snippet runs on both) and scan directory entries recursively (a directory's own
833
+ mtime does NOT bump when a nested file is edited in place):
678
834
 
679
- ```
680
- Warning: Main repo .env.local has changed since this worktree was created.
681
- Consider updating the worktree's .env.local before merging.
835
+ ```bash
836
+ # Resolve stack.env_files the same way step 4 does; emit it literally here.
837
+ ENV_FILES=( ".env.local" ".env" )
838
+ mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null; }
839
+ newest=0
840
+ for ef in "${ENV_FILES[@]}"; do
841
+ src="$MAIN/$ef"
842
+ if [ -d "$src" ]; then
843
+ # newest mtime of any file inside the dir (recursive)
844
+ while IFS= read -r f; do m=$(mtime "$f"); [ "${m:-0}" -gt "$newest" ] && newest=$m; done \
845
+ < <(find "$src" -type f 2>/dev/null)
846
+ elif [ -f "$src" ]; then
847
+ m=$(mtime "$src"); [ "${m:-0}" -gt "$newest" ] && newest=$m
848
+ fi
849
+ done
850
+ SYNCED=$(mtime "$WORKTREE_PATH/.env-synced-marker" 2>/dev/null) # or parse registry envSyncedAt → epoch
851
+ if [ "${newest:-0}" -gt "${SYNCED:-0}" ]; then
852
+ echo "Warning: a main-repo env artifact (stack.env_files) changed since this worktree was created."
853
+ echo "Consider re-copying the env files into the worktree before merging."
854
+ fi
682
855
  ```
683
856
 
684
857
  ### 3. Pre-merge safety commit + checks inside the worktree
@@ -717,15 +890,27 @@ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
717
890
  fi
718
891
 
719
892
  # 2. Run quality checks
720
- # Lint only changed files (vs the trunk branch)
721
- npx eslint --max-warnings=0 $(git diff --name-only "origin/$TRUNK" -- '*.ts' '*.tsx')
722
- npx tsc --noEmit
893
+ # Toolchain-aware "Toolchain-aware gates"): when features.has_toolchain: true,
894
+ # run toolchain.commands.{lint,typecheck,build,test} verbatim; else the defaults below.
895
+ CFG="baldart.config.yml"; [ -f "$CFG" ] || CFG="$(git rev-parse --show-toplevel 2>/dev/null)/baldart.config.yml"
896
+ _tc() { grep -E '^[[:space:]]*has_toolchain:[[:space:]]*true' "$CFG" >/dev/null 2>&1 || return 0
897
+ grep -A20 '^toolchain:' "$CFG" 2>/dev/null | grep -A15 '^[[:space:]]*commands:' \
898
+ | grep -E "^[[:space:]]+$1:" | head -1 \
899
+ | sed -E "s/.*$1:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" | sed -E 's/[[:space:]]+$//'; }
900
+ TC_LINT=$(_tc lint); TC_TC=$(_tc typecheck); TC_BUILD=$(_tc build); TC_TEST=$(_tc test)
901
+
902
+ # Lint — default is changed-files-scoped (vs trunk); a configured whole-tree command
903
+ # (e.g. `npx biome check .`) runs verbatim by the consumer's contract (§ carve-out).
904
+ CHANGED_TS=$(git diff --name-only "origin/$TRUNK" -- '*.ts' '*.tsx')
905
+ eval "${TC_LINT:-npx eslint --max-warnings=0 $CHANGED_TS}"
906
+ eval "${TC_TC:-npx tsc --noEmit}"
723
907
 
724
908
  # Full build (required before PR per AGENTS.md)
725
- npm run build
909
+ eval "${TC_BUILD:-npm run build}"
726
910
 
727
- # Tests
728
- npm run test 2>/dev/null || true
911
+ # Tests — BEST-EFFORT by design: never a STOP trigger. Keep the `|| true` swallow
912
+ # whether configured or default (§ carve-out: the /mw test line stays best-effort).
913
+ { eval "${TC_TEST:-npm run test}"; } 2>/dev/null || true
729
914
  ```
730
915
 
731
916
  If lint, tsc, or build fails → report and STOP. Do NOT proceed to PR.
@@ -898,7 +1083,13 @@ done
898
1083
 
899
1084
  # Continue rebase + verify build (no stash to restore — step 4b never stashes)
900
1085
  git rebase --continue
901
- npm run build
1086
+ # Toolchain-aware (§ "Toolchain-aware gates"): toolchain.commands.build verbatim when set, else default.
1087
+ CFG="baldart.config.yml"; [ -f "$CFG" ] || CFG="$(git rev-parse --show-toplevel 2>/dev/null)/baldart.config.yml"
1088
+ _tc() { grep -E '^[[:space:]]*has_toolchain:[[:space:]]*true' "$CFG" >/dev/null 2>&1 || return 0
1089
+ grep -A20 '^toolchain:' "$CFG" 2>/dev/null | grep -A15 '^[[:space:]]*commands:' \
1090
+ | grep -E "^[[:space:]]+$1:" | head -1 \
1091
+ | sed -E "s/.*$1:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" | sed -E 's/[[:space:]]+$//'; }
1092
+ TC_BUILD=$(_tc build); eval "${TC_BUILD:-npm run build}"
902
1093
  ```
903
1094
 
904
1095
  If build fails after rebase → STOP and report. The rebase introduced incompatibilities.
@@ -911,11 +1102,16 @@ Read `git.merge_strategy` from `baldart.config.yml` (default: `pr`):
911
1102
 
912
1103
  ```bash
913
1104
  # $MAIN and $TRUNK were resolved in step 1 (read from the registry entry, R6) and
914
- # presence-guarded. Fall back to deriving $MAIN from git-common-dir ONLY if it is
1105
+ # presence-guarded. Fall back to deriving $MAIN from the worktree ONLY if it is
915
1106
  # still unset — never silently re-derive over a value the registry already supplied.
916
1107
  if [ -z "$MAIN" ]; then
917
- MAIN=$(git -C "$WORKTREE_PATH" rev-parse --git-common-dir)/..
918
- MAIN=$(cd "$MAIN" && pwd)
1108
+ # Canonical worktree→main resolution (see § "Resolving the main repo root"):
1109
+ # cd INTO the worktree so `git -C ..` resolves the MAIN repo's toplevel. NOT
1110
+ # `--git-common-dir`+`/..` — that returns the parent of the git dir (wrong under
1111
+ # `git init --separate-git-dir`), and appending `/..` to a possibly-relative
1112
+ # `--git-common-dir` under `git -C` resolves against the wrong base.
1113
+ MAIN="$(cd "$WORKTREE_PATH" 2>/dev/null && git -C .. rev-parse --show-toplevel 2>/dev/null)"
1114
+ [ -n "$MAIN" ] || { echo "ERROR: cannot resolve \$MAIN from worktree $WORKTREE_PATH (registry mainRoot was empty)." >&2; exit 1; }
919
1115
  fi
920
1116
 
921
1117
  # Resolve strategy from baldart.config.yml (default: pr)
@@ -1093,8 +1289,15 @@ fi
1093
1289
  ### 5. Post-merge verification
1094
1290
 
1095
1291
  ```bash
1096
- npx tsc --noEmit
1097
- npm run build
1292
+ # Toolchain-aware (§ "Toolchain-aware gates"): toolchain.commands.{typecheck,build} verbatim when set, else defaults.
1293
+ CFG="baldart.config.yml"; [ -f "$CFG" ] || CFG="$(git rev-parse --show-toplevel 2>/dev/null)/baldart.config.yml"
1294
+ _tc() { grep -E '^[[:space:]]*has_toolchain:[[:space:]]*true' "$CFG" >/dev/null 2>&1 || return 0
1295
+ grep -A20 '^toolchain:' "$CFG" 2>/dev/null | grep -A15 '^[[:space:]]*commands:' \
1296
+ | grep -E "^[[:space:]]+$1:" | head -1 \
1297
+ | sed -E "s/.*$1:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" | sed -E 's/[[:space:]]+$//'; }
1298
+ TC_TC=$(_tc typecheck); TC_BUILD=$(_tc build)
1299
+ eval "${TC_TC:-npx tsc --noEmit}"
1300
+ eval "${TC_BUILD:-npm run build}"
1098
1301
  ```
1099
1302
 
1100
1303
  If post-merge build fails → STOP and report. Do NOT cleanup worktree (may need to investigate).
@@ -1181,7 +1384,7 @@ Active worktrees:
1181
1384
  Age: 2 days
1182
1385
  Status: 3 uncommitted changes
1183
1386
  Build: verified ✓
1184
- Env: in sync | STALE (main .env.local changed)
1387
+ Env: in sync | STALE (a main env artifact changed)
1185
1388
 
1186
1389
  2. BUG-0500 fix-auth
1187
1390
  Branch: feat/BUG-0500-fix-auth
@@ -1285,5 +1488,5 @@ Cleanup complete:
1285
1488
  - If merge conflicts during /mw, STOP and ask the user. Do not auto-resolve.
1286
1489
  - Always verify `.worktrees/` is in `.gitignore` before creating it (the nw-docs pre-flight in step 0 enforces this; add it before running if absent).
1287
1490
  - Commit lock protocol: Each worktree has its own `COMMIT_LOCK` in its git dir — no cross-worktree interference.
1288
- - Port persistence: On dev server restart, reuse the port from registry (grep .env.local for PORT=) instead of picking a new one.
1491
+ - Port persistence: On dev server restart, reuse the port from registry (or grep the worktree's primary env file — the first FILE entry of `stack.env_files` that was copied, default `.env.local` for `PORT=`) instead of picking a new one.
1289
1492
  - **NEVER use `git stash` in worktrees.** Stashes are globally shared across all worktrees (`refs/stash` via `git.commondir`). A stash created in one worktree or in the main repo can be popped in another, causing conflicts, data loss, and cascading merge failures (see FEAT-0522 incident). Use explicit file staging only — the file ownership map ensures no overlap between cards. The stash pattern in AGENTS.md/CLAUDE.md applies ONLY to the main repo working tree.
@@ -8,8 +8,8 @@
8
8
  # backlog scan cannot see IDs that are still in flight on an unmerged sibling
9
9
  # worktree branch.
10
10
  #
11
- # Mechanism: every worktree shares the same main repo root (resolved from
12
- # `git rev-parse --git-common-dir`), and `.worktrees/` lives there and is
11
+ # Mechanism: every worktree shares the same main repo root (resolved by
12
+ # `resolve_main()` below), and `.worktrees/` lives there and is
13
13
  # gitignored. We anchor a lock + a per-prefix high-water-mark file there, so a
14
14
  # reservation is atomic across every worktree on this machine. The high-water
15
15
  # bumped under the lock is the correctness mechanism; the max() against the real
@@ -34,15 +34,29 @@ LOCK_MAX_TRIES=150 # ~30s at 0.2s/try
34
34
 
35
35
  err() { printf '%s\n' "$*" >&2; }
36
36
 
37
- # --- Resolve the shared main repo root from any worktree -------------------
37
+ # --- Resolve the main repo root from the main checkout OR any worktree -----
38
+ # CANONICAL main-repo-root resolution for the whole framework (mirrored inline in
39
+ # worktree-manager/SKILL.md + prd, where this script is not reachable from a
40
+ # worktree checkout). Built on `--show-toplevel`, NOT `--git-common-dir`+parent:
41
+ # the latter returns the PARENT OF THE GIT DIR, which is wrong under
42
+ # `git init --separate-git-dir` (the git dir lives outside the working tree).
43
+ # `--show-toplevel` is the true working-tree root in every case. From a linked
44
+ # worktree `--show-toplevel` returns the WORKTREE root, so we detect that
45
+ # (git-dir != git-common-dir) and walk one level up out of `.worktrees/` — the
46
+ # worktree path is always `<main>/.worktrees/<name>` (SKILL.md R8) — and resolve
47
+ # the main checkout's toplevel there. Fails (return 1) rather than print garbage.
38
48
  resolve_main() {
39
- local common
49
+ local top gd common
50
+ top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
51
+ gd="$(git rev-parse --git-dir 2>/dev/null)" || return 1
40
52
  common="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
41
- case "$common" in
42
- /*) ;; # already absolute
43
- *) common="$(pwd)/$common" ;; # relative (we're in the main repo) → absolutise
44
- esac
45
- (cd "$common/.." 2>/dev/null && pwd) || return 1
53
+ if [ "$gd" = "$common" ]; then
54
+ printf '%s\n' "$top" # main checkout — toplevel IS the main repo root
55
+ else
56
+ # linked worktree — the main root is the toplevel of the directory that
57
+ # contains `.worktrees/` (one level above this worktree).
58
+ (cd "$top/.." 2>/dev/null && git rev-parse --show-toplevel 2>/dev/null) || return 1
59
+ fi
46
60
  }
47
61
 
48
62
  # --- Read a paths.* / git.* scalar from baldart.config.yml -----------------
@@ -62,6 +62,10 @@ const paths = cfg.paths || {}
62
62
  const gitCfg = cfg.git || {}
63
63
  const stack = cfg.stack || {}
64
64
  const highRisk = paths.high_risk_modules || []
65
+ // Gitignored env artifacts copied into the worktree (v4.46.0). SAME contract as
66
+ // /nw + /new (worktree-manager step 4): the SSOT is stack.env_files; never list a
67
+ // tracked file (.env.example is already in the checkout). Default minimal set.
68
+ const ENV_FILES = (Array.isArray(stack.env_files) && stack.env_files.length) ? stack.env_files : ['.env.local', '.env']
65
69
  const mergeStrategy = gitCfg.merge_strategy || 'pr'
66
70
  // Stack-agnostic schema-deploy safety invariant (v4.33.0). When true, a remote
67
71
  // schema/RLS/index/migration deploy is auto-executed ONLY from the trunk branch;
@@ -260,7 +264,7 @@ try {
260
264
  `ROLE BOUNDARY (specialization integrity): you are the OPS/GIT agent. You NEVER edit source or doc files — any needed content change belongs to the coder specialist; report it instead.\n\n` +
261
265
  `DETERMINISTIC GATE POLICIES (NO user prompts):\n` +
262
266
  `• G1 dirty-tree (main repo ${MAIN}): partition framework-managed noise exactly as setup.md step 3 ($METRICS=${METRICS}, .baldart/generated|state.json|skill-conflicts.json — NOT overlays/). Genuine user work → auto-stash 'baldart-new2-${firstCard}' (main checkout) and record the label. Never commit/abort/prompt.\n` +
263
- `• Worktree (setup.md step 4): create ONE code worktree off ${TRUNK}; install deps; assign a port; run the baseline (tsc+lint+build). Copy ONLY the artifacts needed (env/.env.local/.env.example/supabase/.temp) do NOT bulk-copy untracked files from the main repo (avoids stray backlog cards in the worktree). Use the git-authoritative idempotency pre-check. E2: baseline FAILS → do NOT fix it yourself (role boundary — the coder specialist repairs it); return baseline:'fail' + a baselineLog precise enough for a coder to act (failing command, error excerpt, suspect files). Wrap the build in \`timeout 600 <build-cmd>\` (10 min); if killed, return baseline:'timeout' + the partial log. On baseline PASS, VERIFY the worktree on disk and return EVIDENCE (not just a flag): set worktreeVerified:true ONLY after running \`git -C ${MAIN} worktree list --porcelain\` (the worktree path MUST appear in the output) AND \`test -d <wt>/node_modules\` AND confirming the branch; put the LITERAL stdout into worktreeEvidence{ worktreeListPorcelain, artifactsLs:\`ls -la <wt>/node_modules <wt>/.next 2>/dev/null | head\`, baselineLogTail }. The workflow string-matches this evidence — NEVER report worktreeVerified:true without actually running the commands.\n` +
267
+ `• Worktree (setup.md step 4): create ONE code worktree off ${TRUNK}; install deps; assign a port; run the baseline (tsc+lint+build). Copy the gitignored env artifacts in stack.env_files (resolved: ${JSON.stringify(ENV_FILES)}) — FILES via \`cp\` (plain cp, dereferences symlinks; never cp -P), DIRS via \`cp -r\`; a missing FILE → WARN (never abort). Do NOT add .env.example (tracked → already in the checkout) and do NOT bulk-copy untracked files from the main repo (avoids stray backlog cards in the worktree). Use the git-authoritative idempotency pre-check. E2: baseline FAILS → do NOT fix it yourself (role boundary — the coder specialist repairs it); return baseline:'fail' + a baselineLog precise enough for a coder to act (failing command, error excerpt, suspect files). Wrap the build in \`timeout 600 <build-cmd>\` (10 min); if killed, return baseline:'timeout' + the partial log. On baseline PASS, VERIFY the worktree on disk and return EVIDENCE (not just a flag): set worktreeVerified:true ONLY after running \`git -C ${MAIN} worktree list --porcelain\` (the worktree path MUST appear in the output) AND \`test -d <wt>/node_modules\` AND confirming the branch; put the LITERAL stdout into worktreeEvidence{ worktreeListPorcelain, artifactsLs:\`ls -la <wt>/node_modules <wt>/.next 2>/dev/null | head\`, baselineLogTail }. The workflow string-matches this evidence — NEVER report worktreeVerified:true without actually running the commands.\n` +
264
268
  codexResolveBullet +
265
269
  g3Bullet +
266
270
  `• G4 card-field validation (setup.md 1b/1c): card missing requirements/acceptance_criteria/files_likely_touched → EXCLUDE (excluded[] + reason). Never HALT for one bad card.\n` +
@@ -64,6 +64,21 @@ protocol. The `/new` workflow scripts receive the resolved config via their
64
64
  `args.config` payload (the consuming skill passes `baldart.config.yml`) — they
65
65
  must never hard-code project facts (see the workflows contamination contract).
66
66
 
67
+ Since v4.42.0 the **execution-side** gates resolve through this protocol too: the
68
+ `worktree-manager` skill (`/nw` baseline + `/mw` pre-merge / rebase / post-merge
69
+ build gates), the classic `/new` reference suite (`implement`, `review-cycle`,
70
+ `completeness`, `final-review`, `commit`, `team-mode` — cited as `§ "Toolchain
71
+ gates"` from the `/new` core), and the standalone `/bug` + `/simplify` verify
72
+ steps. Two execution-context-specific resolution mechanisms are used: a markdown
73
+ **prose note** where a full model reads the skill and writes the gate command
74
+ (the `/new` references, `/bug`, `/simplify`, the agents), and an inline **shell
75
+ resolver** in `worktree-manager` (it has no `args.config` and its baseline runs in
76
+ a weak/background subagent, so it resolves `toolchain.commands.*` from
77
+ `baldart.config.yml` on disk — the same way it already resolves `git.trunk_branch`
78
+ — rather than relying on a prose note a weak model could skip). `npm install` is
79
+ **not** a gate (there is no `commands.install` key), and `markdownlint` is not in
80
+ the curated map — both stay as written everywhere.
81
+
67
82
  ## Fallback rules
68
83
 
69
84
  - A configured command that EXITS NON-ZERO is a genuine gate **FAIL** — do not
@@ -162,6 +162,12 @@ stack:
162
162
 
163
163
  # Schema-deploy safety invariant (since v4.33.0). Default false (no-op).
164
164
  schema_deploy_from_trunk_only: false # true → /new + new2 auto-deploy schema only from git.trunk_branch
165
+
166
+ # Gitignored env artifacts copied into each git worktree (v4.46.0).
167
+ env_files: # default ['.env.local', '.env']
168
+ - .env.local
169
+ - .env
170
+ # - some/state-dir # a DIR entry is copied with cp -r (project-specific opt-in)
165
171
  ```
166
172
 
167
173
  Skills propose only `canonical` libraries and refuse `forbidden` ones. Empty
@@ -196,6 +202,20 @@ the skill/agent set:
196
202
  multiple cards in parallel worktrees against one shared remote datastore. The actual
197
203
  command + env loader stay in the project overlay — core only enforces the branch gate.
198
204
  `baldart configure` defaults it to `true` when it detects parallel worktree usage.
205
+ - `stack.env_files` (list, v4.46.0+, default `['.env.local', '.env']`) is the
206
+ SSOT for **gitignored env artifacts copied into each git worktree** by
207
+ `worktree-manager` (`/nw`), reused identically by `/new` and `new2` — no
208
+ per-path divergence. A worktree is a fresh checkout, so any gitignored env the
209
+ build needs must be copied from main. **List only gitignored artifacts** — a
210
+ tracked example env is already present in every checkout. Entries may be files
211
+ (copied with `cp`, which dereferences symlinks) or directories (copied with
212
+ `cp -r`). The mechanism is **stack-agnostic**: add whatever your project needs.
213
+ A directory that carries remote-link/credential state makes the worktree act on
214
+ the same remote as main — add it intentionally (and consider pairing with
215
+ `schema_deploy_from_trunk_only: true`). Such project-specific opinions live in
216
+ the project's own config value / overlay, not in the generic framework. A
217
+ missing FILE is WARNed at copy time (never fatal); a build that truly needs it
218
+ fails clearly.
199
219
  - `security-reviewer` evaluates access rules per `stack.database`
200
220
  (Firestore rules, Supabase RLS, Mongo validators, DynamoDB IAM,
201
221
  Postgres RLS+GRANT).
@@ -122,6 +122,24 @@ stack:
122
122
  # "render" | "fly" | "self-hosted" | "none" | "".
123
123
  deployment: ""
124
124
 
125
+ # Gitignored environment artifacts copied into each git worktree (since
126
+ # v4.46.0). A worktree is a fresh checkout, so any GITIGNORED env file/dir the
127
+ # build/run needs (it is NOT in the checkout) must be copied from the main repo.
128
+ # worktree-manager (`/nw`) reads this list; `/new` + new2 reuse the SAME list
129
+ # (no per-path divergence). Rules:
130
+ # - List ONLY gitignored artifacts. A tracked file (e.g. an example env
131
+ # committed to git) is already in every checkout — listing it is redundant.
132
+ # - Entries may be FILES (copied with `cp`) or DIRECTORIES (copied with
133
+ # `cp -r`). Add any project-specific entry your stack needs (e.g. a CLI's
134
+ # gitignored local-state dir). NOTE: a directory carrying remote-link or
135
+ # credential state makes the worktree act on the same remote as main — add
136
+ # such a dir intentionally (and consider schema_deploy_from_trunk_only).
137
+ # - A missing FILE here is WARNed at copy time (never fatal); a build that
138
+ # truly needs it fails clearly. Default: the minimal secret set.
139
+ env_files:
140
+ - .env.local
141
+ - .env
142
+
125
143
  # Schema-deploy-from-trunk-only safety invariant (since v4.33.0). Stack-agnostic
126
144
  # guard against migration-history desync / "code-ahead-of-schema" production
127
145
  # outages on projects that develop multiple cards in PARALLEL git worktrees
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.43.1",
3
+ "version": "4.46.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"
@@ -384,6 +384,17 @@ function detect(cwd = process.cwd()) {
384
384
  else if (exists('app.yaml') || exists('cloudbuild.yaml')) detectedDeployment = 'gcp';
385
385
  // Note: bare Dockerfile → leave empty; user picks "self-hosted" or actual target.
386
386
 
387
+ // ---- Worktree env artifacts (stack.env_files) --------------------------
388
+ // GITIGNORED env files that must be copied into each worktree (they are NOT in
389
+ // the git checkout). Probe only the UNIVERSAL gitignored env conventions
390
+ // (.env.local / .env) — never a tracked file like .env.example, and never a
391
+ // stack-specific artifact (those are a per-PROJECT opinion: a project that
392
+ // needs e.g. a tool's local-state dir adds it to stack.env_files itself / via
393
+ // its overlay; the generic installer stays stack-agnostic). Fall back to the
394
+ // minimal set so the list is never empty.
395
+ const detectedEnvFiles = ['.env.local', '.env'].filter((f) => exists(f));
396
+ if (detectedEnvFiles.length === 0) detectedEnvFiles.push('.env.local', '.env');
397
+
387
398
  const detected = {
388
399
  paths: {
389
400
  design_system: designSystemPath,
@@ -433,6 +444,8 @@ function detect(cwd = process.cwd()) {
433
444
  // Stack-agnostic schema-deploy safety invariant (v4.33.0). Defaulted ON when
434
445
  // parallel worktrees are detected (the desync-prone topology), else false.
435
446
  schema_deploy_from_trunk_only: usesWorktrees,
447
+ // Gitignored env artifacts copied into each worktree (v4.46.0).
448
+ env_files: detectedEnvFiles,
436
449
  // New: surface the detected monorepo + DS signals so skills can read them.
437
450
  monorepo: isMonorepo ? {
438
451
  detected: true,
@@ -1057,6 +1070,20 @@ async function interactivePrompts(merged, detected) {
1057
1070
  'confirm'
1058
1071
  );
1059
1072
 
1073
+ // Worktree env artifacts (stack.env_files, v4.46.0). Gitignored files/dirs
1074
+ // copied into each worktree (stack-agnostic — the user lists whatever their
1075
+ // project needs; a directory entry is copied recursively).
1076
+ const envFilesDefault = (Array.isArray(merged.stack.env_files) && merged.stack.env_files.length
1077
+ ? merged.stack.env_files
1078
+ : detected.stack.env_files) || ['.env.local', '.env'];
1079
+ const envFilesAns = await promptForKey(
1080
+ 'Gitignored env files/dirs copied into each worktree — comma-separated (gitignored only; NEVER a tracked file like .env.example; a directory entry is copied recursively)',
1081
+ envFilesDefault.join(',')
1082
+ );
1083
+ merged.stack.env_files = envFilesAns
1084
+ ? envFilesAns.split(',').map((s) => s.trim()).filter(Boolean)
1085
+ : [];
1086
+
1060
1087
  return merged;
1061
1088
  }
1062
1089
 
@@ -1141,6 +1168,7 @@ async function configure(opts = {}) {
1141
1168
  `Auth provider: ${detected.stack.auth_provider || '—'}`,
1142
1169
  `Framework: ${detected.stack.framework || '—'}`,
1143
1170
  `Deployment: ${detected.stack.deployment || '—'}`,
1171
+ `Worktree env: ${(detected.stack.env_files || []).join(', ') || '—'}`,
1144
1172
  ]);
1145
1173
 
1146
1174
  if (opts.nonInteractive) {
@@ -1285,12 +1285,16 @@ async function update(options = {}, unknownArgs = []) {
1285
1285
  const missingGit = Object.keys(tpl.git || {})
1286
1286
  .filter((k) => !(k in (cur2.git || {})));
1287
1287
  // Scalar top-level stack.* keys — e.g. stack.database,
1288
- // stack.auth_provider, stack.framework, stack.deployment (strings) and
1289
- // stack.schema_deploy_from_trunk_only (boolean, since v4.33.0). Sub-object
1290
- // keys (charting, animation, testing) are intentionally skipped:
1291
- // their drift is handled by individual sub-prompts in configure.
1288
+ // stack.auth_provider, stack.framework, stack.deployment (strings),
1289
+ // stack.schema_deploy_from_trunk_only (boolean, since v4.33.0) AND
1290
+ // top-level ARRAY keys (stack.env_files, v4.46.0
1291
+ // `typeof [] === 'object'`, so it needs Array.isArray to be caught;
1292
+ // without it the new key would silently never be asked). Sub-OBJECT
1293
+ // keys (charting, animation, testing) stay skipped — Array.isArray({})
1294
+ // is false, so they are not flagged — their drift is handled by
1295
+ // individual sub-prompts in configure.
1292
1296
  const missingStack = Object.keys(tpl.stack || {})
1293
- .filter((k) => ['string', 'boolean'].includes(typeof tpl.stack[k]))
1297
+ .filter((k) => ['string', 'boolean'].includes(typeof tpl.stack[k]) || Array.isArray(tpl.stack[k]))
1294
1298
  .filter((k) => !(k in (cur2.stack || {})));
1295
1299
  // Top-level nested config namespaces (e.g. `graph:` since v4.21.0).
1296
1300
  // Unlike `lsp:` (which propagates only via its `has_lsp_layer` flag),