baldart 4.45.0 → 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,22 @@ 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
+
8
24
  ## [4.45.0] - 2026-06-15
9
25
 
10
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).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.45.0
1
+ 4.46.0
@@ -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.
@@ -20,6 +20,7 @@ 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.
23
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.
24
25
  - Protocol reference: `framework/agents/project-context.md`. Skills must ASK when a needed key is missing — never assume.
25
26
 
@@ -108,7 +109,7 @@ backlog cards, design references — and never touch source code.
108
109
  What docs mode SKIPS vs the standard flow:
109
110
  - No `npm install` (no `node_modules/` inside the worktree)
110
111
  - No port allocation (no dev server)
111
- - No `.env`/`.env.local` copy
112
+ - No env-artifact (`stack.env_files`) copy
112
113
  - No `npm run build` baseline verification
113
114
  - No lint / tsc baseline checks
114
115
  - No `--dev` server bootstrap
@@ -240,7 +241,7 @@ git -C "$MAIN_ROOT" worktree add "$WORKTREE_REL" -b "$BRANCH" "origin/$TRUNK"
240
241
  }
241
242
  ```
242
243
 
243
- **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`,
244
245
  `npx tsc`, `npx eslint`, dev server start, `git checkout`/`switch`/`branch` on main.
245
246
 
246
247
  ### mw-docs programmatic
@@ -633,8 +634,19 @@ npm install
633
634
  # 2. Own .next cache — already isolated since each worktree has its own
634
635
  # directory tree. The default .next inside the worktree is sufficient.
635
636
 
636
- # 3. Copy environment files from main repo root
637
- # Build requires Firebase env vars this copy is critical.
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
+ #
638
650
  # cwd is the worktree; its parent (`.worktrees/`) lives inside the main repo,
639
651
  # so `git -C ..` resolves the MAIN repo's toplevel — correct under a
640
652
  # `--separate-git-dir` repo too (unlike `--git-common-dir`+parent). The old
@@ -642,8 +654,42 @@ npm install
642
654
  # guess; fail loud instead. See § "Resolving the main repo root".
643
655
  MAIN_ROOT="$(git -C .. rev-parse --show-toplevel 2>/dev/null)"
644
656
  [ -n "$MAIN_ROOT" ] || { echo "ERROR: cannot resolve main repo root from worktree $(pwd)" >&2; exit 1; }
645
- cp "$MAIN_ROOT/.env.local" .env.local 2>/dev/null || true
646
- cp "$MAIN_ROOT/.env" .env 2>/dev/null || true
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
647
693
 
648
694
  # 4. Pick a free dev server port (avoid 3000 used by main)
649
695
  PORT=3001
@@ -651,11 +697,15 @@ while lsof -i :$PORT -sTCP:LISTEN >/dev/null 2>&1; do
651
697
  PORT=$((PORT + 1))
652
698
  if [ $PORT -gt 3099 ]; then echo "ERROR: No free port in 3001-3099" >&2; exit 1; fi
653
699
  done
654
- # Write PORT to .env.local (append or replace existing PORT line)
655
- if grep -q "^PORT=" .env.local 2>/dev/null; then
656
- 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"
657
707
  else
658
- echo "PORT=$PORT" >> .env.local
708
+ echo "PORT=$PORT" >> "$PORT_ENV_FILE"
659
709
  fi
660
710
 
661
711
  # 5. ASSERT git hooks are ACTIVE — not just readable.
@@ -776,12 +826,32 @@ fi
776
826
 
777
827
  ### 2. Env sync check
778
828
 
779
- Compare main repo `.env.local` modification time with registry `envSyncedAt`.
780
- 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):
781
834
 
782
- ```
783
- Warning: Main repo .env.local has changed since this worktree was created.
784
- 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
785
855
  ```
786
856
 
787
857
  ### 3. Pre-merge safety commit + checks inside the worktree
@@ -1314,7 +1384,7 @@ Active worktrees:
1314
1384
  Age: 2 days
1315
1385
  Status: 3 uncommitted changes
1316
1386
  Build: verified ✓
1317
- Env: in sync | STALE (main .env.local changed)
1387
+ Env: in sync | STALE (a main env artifact changed)
1318
1388
 
1319
1389
  2. BUG-0500 fix-auth
1320
1390
  Branch: feat/BUG-0500-fix-auth
@@ -1418,5 +1488,5 @@ Cleanup complete:
1418
1488
  - If merge conflicts during /mw, STOP and ask the user. Do not auto-resolve.
1419
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).
1420
1490
  - Commit lock protocol: Each worktree has its own `COMMIT_LOCK` in its git dir — no cross-worktree interference.
1421
- - 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.
1422
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.
@@ -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` +
@@ -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.45.0",
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),