baldart 4.52.3 → 4.53.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,28 @@ 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.53.0] - 2026-06-18
9
+
10
+ **Worktree setup for `/new` / `/nw` / `new2` is now a deterministic SSOT script — no model in the loop for pure plumbing.** Diagnosed from a real `/new FEAT-0036 -full` run where the Phase-0 worktree-setup subagent **created the worktree but went idle before `npm install`** (node_modules missing, no registry entry), forcing the orchestrator to burn turns probing → messaging the agent → waiting → cleaning the half-built orphan → falling back to inline `/nw` (which reloads the ~1200-line skill body into the orchestrator — the exact cost the delegation was meant to avoid). Same root cause as the earlier haiku *fabrication* (a well-formed `baseline: pass` with nothing on disk, 2/2): a **model in the loop for mechanical work** — it either fabricates the expected return or stalls mid-execution. The fix removes the model: the entire build sequence (worktree add → sync untracked cards → install → copy `stack.env_files` → allocate port → write registry → baseline) now lives **once** in `framework/.claude/skills/worktree-manager/scripts/setup-worktree.sh` and is invoked identically by all three callers as a background `Bash`. A deterministic script cannot fabricate or stall: it does the work or fails honestly with a log + a structured manifest. The orchestrator's disk-verification gate (setup.md step 6a) and git-authoritative idempotency pre-check (step 4a2) are unchanged — defence-in-depth survives the redesign. Non-interactive HARD (stdin closed, `CI=1`, build under `timeout 600`); port allocation is atomic under a `.worktrees/.wt-setup.lock` mutex (mirrors `allocate-id.sh`) so concurrent `/nw` runs never collide; the registry is written via `node` (atomic tmp+rename, no jq dependency); the script never self-repairs a failing baseline (role boundary — the coder does, downstream). Verified end-to-end on a throwaway repo: happy path (worktree + env+PORT + card-sync + `buildVerified:true` + manifest), collision (loud fail, exit 3), build-fail (`baseline:fail`, port held, `buildVerified:false`), and second-worktree port allocation (3001+3002 reserved → 3003). Opt-in-with-fallback: a pre-this-release subtree without the script falls back to inline `/nw`. **MINOR** (new shipped SSOT asset + behaviour change to existing skills; backward-compatible, no install-layout or command change, no `baldart.config.yml` key — `stack.env_files` / `toolchain.commands.*` are reused).
11
+
12
+ ### Added
13
+
14
+ - **`framework/.claude/skills/worktree-manager/scripts/setup-worktree.sh`** — the single source of truth for code-worktree creation + baseline, consumed by `/nw`, `/new` (setup.md step 4), and `new2`. Resolves config (`$MAIN`/`$TRUNK`/`stack.env_files`/`toolchain.commands.*`/port range) from args with `baldart.config.yml` fallback; writes a stable manifest block (`status`/`error`/`worktree_path`/`branch`/`port`/`created_at`/`baseline`/`baseline_log`) on every exit path.
15
+
16
+ ### Changed
17
+
18
+ - **`framework/.claude/skills/new/references/setup.md`** — Phase-0 step 4 replaces the Sonnet-subagent-via-Skill delegation (+ its `/nw`-inline fallback chain) with a background launch of `setup-worktree.sh`; step 5 barrier + step 6a disk gate now read the manifest; fallback collapses to `script → inline /nw → HALT` (honest/deterministic failures are not fallback-eligible). `SKILL.md` worktree `Created:` note updated.
19
+ - **`framework/.claude/skills/worktree-manager/SKILL.md`** — `/nw` steps 3-6 (create/isolate/baseline/registry) collapsed to an invocation of `setup-worktree.sh` + a prose description of what it does; steps 7-8 read `worktree_path`/`port` from the manifest instead of in-process shell vars.
20
+ - **`framework/.claude/workflows/new2.js`** — the Pre-flight agent's worktree bullet now runs `setup-worktree.sh` instead of hand-rolling the sequence; it still independently verifies the worktree on disk for the (structurally weaker) E2.5 evidence gate, and falls back to the inline sequence if the script is absent.
21
+
22
+ ## [4.52.4] - 2026-06-18
23
+
24
+ **SSOT hygiene: `/cont` (context-primer) no longer restates the code-search tier hierarchy inline.** Follow-up to v4.52.3. The prompt template `context-primer` hands to `codebase-architect` re-stated the "prefer LSP/graph over grep for symbols" guidance inline (step 3a) instead of citing the protocol modules — so after v4.52.3 added the anti-flail rule to `code-search-protocol.md` / `code-graph-protocol.md` / the `codebase-architect` system prompt, that inline copy was the one spot that didn't carry it. Functionally harmless (the spawned `codebase-architect` already governs via its own system prompt + the protocols), but a latent drift surface. Trimmed step 3a to a citation of both protocol modules ("follow their tier order, budgets, and anti-flail rule — don't restate them here"), keeping context-primer's own task shaping (canonical-router-first, agent memory, backlog, git, verification) intact. Verified no other skill restates the hierarchy (`lsp-bootstrap`'s line is a user-facing confirmation, not a protocol copy). **PATCH** (doc/guidance hygiene; no behaviour change — the search behaviour was already governed by the agent + protocols; no `baldart.config.yml` key).
25
+
26
+ ### Changed
27
+
28
+ - **`framework/.claude/skills/context-primer/SKILL.md`** — step 3a of the SEARCH STRATEGY block now cites `code-search-protocol.md` + `code-graph-protocol.md` for tier order / budgets / anti-flail instead of restating the LSP/graph-over-grep hierarchy inline.
29
+
8
30
  ## [4.52.3] - 2026-06-18
9
31
 
10
32
  **`codebase-architect`: structural tier (graph/LSP) is the PRIMARY source for symbol queries, with a hard anti-flail grep cap.** Diagnosed from a real `/prd` discovery run: a foreground `codebase-architect` verification burned its whole turn budget (56 tool uses, 88.1k tokens) grepping ~10× for one function (`createDraft`) inside an 8000-line re-export barrel, **terminated mid-investigation before producing its `totals:` report** (the truncation `/prd` then mis-recovered by resuming the completed agent in the background, breaking the foreground/blocking discovery contract). Root cause: the agent reached for the graph (`graphify explain`, which located the symbol instantly) only *after* the grep flail — because the protocols framed graph/LSP as "escalation" (i.e. *after* grep) and excluded the graph from single-symbol queries entirely, and nothing capped per-symbol grep. Fixed by three surgical guidance edits: (1) `codebase-architect` step 3 now routes symbol location/relation queries to the structural tier **first** when its flag is on (grep is the fallback), with an **anti-flail heuristic** — don't grind grep to locate one symbol; after ~2 misses (or on a barrel / very large file) switch tools: to `graphify explain`/`query` or LSP `go-to-definition` when a flag is on, or — **when neither flag is on (the default config)** — read the file's exports/headings directly instead of full-text-grepping it again; (2) `code-search-protocol.md` adds the same flag-aware per-symbol anti-flail rule to **Budget** + a fallback rule (LSP off & grep can't find a definition → graph is a valid locator); (3) `code-graph-protocol.md` narrows the "graph adds nothing for single-symbol" exclusion — the graph IS a valid fallback for *locating* a definition when grep/LSP miss on a barrel. The downstream `/prd` post-condition gap (how to react to an incomplete foreground grounding return) is intentionally **not** fixed here — this release attacks the root cause (budget burn), not the symptom. The "prefer LSP/graph over grep" guidance already existed and was ignored on the incident run; the reframe + graph-as-locator fix are net-positive regardless, but the behavioural claim is unverified — the real test is the next run. **PATCH** (behaviour-correcting guidance to existing agent + protocol modules; no new agent/skill/capability, no `baldart.config.yml` key).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.52.3
1
+ 4.53.0
@@ -83,14 +83,12 @@ SEARCH STRATEGY — canonical-router-first, verify-only-if-needed:
83
83
  c. Git: `git log --oneline -10 --grep="{keywords}"` (10 results max).
84
84
 
85
85
  3. Broader search (when steps 1-2 are thin):
86
- a. Grep the project's source root for 2-3 specific identifiers from steps 1-2.
87
- When `features.has_lsp_layer: true` AND the identifier is a symbol (function /
88
- type / class name, not a free-text phrase), prefer LSP `find-references` /
89
- `go-to-definition` over Grep it filters textual collisions before any file
90
- read. Cap at 3 LSP calls, then fall back. See `framework/agents/code-search-protocol.md`.
91
- When `features.has_code_graph: true` AND the question is structural/relational
92
- (how X connects to Y, impact, clusters), prefer the Graphify code graph
93
- (`graphify query`/`path`/`affected`) — see `framework/agents/code-graph-protocol.md`.
86
+ a. Search for 2-3 specific identifiers from steps 1-2, routing by query shape
87
+ per `framework/agents/code-search-protocol.md` (symbols LSP when
88
+ `has_lsp_layer`) and `framework/agents/code-graph-protocol.md` (structural /
89
+ relational Graphify when `has_code_graph`); grep is the fallback for textual
90
+ patterns. Follow those modules' tier order, budgets, and anti-flail rule
91
+ don't restate them here.
94
92
  b. Read key file headers (first 50 lines) to identify structure.
95
93
  DO NOT read `${paths.prd_dir}/` or `${paths.references_dir}/` files in full —
96
94
  route via the registry and read only the specific section you need.
@@ -80,7 +80,7 @@ Branch: [feat/FEAT-XXXX-slug]
80
80
  Slug: [the slug used to derive Branch/Path — see Pre-flight step 4]
81
81
  Path: [.worktrees/feat-FEAT-XXXX-slug]
82
82
  Port: [from registry]
83
- Created: [ISO-8601 timestamp the worktree was created — Pre-flight step 6c backfills from the worktree-setup subagent's created_at; Phase 8 cycle_time_mins start anchor]
83
+ Created: [ISO-8601 timestamp the worktree was created — Pre-flight step 6c backfills from the worktree-setup script's manifest created_at; Phase 8 cycle_time_mins start anchor]
84
84
  Group parent: [FEAT-XXXX or "standalone"]
85
85
  Main repo: [/absolute/path/to/main/repo]
86
86
  Trunk branch: [resolved git.trunk_branch — Phase 0 step 0 populates]
@@ -184,7 +184,7 @@
184
184
 
185
185
  (No user-visible Task spine / Progress Bar is created — the internal tracker is the only state surface; see SKILL.md § "State surface — the tracker only".)
186
186
 
187
- 3d. **Codex batch cross-card grounding check** (background — launched together with the worktree-setup subagent in step 4, then a single barrier in step 5)
187
+ 3d. **Codex batch cross-card grounding check** (background — launched together with the worktree-setup script in step 4, then a single barrier in step 5)
188
188
 
189
189
  > **Why**: a non-Anthropic frontier model (Codex, via `codex-companion.mjs`) reviews the full card batch for cross-card conflicts that per-card plan-auditor checks cannot detect (each plan-auditor sees only one card).
190
190
 
@@ -281,37 +281,29 @@
281
281
  - **`WT_PATH` set AND `registry.json` has a complete code entry for this slug** (a finished prior run — `buildVerified` recorded) → **resume**: read `path`/`branch`/`port`/`createdAt`/`buildVerified` from that entry, skip to step 6; re-run the baseline as a single background `Bash` (output to `/tmp`) **only if** `buildVerified` is not `true`.
282
282
  - **`WT_PATH` set but NO complete registry entry** (a prior attempt interrupted mid-setup — the normal compaction-mid-barrier state: worktree created, build unfinished, entry never written) → it is a half-built orphan with **no card work** (we are still in pre-flight, zero commits). **Reset clean and recreate**: `git -C "$MAIN" worktree remove --force "$WT_PATH"` then `git -C "$MAIN" branch -D "$WT_BRANCH"` (both ignore-if-absent), then proceed to 4b. A pre-flight worktree has nothing to lose, so a clean recreate is always safe — and it sidesteps the fail-loud collision a naive re-spawn would hit.
283
283
  This — detection by `git worktree list`, not the lagging registry — is what makes the deferred-flush pre-flight genuinely idempotent across compaction.
284
- b. **Spawn ONE background subagent** (Agent tool, **`mode: "bypassPermissions"`** mandatory per the SKILL.md meta-rules; a background agent that hit a permission prompt with no human present would stall the barrier forever `run_in_background: true`, `name: "worktree-setup-<FIRST-CARD-ID>"`, a subagent type that can use the Skill tool — `general-purpose`, **`model: "sonnet"`** — the mission runs the multi-step `/nw` skill (group cards, create the worktree, install deps, allocate the dev-server port, write the registry entry, run the baseline) **via the Skill tool**: a sustained tool-execution chain, NOT one-shot plumbing. A too-weak model pattern-matches the expected return block instead of doing the workthe observed 2/2 fabrication was a well-formed block reporting `baseline: pass` in ~6s with **no worktree on disk**. Sonnet executes it for real or fails honestly; either way the returned block is **never trusted as evidence** — the orchestrator's disk-verification gate (step 6a below) is the source of truth) whose ENTIRE mission is to run `/nw` and return the block below. Briefing:
285
- ```
286
- Invoke the worktree-manager skill in `/nw` programmatic mode with:
287
- { cards: [<all card IDs>], groupParent: <PARENT-ID|null>, slug: "<slug>" }
288
- Let it: group cards, derive the branch from git_strategy.branch (fallback
289
- feat/<PARENT-ID>-<slug>), create the worktree in .worktrees/, install deps,
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
293
- IDs in the `cards` field), and run the baseline (tsc + lint + build)
294
- UNDER A HARD TIMEOUT — wrap the build step in `timeout 600 <build-cmd>`
295
- (10 min) so a hung or interactive build cannot stall the pre-flight barrier.
296
- Redirect ALL install/build/git output to files under /tmp — never inline it.
297
- Return ONLY this block, nothing else (no logs, no narration, no skill recap):
298
- worktree_path: <absolute path>
299
- branch: <branch>
300
- port: <n>
301
- created_at: <ISO-8601, stamped WHEN you create the worktree — before install/build>
302
- baseline: pass | fail | timeout
303
- baseline_log: <path on failure/timeout, else "-">
304
- If the build fails, return `baseline: fail` with the log path and STOP.
305
- If the timeout kills the build, return `baseline: timeout` with the partial
306
- log path and STOP — do not continue.
307
- ```
308
- c. The subagent's context (the worktree-manager skill body, install/build logs, git output) **lives and dies inside it** — the orchestrator receives only the structured block. Combined with the Codex check (3d, also background), this replaces the old ~30-turn foreground pre-flight tail with background ops and a single resume.
309
- d. **Fallback executor — when the subagent does not produce a VERIFIED worktree.** The subagent's returned block is **never trusted as evidence**; step 6a verifies the worktree on disk. Trigger this fallback when EITHER (i) the subagent returns empty / without the structured block (cannot invoke the Skill tool in this consumer, or the known "0 tool uses · Done" empty-result), OR (ii) it returns a well-formed block but the **step-6a disk gate is VERIFIED:false** (the observed 2/2 fabrication: `baseline: pass` in ~6s with no worktree on disk). In both cases do **NOT** strand the barrier: the orchestrator **falls back to invoking `/nw` inline itself** — a genuinely **DIFFERENT executor** (the full-model orchestrator interpreting the skill prose), **not** a re-spawn of the same subagent and **not** a frozen script, so neither a Skill-from-subagent capability gap nor a weak-model fabrication can recur on the fallback. Slug from 4a; record `worktree_path`/`branch`/`port` and stamp `created_at` from the inline return; then **re-run the step-6a disk gate ONCE**. **Cap (no loop):** the chain is strictly `subagent → inline /nw → HALT+report` — never re-spawn the subagent, never loop the inline path. If the inline attempt ALSO fails the gate, or the failure is **deterministic** (port exhaustion across 3001-3099, a corrupted lockfile, or a genuine `baseline: fail`/`timeout` build error — recreating cannot fix these), **HALT and report** rather than retry. You lose the prefix saving for this one run, but pre-flight completes or halts cleanly — never silently on a phantom worktree. This is the same opt-in-with-fallback discipline the Codex check (3d) and the dynamic-workflow gate use.
310
- 5. **End the turn — barrier on ALL launched background ops (wait for every one, not the first).** Having launched the Codex cross-card check (3d) and the worktree-setup subagent (4), the orchestrator has nothing to do until they return. **End the turn** — do NOT poll with `sleep`/`echo "waiting"` loops (§ "Context economy"; same rule as team-mode Step C). Background agents and background `Bash` re-invoke the orchestrator automatically on completion. **Wait for EVERY launched op before step 6**: each completion wakes you separately, so on each wake check whether *all* launched ops have returned — if one is still in flight, **end the turn again** and wait. Do NOT proceed to step 6 on the first completion, or you would read a half-written `$AUDIT_FILE` (and 3d's "If PASS or file empty: proceed normally" would silently swallow real conflicts) or a missing worktree block. (If 3d was SKIPPED by the provenance gate, the only op is the worktree subagent — or none, if step 4a2 resumed an existing worktree.) **Recovery**: a compaction mid-barrier re-enters pre-flight from step 4; the 4a2 git pre-check makes that safe (the worktree is detected via `git worktree list` and resumed-or-reset, never blindly re-created into a fail-loud collision).
284
+ b. **Launch the deterministic setup SCRIPT as a background `Bash` (NO model in the loop this is pure plumbing).** The SSOT for the entire code-worktree build sequence (worktree add install env copy port registry baseline) is **`scripts/setup-worktree.sh`** in the `worktree-manager` skill — the SAME script `/nw` and `new2` invoke (no per-caller duplication). A deterministic script **cannot fabricate or stall**: it does the work or fails honestly with a log. This replaces the old Sonnet-subagent-via-Skill delegation, whose two observed failure modes were exactly a model-in-the-loop pathologyhaiku FABRICATED a well-formed `baseline: pass` with nothing on disk (2/2), and Sonnet went IDLE mid-execution leaving a half-built worktree (`node_modules` missing). The orchestrator resolves only the cheap in-context args and passes them; the script resolves `stack.env_files` / toolchain commands / the port range from `baldart.config.yml` itself.
285
+ - **Script-presence check (opt-in-with-fallback).** Resolve it: `SETUP_SH="$(ls .framework/framework/.claude/skills/worktree-manager/scripts/setup-worktree.sh .claude/skills/worktree-manager/scripts/setup-worktree.sh 2>/dev/null | head -1)"`. If **absent** (a pre-this-release install whose subtree predates the script), skip straight to the **4d inline `/nw` fallback** — never block on a missing script.
286
+ - **Launch** with `run_in_background: true` and `timeout: 640000` (a hair above the script's own 600 s build timeout, so the harness never kills it before the script's timeout fires):
287
+ ```bash
288
+ WT_MANIFEST="/tmp/wt-setup-<FIRST-CARD-ID>-<SESSION-ID>.txt"
289
+ bash "$SETUP_SH" \
290
+ --main "$MAIN" --trunk "$TRUNK" \
291
+ --branch "$WT_BRANCH" --slug "<slug from 4a>" \
292
+ --card "<PARENT-ID, or the lead card ID>" \
293
+ --cards "<comma-separated ALL card IDs in the batch>" \
294
+ --group-parent "<PARENT-ID, or empty>" \
295
+ --env-files "<stack.env_files as CSV, default '.env.local,.env'>" \
296
+ --manifest "$WT_MANIFEST"
297
+ ```
298
+ `$MAIN`/`$TRUNK` are the Phase-0 variables; `$WT_BRANCH` is the branch resolved in 4a2. **Pass `--env-files` explicitly** (resolve `stack.env_files` from `baldart.config.yml` — you have already read the config in Phase 0): the orchestrator's YAML read is authoritative, so the script's own best-effort `env_files` parser stays a `/nw`-standalone fallback and never sits on this load path. Record the exact `$WT_MANIFEST` path in the tracker `## Worktree` section so the resume step (6a) reads the SAME file. The script writes its structured block to that file on **every** exit path.
299
+ c. The install/build output lives in the script's own log (`/tmp/wt-setup-<slug>.log`), never in the orchestrator context — the same context-economy win the subagent gave, but with **no model to fabricate or stall**. Combined with the Codex check (3d, also background), this keeps the pre-flight tail to background ops + a single resume.
300
+ d. **Fallback executor — inline `/nw` — ONLY when the script could not run or the disk gate fails.** Trigger when EITHER (i) the script is **absent** (4b presence check), OR (ii) the **step-6a disk gate is VERIFIED:false** (defence-in-depth — a deterministic script can still fail mute on a full disk / partial install). In those cases the orchestrator **falls back to invoking `/nw` inline itself** — the full-model orchestrator interpreting the skill prose, a genuinely different executor — slug from 4a, then re-runs the step-6a disk gate ONCE. **Honest deterministic failures are NOT fallback-eligible**: a manifest `status: ok` is impossible with a bad build, and a manifest reporting `baseline: fail` / `baseline: timeout` (the build genuinely broke or hung) or `status: error` with a deterministic cause (collision the 4a2 reset already handled, port exhaustion across the configured range) → **HALT and report** (point the user at `baseline_log`); recreating cannot fix them, and a real baseline break is repaired by the coder downstream, not by re-running setup. **Cap (no loop):** the chain is strictly `script → inline /nw → HALT+report` — never re-launch the script, never loop the inline path.
301
+ 5. **End the turn — barrier on ALL launched background ops (wait for every one, not the first).** Having launched the Codex cross-card check (3d) and the worktree-setup script (4), the orchestrator has nothing to do until they return. **End the turn** — do NOT poll with `sleep`/`echo "waiting"` loops (§ "Context economy"; same rule as team-mode Step C). Background `Bash` re-invokes the orchestrator automatically on completion. **Wait for EVERY launched op before step 6**: each completion wakes you separately, so on each wake check whether *all* launched ops have returned — if one is still in flight, **end the turn again** and wait. Do NOT proceed to step 6 on the first completion, or you would read a half-written `$AUDIT_FILE` (and 3d's "If PASS or file empty: proceed normally" would silently swallow real conflicts) or a half-written `$WT_MANIFEST`. (If 3d was SKIPPED by the provenance gate, the only op is the worktree script or none, if step 4a2 resumed an existing worktree.) **Recovery**: a compaction mid-barrier re-enters pre-flight from step 4; the 4a2 git pre-check makes that safe (the worktree is detected via `git worktree list` and resumed-or-reset, never blindly re-created into a fail-loud collision).
311
302
  6. **On resume — flush the pre-flight tracker sections in one pass (no incremental per-sub-step churn).** When all launched ops have returned:
312
- a. **Worktree integrity gate (BLOCKING — the disk is the source of truth, not the returned block).**
313
- - **Honest-failure signals first** (trust these a reported failure is real, not a fabrication, and recreating the worktree would not fix it): if the block reports `baseline: fail` STOP and report (point the user at `baseline_log`); if `baseline: timeout` (the build exceeded the launch timeout, §4b/§5) STOP and report the timeout. Do NOT continue to Phase 1; do NOT recreate.
314
- - **On `baseline: pass`, VERIFY the worktree on disk YOURSELF** these are orchestrator `Bash` calls run in this (full-model) context; **never** trust a `verified`-style field returned by a subagent (it is exactly as fabricable as the block itself). With `$WT_PATH`/`$WT_BRANCH` from the block and `$MAIN` from the tracker (`## Worktree` `Main repo:`):
303
+ a. **Worktree integrity gate (BLOCKING — the disk is the source of truth, not the manifest).** Read the manifest at the `$WT_MANIFEST` path recorded in the tracker (`## Worktree`) — a small fixed-key block (`status` / `error` / `worktree_path` / `branch` / `port` / `created_at` / `baseline` / `baseline_log`); never `cat` a log.
304
+ - **A TERMINAL manifest is a NECESSARY pre-condition for VERIFIED.** The script writes a *provisional* `status: error` manifest the instant the worktree exists, then overwrites it with `status: ok` + `baseline: pass` only after the build passes. So if the manifest is **missing, empty, or NOT terminal** (`status` is not `ok`, or `baseline` is not `pass`) → the worktree is half-built (the process was killed mid-build likely on a host without a `timeout` binary, where only the caller's background timeout bounds it). Do **NOT** run the disk checks 1-4 and do **NOT** declare VERIFIED on their strength: `node_modules` existing on a killed install is exactly the false-pass this guard prevents. Route a non-terminal/missing manifest to the **4d inline `/nw` fallback** (or HALT if 4d is exhausted) — never VERIFIED.
305
+ - **Honest-failure signals** (trust these — a reported failure is real, not a fabrication, and recreating the worktree would not fix it): if the manifest is `status: error` with `baseline: fail` STOP and report (point the user at `baseline_log`); `baseline: timeout` (the build exceeded the 600 s script timeout, §4b) → STOP and report the timeout; `status: error` with a deterministic setup cause (collision, port exhaustion see the `error:` line) → STOP and report. Do NOT continue to Phase 1; do NOT recreate. (A non-terminal `status: error` whose `error:` line is the provisional "setup incomplete" message — i.e. NOT one of these honest/deterministic causes — → 4d inline `/nw` fallback per the bullet above.)
306
+ - **On `status: ok` + `baseline: pass`, VERIFY the worktree on disk YOURSELF** — these are orchestrator `Bash` calls run in this (full-model) context; the manifest is corroboration, the disk is proof. With `$WT_PATH` = the manifest's `worktree_path`, `$WT_BRANCH` from 4a2, and `$MAIN` from the tracker (`## Worktree` `Main repo:`):
315
307
  1. `git -C "$MAIN" worktree list --porcelain` lists `$WT_PATH`;
316
308
  2. `test -d "$WT_PATH"`;
317
309
  3. `git -C "$WT_PATH" rev-parse --abbrev-ref HEAD` equals `$WT_BRANCH`;
@@ -321,7 +313,7 @@
321
313
  b. **Codex verdict**: handle it via the verdict-extraction discipline in 3d (read `$AUDIT_FILE` through the `[codex]`-stripping filter; keep distilled findings only).
322
314
  c. **One-pass tracker flush (no round-trips).** Assemble the pre-flight sections **in-context** (they are all small) and fill them with **back-to-back `Edit`s and no intervening reads**. The win is killing the old read-modify-read-modify churn (~5 incremental edits), **not** the literal tool-call count. **Do NOT `Write`-overwrite the whole file from in-context memory**: Phase 0 already wrote `Main repo:` / `Trunk branch:` / `Metrics dir:` into `## Worktree` and the `Status`/divergence lines into `## Phase 0`, and after a barrier compaction you may no longer hold those in-context (`$MAIN` "does not survive context compaction" — § Phase 0 step 1) — an overwrite-from-memory would silently drop them and HALT later with "`$MAIN` absent from tracker". Surgical `Edit`s on the placeholder sections leave Phase 0's content intact. Sections filled here:
323
315
  - `## File Ownership Map` (3b).
324
- - `## Worktree` — path / branch / slug / port, plus `Created:` = **the subagent block's `created_at`** (worktree-creation time, NOT resume time, so Phase 8's `cycle_time_mins` still spans the build window). On the **4a2 resume** path, `Created:` = the registry entry's `createdAt`; on the **4d inline fallback**, stamp `date -u +%Y-%m-%dT%H:%M:%SZ` at creation. (Never leave `Created:` empty — `cycle_time_mins` anchors on it.)
316
+ - `## Worktree` — path / branch / slug / port, plus `Created:` = **the manifest's `created_at`** (worktree-creation time, NOT resume time, so Phase 8's `cycle_time_mins` still spans the build window). On the **4a2 resume** path, `Created:` = the registry entry's `createdAt`; on the **4d inline fallback**, stamp `date -u +%Y-%m-%dT%H:%M:%SZ` at creation. (Never leave `Created:` empty — `cycle_time_mins` anchors on it.)
325
317
  - `## Cross-Card Conflicts (Codex)` — distilled findings (the 3d skip-decision already wrote the `SKIPPED`/`RUN — reason` line; append the distilled findings on the RUN path, nothing to add on SKIP).
326
318
  - In team mode: `## Team Mode` + `## Parallel Groups` (per team-mode.md).
327
319
  `## Execution Mode` was already written at step 3c — do NOT rewrite it here. **Rationale**: pre-flight is idempotent and cheap to redo (step 4a2's git pre-check guards worktree re-creation), so the data sections do not need mid-flight persistence; per-phase incremental writes resume for card execution, where mid-phase recovery actually matters. The file already exists (the skeleton was created at batch start per § Context Tracking; Phase 0 wrote `## Phase 0`) — backfill, do NOT re-create.
@@ -574,202 +574,65 @@ Check registry for duplicate:
574
574
 
575
575
  Branch name: `feat/<CARD-ID>-<slug>` (with card) or `feat/<slug>` (slug-only). Use `hotfix/` prefix if card specifies hotfix.
576
576
 
577
- ### 3. Create the worktree
577
+ ### 3-6. Create + isolate + baseline + registry — `scripts/setup-worktree.sh` (SSOT)
578
578
 
579
- Base the new worktree on `origin/<$TRUNK>` (the remote tip we just fetched in step 1) rather than the local trunk branch that way we never need the local trunk to be checked out or up to date on the main repo.
579
+ Steps 1-2 resolved everything model-judgement is needed for: `$MAIN`, `$TRUNK`, the
580
+ branch, the slug, the card list. The **rest is pure plumbing** — create the worktree
581
+ off `origin/$TRUNK`, sync untracked cards, `npm install`, copy `stack.env_files`,
582
+ allocate a free port, write the registry entry, run the baseline. That sequence is
583
+ **not** re-authored here: it lives **once** in
584
+ `scripts/setup-worktree.sh`, the single source of truth invoked identically by
585
+ `/nw`, `/new` (setup.md step 4), and `new2`. Re-inlining it as a parallel bash copy
586
+ is exactly the duplication CLAUDE.md forbids — and a deterministic script cannot
587
+ fabricate a `baseline: pass` or stall mid-build the way a model-in-the-loop did.
580
588
 
581
- The worktree path is **deterministic** (R8): derived from the card ID + kebab-case slug only, never a timestamp — so a resumed run resolves to the same path and the duplicate check in step 1 is meaningful.
589
+ Invoke it from the **main checkout** (cwd = `$MAIN`):
582
590
 
583
591
  ```bash
584
- # With card ID:
585
- WORKTREE_PATH=".worktrees/feat-<CARD-ID>-<slug>"
586
- git worktree add "$WORKTREE_PATH" -b "feat/<CARD-ID>-<slug>" "origin/$TRUNK"
587
-
588
- # Slug-only (no card):
589
- WORKTREE_PATH=".worktrees/feat-<slug>"
590
- git worktree add "$WORKTREE_PATH" -b "feat/<slug>" "origin/$TRUNK"
591
- ```
592
-
593
- ### 3b. Sync untracked backlog cards from main repo
594
-
595
- Backlog cards created during `/prd` sessions may exist as untracked files in the main repo
596
- but are NOT on the trunk branch yet. The worktree (branched from the trunk) won't have them.
597
-
598
- ```bash
599
- # BACKLOG_DIR = value of paths.backlog_dir from baldart.config.yml (fallback `backlog`
600
- # when the key is absent — emit it resolved, never the literal ${paths.backlog_dir}
601
- # token, which is not valid bash). Same resolution as /new's card-scoped diff block.
602
- BACKLOG_DIR="<value of paths.backlog_dir, or 'backlog' if the key is absent>"
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)"
611
-
612
- # For each card in the batch, check if its YAML exists in the main repo but not in the worktree
613
- for CARD_FILE in $(ls "$MAIN_ROOT/$BACKLOG_DIR"/*.yml 2>/dev/null); do
614
- BASENAME=$(basename "$CARD_FILE")
615
- if [ ! -f "$WORKTREE_PATH/$BACKLOG_DIR/$BASENAME" ]; then
616
- cp "$CARD_FILE" "$WORKTREE_PATH/$BACKLOG_DIR/$BASENAME"
617
- echo "Synced untracked card: $BASENAME"
618
- fi
619
- done
592
+ SETUP_SH="$(ls .claude/skills/worktree-manager/scripts/setup-worktree.sh \
593
+ .framework/framework/.claude/skills/worktree-manager/scripts/setup-worktree.sh 2>/dev/null | head -1)"
594
+ WT_MANIFEST="/tmp/nw-setup-<slug>.txt"
595
+ bash "$SETUP_SH" \
596
+ --main "$MAIN" --trunk "$TRUNK" \
597
+ --branch "<branch from step 2>" --slug "<slug>" \
598
+ --card "<CARD-ID or empty>" --cards "<comma-separated card IDs, or empty>" \
599
+ --group-parent "<groupParent or empty>" \
600
+ --env-files "<stack.env_files as CSV, default '.env.local,.env'>" \
601
+ --manifest "$WT_MANIFEST"
620
602
  ```
621
603
 
622
- This prevents "File not found" errors when Phase 4 tries to mark cards as DONE.
623
-
624
- ### 4. Full isolation setup
625
-
626
- Run ALL of these inside the worktree directory:
627
-
628
- ```bash
629
- cd "$WORKTREE_PATH"
630
-
631
- # 1. Own node_modules (full independent install)
632
- npm install
633
-
634
- # 2. Own .next cachealready isolated since each worktree has its own
635
- # directory tree. The default .next inside the worktree is sufficient.
636
-
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
693
-
694
- # 4. Pick a free dev server port (avoid 3000 used by main)
695
- PORT=3001
696
- while lsof -i :$PORT -sTCP:LISTEN >/dev/null 2>&1; do
697
- PORT=$((PORT + 1))
698
- if [ $PORT -gt 3099 ]; then echo "ERROR: No free port in 3001-3099" >&2; exit 1; fi
699
- done
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"
707
- else
708
- echo "PORT=$PORT" >> "$PORT_ENV_FILE"
709
- fi
710
-
711
- # 5. ASSERT git hooks are ACTIVE — not just readable.
712
- # The worktree shares core.hooksPath via .git/commondir, so the same
713
- # resolution applies here as in the main checkout. Reading the value
714
- # (the old `git config core.hooksPath || true`) is NOT enough: it never
715
- # catches the failure mode where core.hooksPath points at an empty dir
716
- # while the repo's versioned hooks live elsewhere (pre-commit / pre-push
717
- # silently inactive). Detect the versioned-hooks dir and assert it is served.
718
- HOOK_SRC=""
719
- for d in .husky .githooks scripts/git-hooks githooks; do
720
- if [ -d "$d" ] && ls "$d" 2>/dev/null | grep -qE '^(pre-commit|pre-push|commit-msg)$'; then
721
- HOOK_SRC="$d"; break
722
- fi
723
- done
724
- if [ -n "$HOOK_SRC" ]; then
725
- ACTIVE="$(git config --get core.hooksPath || echo .git/hooks)"
726
- # Lean literal-string match (HOOK_SRC is relative). Full path resolution +
727
- # canonicalization lives in `baldart doctor` (src/utils/githooks.js); an
728
- # absolute core.hooksPath here would warn spuriously — rare, run doctor to confirm.
729
- case "$ACTIVE" in
730
- "$HOOK_SRC"|"$HOOK_SRC"/*|.husky/_) : ;; # active dir serves the versioned hooks (incl. husky v9 .husky/_)
731
- *)
732
- echo "⚠️ WARNING: git hooks INACTIVE — the versioned hooks live in '$HOOK_SRC' but core.hooksPath = '$ACTIVE'." >&2
733
- echo " Re-activate them with: git config core.hooksPath $HOOK_SRC" >&2
734
- ;;
735
- esac
736
- fi
737
- ```
738
-
739
- ### 5. Verify baseline
740
-
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
-
752
- # TypeScript + lint (fast)
753
- eval "${TC_TC:-npx tsc --noEmit}"
754
- eval "${TC_LINT:-npx eslint --max-warnings=0 src/}"
755
-
756
- # Full build verification (required — confirms worktree is functional)
757
- eval "${TC_BUILD:-npm run build}"
758
- ```
759
-
760
- If build fails → STOP and report. Do NOT continue — the worktree is broken.
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".)
763
-
764
- ### 6. Update registry
765
-
766
- Add entry to `.worktrees/registry.json` with all fields: card, cards, groupParent, slug, branch, `trunkBranch` (the resolved `$TRUNK`), `mainRoot` (the resolved `$MAIN`), absolute path, port, `createdAt`, `envSyncedAt`, and `buildVerified`. Persisting `trunkBranch` + `mainRoot` (R6) lets `/mw`/`/cw` read them back with a presence guard instead of re-deriving or assuming `develop`.
604
+ Pass `--env-files` explicitly (resolve `stack.env_files` from `baldart.config.yml`)
605
+ the script's built-in `env_files` parser is only a best-effort last-resort fallback for
606
+ a bare invocation; the caller's read is authoritative.
607
+
608
+ What the script does (and what the omitted flags fall back to), so you can read its
609
+ output without re-deriving it:
610
+ - **Create**: `git worktree add .worktrees/<branch-with-slashes→dashes> -b <branch> origin/$TRUNK` (deterministic path, R8). Fails loud on a branch/path collision — a code worktree is never silently re-created (the caller's idempotency pre-check resets an orphan first).
611
+ - **Sync untracked cards** from `$MAIN/<paths.backlog_dir>` (so `/new` Phase 4 can mark cards DONE).
612
+ - **Install** deps non-interactively (stdin closed, `CI=1`).
613
+ - **Copy `stack.env_files`** (FILES via plain `cp`, DIRS via `cp -R`; missing FILE → WARN, never abort) and write `PORT=<n>` into the primary copied env file. The SAME `stack.env_files` list drives `/nw`, `/new`, `new2` — pass `--env-files` to override, else the script resolves it from config (default `.env.local,.env`).
614
+ - **Port**: first free in `3001-3099` (skips registry-held ports + live listeners), allocated under a `.worktrees/.wt-setup.lock` mutex so concurrent `/nw` runs never collide.
615
+ - **Baseline** (toolchain-aware — `toolchain.commands.{typecheck,lint,build}` when `features.has_toolchain: true`, else `npx tsc --noEmit` / `npx eslint --max-warnings=0 src/` / `npm run build`): tsc/lint are **report-but-continue**, the build is **STOP-on-fail** and runs under a hard `timeout 600` (so a hung/interactive build cannot stall a caller's barrier).
616
+ - **Registry**: writes/updates the `.worktrees/registry.json` entry (all fields incl. `trunkBranch`/`mainRoot`/`port`/`createdAt`/`buildVerified`) atomically via `node` `buildVerified` flips to `true` only after the build passes.
617
+
618
+ The script writes a structured manifest to `$WT_MANIFEST` on **every** exit path
619
+ (`status` / `error` / `worktree_path` / `branch` / `port` / `created_at` /
620
+ `baseline` / `baseline_log`) read THAT for the result; the build log lives in
621
+ `/tmp/wt-setup-<slug>.log` and never needs to enter context. `status: error` with
622
+ `baseline: fail`/`timeout` is an **honest** build failure (the coder repairs it
623
+ downstream the script never self-repairs, role boundary); `status: error` with a
624
+ deterministic setup cause (collision, port exhaustion) report, do not retry.
767
625
 
768
626
  ### 7. Auto-start dev server (if `--dev` flag or `/nw --dev`)
769
627
 
628
+ The script set up the worktree in its own process; resolve `$WORKTREE_PATH` and `$PORT`
629
+ from the manifest written in step 3-6 (`worktree_path` / `port`). Only on `status: ok`.
630
+
770
631
  If the user passed `--dev` (e.g., `/nw --dev FEAT-0500 menu-fix`), start the dev server in background:
771
632
 
772
633
  ```bash
634
+ WORKTREE_PATH="$(grep '^worktree_path:' "$WT_MANIFEST" | sed 's/^worktree_path:[[:space:]]*//')"
635
+ PORT="$(grep '^port:' "$WT_MANIFEST" | sed 's/^port:[[:space:]]*//')"
773
636
  cd "$WORKTREE_PATH"
774
637
  nohup npm run dev > .next/dev.log 2>&1 &
775
638
  DEV_PID=$!
@@ -784,14 +647,18 @@ fi
784
647
 
785
648
  ### 8. Report to user
786
649
 
650
+ Read the values from the `$WT_MANIFEST` block (`worktree_path` / `branch` / `port` /
651
+ `baseline`). On `status: error`, report the `error:` line and the `baseline_log` instead
652
+ of the success block.
653
+
787
654
  ```
788
655
  Worktree ready:
789
- Path: <full-absolute-path>
656
+ Path: <worktree_path>
790
657
  Branch: feat/<CARD-ID>-<slug> (or feat/<slug> if no card)
791
- Port: <PORT>
658
+ Port: <port>
792
659
  Card: <CARD-ID> (or "none" if slug-only)
793
- Build: verified ✓
794
- Dev: http://localhost:<PORT> (PID: <DEV_PID>) ← only if --dev
660
+ Build: verified ✓ (baseline: pass)
661
+ Dev: http://localhost:<port> (PID: <DEV_PID>) ← only if --dev
795
662
 
796
663
  Start dev server: cd <path> && npm run dev ← only if NOT --dev
797
664
  The worktree has its own node_modules and .next cache — fully independent.
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # setup-worktree.sh — SSOT deterministic CODE-worktree creation + baseline.
4
+ #
5
+ # This is the single source of truth for the worktree build sequence consumed by
6
+ # three callers (no per-caller duplication — the sin CLAUDE.md forbids):
7
+ # • /nw (worktree-manager § "/nw") — interactive + slug-only + programmatic
8
+ # • /new (skill: framework/.claude/skills/new/references/setup.md step 4)
9
+ # • new2 (framework/.claude/workflows/new2.js Pre-flight)
10
+ #
11
+ # Why a deterministic script instead of a model-driven subagent: the worktree
12
+ # setup is PURE PLUMBING (git worktree add → npm install → env copy → port →
13
+ # registry → baseline). Driving it through a Skill-interpreting subagent put a
14
+ # model in the loop for mechanical work — and the model either FABRICATED a
15
+ # well-formed "baseline: pass" block with nothing on disk (haiku, 2/2), or went
16
+ # IDLE mid-execution leaving a half-built worktree (sonnet, node_modules missing).
17
+ # A deterministic script cannot fabricate or stall: it does the work or fails
18
+ # honestly with a log. Callers launch it as a BACKGROUND Bash, end the turn, and
19
+ # on resume read the structured manifest + run their own disk-verification gate.
20
+ #
21
+ # SCOPE: the FRESH-path only. The caller's git-authoritative idempotency pre-check
22
+ # (setup.md step 4a2) decides resume / reset / fresh BEFORE invoking this — this
23
+ # script assumes a clean slate and fails loud on a branch/path collision (a code
24
+ # worktree is not silently re-creatable). It does NOT repair a failing baseline
25
+ # (role boundary — the coder specialist does that downstream); it reports the
26
+ # failure with a log precise enough to act on.
27
+ #
28
+ # Usage:
29
+ # setup-worktree.sh --branch <branch> --slug <slug> --manifest <path> [opts]
30
+ #
31
+ # Required:
32
+ # --branch <branch> resolved branch name (caller derives it; e.g.
33
+ # feat/FEAT-0200-menu-ranking or feat/<slug>)
34
+ # --slug <slug> kebab-case slug (used for the log filename only;
35
+ # the worktree path derives from --branch)
36
+ # --manifest <path> file to write the structured result block to (the
37
+ # caller reads THIS at resume — never the bg stdout)
38
+ #
39
+ # Optional (each falls back to baldart.config.yml / autodetect when omitted, so
40
+ # /nw can call with just the three required flags; /new + new2 pass the values
41
+ # they already resolved in Phase 0 to avoid re-deriving / drift):
42
+ # --card <id> primary card id (default: none → null)
43
+ # --cards <a,b,c> all card ids sharing the wt (default: [])
44
+ # --group-parent <id> group.parent (default: none → null)
45
+ # --main <path> main repo root (default: resolve_main)
46
+ # --trunk <branch> git.trunk_branch (default: config/autodetect)
47
+ # --env-files <a,b> stack.env_files CSV (default: config / .env.local,.env)
48
+ # --backlog-dir <dir> paths.backlog_dir (default: config / backlog)
49
+ # --tc-typecheck <cmd> toolchain typecheck cmd (default: config / npx tsc --noEmit)
50
+ # --tc-lint <cmd> toolchain lint cmd (default: config / npx eslint --max-warnings=0 src/)
51
+ # --tc-build <cmd> toolchain build cmd (default: config / npm run build)
52
+ # --install-cmd <cmd> dependency install cmd (default: npm install)
53
+ # --log <path> install/build log sink (default: /tmp/wt-setup-<slug>.log)
54
+ # --port-min <n> port scan floor (default: 3001)
55
+ # --port-max <n> port scan ceiling (default: 3099)
56
+ # --build-timeout <s> hard build timeout seconds (default: 600)
57
+ #
58
+ # Manifest written (parsed by callers — STABLE contract):
59
+ # status: ok | error
60
+ # error: <message | ->
61
+ # worktree_path: <abs | ->
62
+ # branch: <branch>
63
+ # port: <n | ->
64
+ # created_at: <ISO-8601 | -> (stamped at worktree creation, NOT at finish)
65
+ # baseline: pass | fail | timeout | -
66
+ # baseline_log: <path | ->
67
+ #
68
+ # Exit: 0 on a VERIFIED worktree with baseline:pass; non-zero otherwise. The
69
+ # caller never trusts the exit code or the manifest as the sole evidence — it
70
+ # re-verifies the worktree on disk (setup.md step 6a). This script writes the
71
+ # manifest on EVERY exit path so a non-zero exit is always diagnosable.
72
+
73
+ set -u
74
+
75
+ err() { printf '%s\n' "$*" >&2; }
76
+
77
+ # --- args ------------------------------------------------------------------
78
+ BRANCH= SLUG= MANIFEST= CARD= CARDS= GROUP_PARENT= MAIN= TRUNK=
79
+ ENV_FILES_ARG= BACKLOG_DIR= TC_TC= TC_LINT= TC_BUILD= INSTALL_CMD=
80
+ LOG= PORT_MIN=3001 PORT_MAX=3099 BUILD_TIMEOUT=600
81
+ ENV_FILES_SET=0
82
+ while [ $# -gt 0 ]; do
83
+ case "$1" in
84
+ --branch) BRANCH="${2:-}"; shift 2 ;;
85
+ --slug) SLUG="${2:-}"; shift 2 ;;
86
+ --manifest) MANIFEST="${2:-}"; shift 2 ;;
87
+ --card) CARD="${2:-}"; shift 2 ;;
88
+ --cards) CARDS="${2:-}"; shift 2 ;;
89
+ --group-parent) GROUP_PARENT="${2:-}"; shift 2 ;;
90
+ --main) MAIN="${2:-}"; shift 2 ;;
91
+ --trunk) TRUNK="${2:-}"; shift 2 ;;
92
+ --env-files) ENV_FILES_ARG="${2:-}"; ENV_FILES_SET=1; shift 2 ;;
93
+ --backlog-dir) BACKLOG_DIR="${2:-}"; shift 2 ;;
94
+ --tc-typecheck) TC_TC="${2:-}"; shift 2 ;;
95
+ --tc-lint) TC_LINT="${2:-}"; shift 2 ;;
96
+ --tc-build) TC_BUILD="${2:-}"; shift 2 ;;
97
+ --install-cmd) INSTALL_CMD="${2:-}"; shift 2 ;;
98
+ --log) LOG="${2:-}"; shift 2 ;;
99
+ --port-min) PORT_MIN="${2:-}"; shift 2 ;;
100
+ --port-max) PORT_MAX="${2:-}"; shift 2 ;;
101
+ --build-timeout) BUILD_TIMEOUT="${2:-}"; shift 2 ;;
102
+ *) err "ERROR: unknown arg: $1"; exit 2 ;;
103
+ esac
104
+ done
105
+
106
+ # --- manifest writer (called on EVERY exit path) ---------------------------
107
+ M_STATUS="error" M_ERROR="-" M_WTPATH="-" M_PORT="-" M_CREATED="-" M_BASELINE="-" M_BLOG="-"
108
+ write_manifest() {
109
+ [ -n "$MANIFEST" ] || return 0
110
+ {
111
+ printf 'status: %s\n' "$M_STATUS"
112
+ printf 'error: %s\n' "$M_ERROR"
113
+ printf 'worktree_path: %s\n' "$M_WTPATH"
114
+ printf 'branch: %s\n' "${BRANCH:--}"
115
+ printf 'port: %s\n' "$M_PORT"
116
+ printf 'created_at: %s\n' "$M_CREATED"
117
+ printf 'baseline: %s\n' "$M_BASELINE"
118
+ printf 'baseline_log: %s\n' "$M_BLOG"
119
+ } > "$MANIFEST" 2>/dev/null || true
120
+ }
121
+ fail() { M_STATUS="error"; M_ERROR="$1"; write_manifest; err "ERROR: $1"; exit "${2:-1}"; }
122
+
123
+ [ -n "$BRANCH" ] || { err "ERROR: --branch is required"; exit 2; }
124
+ [ -n "$SLUG" ] || { err "ERROR: --slug is required"; exit 2; }
125
+ [ -n "$MANIFEST" ] || { err "ERROR: --manifest is required"; exit 2; }
126
+
127
+ # --- canonical main-repo-root resolver (mirrors allocate-id.sh resolve_main;
128
+ # inlined by the same rationale that script documents — it is not on disk
129
+ # inside a freshly-checked-out worktree, and sourcing across the freshly
130
+ # created worktree boundary is fragile). Built on --show-toplevel, NEVER
131
+ # --git-common-dir+parent (wrong under git init --separate-git-dir). -------
132
+ resolve_main() {
133
+ local top gd common
134
+ top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
135
+ gd="$(git rev-parse --git-dir 2>/dev/null)" || return 1
136
+ common="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
137
+ if [ "$gd" = "$common" ]; then
138
+ printf '%s\n' "$top"
139
+ else
140
+ (cd "$top/.." 2>/dev/null && git rev-parse --show-toplevel 2>/dev/null) || return 1
141
+ fi
142
+ }
143
+
144
+ # --- read a paths.* / git.* / stack.* scalar from baldart.config.yml --------
145
+ config_scalar() {
146
+ local block="$1" key="$2" cfg="$MAIN/baldart.config.yml"
147
+ [ -f "$cfg" ] || return 0
148
+ grep -A60 "^${block}:" "$cfg" 2>/dev/null \
149
+ | grep -m1 "[[:space:]]*${key}:" \
150
+ | sed -E "s/.*${key}:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" \
151
+ | sed -E 's/[[:space:]]+$//'
152
+ }
153
+
154
+ # --- resolve MAIN ----------------------------------------------------------
155
+ if [ -z "$MAIN" ]; then
156
+ MAIN="$(resolve_main)" || fail "cannot resolve main repo root (not inside a git repository)" 1
157
+ fi
158
+ [ -d "$MAIN/.git" ] || [ -e "$MAIN/.git" ] || fail "resolved main '$MAIN' is not a git repo" 1
159
+
160
+ # --- resolve TRUNK (config → origin/HEAD → develop/main/master) -------------
161
+ if [ -z "$TRUNK" ]; then
162
+ TRUNK="$(config_scalar git trunk_branch)"
163
+ fi
164
+ if [ -z "$TRUNK" ]; then
165
+ TRUNK="$(git -C "$MAIN" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed -E 's#^refs/remotes/origin/##' || true)"
166
+ fi
167
+ if [ -z "$TRUNK" ]; then
168
+ for cand in develop main master; do
169
+ if git -C "$MAIN" show-ref --verify --quiet "refs/heads/$cand"; then TRUNK="$cand"; break; fi
170
+ done
171
+ fi
172
+ [ -n "$TRUNK" ] || fail "git.trunk_branch unresolved (config + origin/HEAD + develop/main/master all empty)" 1
173
+
174
+ # --- resolve env_files (CSV → array; default .env.local,.env when unset) ----
175
+ ENV_FILES=()
176
+ if [ "$ENV_FILES_SET" = 1 ]; then
177
+ # explicit --env-files wins, even if empty (caller may intentionally pass none)
178
+ IFS=',' read -r -a ENV_FILES <<< "$ENV_FILES_ARG"
179
+ else
180
+ # not passed → try config stack.env_files (CSV/inline-list best-effort), else default
181
+ cfg_env="$(grep -A4 '^[[:space:]]*env_files:' "$MAIN/baldart.config.yml" 2>/dev/null | tr -d '[]"' | grep -oE "\.[A-Za-z0-9._/-]+" | tr '\n' ',' || true)"
182
+ if [ -n "$cfg_env" ]; then
183
+ IFS=',' read -r -a ENV_FILES <<< "$cfg_env"
184
+ else
185
+ ENV_FILES=( ".env.local" ".env" )
186
+ fi
187
+ fi
188
+ # strip whitespace from each entry
189
+ _clean=(); for ef in "${ENV_FILES[@]:-}"; do ef="$(printf '%s' "$ef" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"; [ -n "$ef" ] && _clean+=("$ef"); done
190
+ ENV_FILES=("${_clean[@]:-}")
191
+
192
+ # --- resolve backlog dir + toolchain commands ------------------------------
193
+ [ -n "$BACKLOG_DIR" ] || { BACKLOG_DIR="$(config_scalar paths backlog_dir)"; [ -n "$BACKLOG_DIR" ] || BACKLOG_DIR="backlog"; }
194
+ [ -n "$INSTALL_CMD" ] || INSTALL_CMD="npm install"
195
+ # toolchain.commands.* only when features.has_toolchain: true; else the defaults.
196
+ _tc() {
197
+ local cfg="$MAIN/baldart.config.yml"
198
+ grep -E '^[[:space:]]*has_toolchain:[[:space:]]*true' "$cfg" >/dev/null 2>&1 || return 0
199
+ grep -A20 '^toolchain:' "$cfg" 2>/dev/null | grep -A15 '^[[:space:]]*commands:' \
200
+ | grep -E "^[[:space:]]+$1:" | head -1 \
201
+ | sed -E "s/.*$1:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" | sed -E 's/[[:space:]]+$//'
202
+ }
203
+ [ -n "$TC_TC" ] || TC_TC="$(_tc typecheck)"; [ -n "$TC_TC" ] || TC_TC="npx tsc --noEmit"
204
+ [ -n "$TC_LINT" ] || TC_LINT="$(_tc lint)"; [ -n "$TC_LINT" ] || TC_LINT="npx eslint --max-warnings=0 src/"
205
+ [ -n "$TC_BUILD" ] || TC_BUILD="$(_tc build)"; [ -n "$TC_BUILD" ] || TC_BUILD="npm run build"
206
+
207
+ # --- log sink --------------------------------------------------------------
208
+ [ -n "$LOG" ] || LOG="/tmp/wt-setup-${SLUG}.log"
209
+ : > "$LOG" 2>/dev/null || true
210
+
211
+ # --- worktree path is deterministic from the branch (R8) -------------------
212
+ WT_REL=".worktrees/$(printf '%s' "$BRANCH" | tr '/' '-')"
213
+ WT_ABS="$MAIN/$WT_REL"
214
+ M_WTPATH="$WT_ABS"
215
+
216
+ # --- lock helpers (mkdir is atomic on POSIX; same pattern as allocate-id.sh) -
217
+ WT_DIR="$MAIN/.worktrees"
218
+ LOCKDIR="$WT_DIR/.wt-setup.lock"
219
+ LOCK_STALE_SECONDS=30
220
+ LOCK_MAX_TRIES=150
221
+ file_mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0; }
222
+ acquire_lock() {
223
+ local tries=0 now m age
224
+ mkdir -p "$WT_DIR" 2>/dev/null || true
225
+ while ! mkdir "$LOCKDIR" 2>/dev/null; do
226
+ if [ -d "$LOCKDIR" ]; then
227
+ now="$(date +%s)"; m="$(file_mtime "$LOCKDIR")"
228
+ case "$m" in ''|*[!0-9]*) m=0 ;; esac
229
+ age=$(( now - m ))
230
+ if [ "$m" -gt 0 ] && [ "$age" -gt "$LOCK_STALE_SECONDS" ]; then
231
+ err "WARN: stealing stale wt-setup lock (age ${age}s)"; rm -rf "$LOCKDIR" 2>/dev/null; continue
232
+ fi
233
+ fi
234
+ tries=$(( tries + 1 ))
235
+ [ "$tries" -gt "$LOCK_MAX_TRIES" ] && { err "WARN: could not acquire wt-setup lock — proceeding without it (port race possible)"; return 1; }
236
+ sleep 0.2
237
+ done
238
+ printf '%s\n' "$$" > "$LOCKDIR/pid" 2>/dev/null || true
239
+ return 0
240
+ }
241
+ release_lock() { rm -rf "$LOCKDIR" 2>/dev/null || true; }
242
+
243
+ # --- registry upsert via node (atomic tmp+rename; no jq dependency) ---------
244
+ # Fields are passed through the environment to avoid bash→JSON quoting hell.
245
+ REG="$WT_DIR/registry.json"
246
+ registry_upsert() {
247
+ REG_FILE="$REG" \
248
+ R_CARD="$CARD" R_CARDS="$CARDS" R_GROUP="$GROUP_PARENT" R_SLUG="$SLUG" \
249
+ R_BRANCH="$BRANCH" R_TRUNK="$TRUNK" R_MAIN="$MAIN" R_PATH="$WT_ABS" \
250
+ R_PORT="$1" R_CREATED="$M_CREATED" R_ENVSYNCED="$2" R_BUILDVERIFIED="$3" \
251
+ node -e '
252
+ const fs = require("fs");
253
+ const f = process.env.REG_FILE;
254
+ let reg = { worktrees: [] };
255
+ try { const j = JSON.parse(fs.readFileSync(f, "utf8")); if (j && Array.isArray(j.worktrees)) reg = j; } catch (_) {}
256
+ const csv = (s) => (s || "").split(",").map(x => x.trim()).filter(Boolean);
257
+ const num = (s) => { const n = parseInt(s, 10); return Number.isFinite(n) ? n : null; };
258
+ const bool = (s) => s === "true" ? true : s === "false" ? false : null;
259
+ const entry = {
260
+ card: process.env.R_CARD || null,
261
+ cards: csv(process.env.R_CARDS),
262
+ groupParent: process.env.R_GROUP || null,
263
+ slug: process.env.R_SLUG || null,
264
+ branch: process.env.R_BRANCH,
265
+ trunkBranch: process.env.R_TRUNK || null,
266
+ mainRoot: process.env.R_MAIN || null,
267
+ path: process.env.R_PATH,
268
+ port: num(process.env.R_PORT),
269
+ createdAt: process.env.R_CREATED && process.env.R_CREATED !== "-" ? process.env.R_CREATED : null,
270
+ envSyncedAt: process.env.R_ENVSYNCED && process.env.R_ENVSYNCED !== "-" ? process.env.R_ENVSYNCED : null,
271
+ buildVerified: bool(process.env.R_BUILDVERIFIED),
272
+ };
273
+ const i = reg.worktrees.findIndex(w => w && w.branch === entry.branch);
274
+ if (i >= 0) reg.worktrees[i] = { ...reg.worktrees[i], ...entry };
275
+ else reg.worktrees.push(entry);
276
+ const tmp = f + ".tmp";
277
+ fs.writeFileSync(tmp, JSON.stringify(reg, null, 2) + "\n");
278
+ fs.renameSync(tmp, f);
279
+ ' 2>>"$LOG"
280
+ }
281
+ # ports currently held by registry entries (so concurrent setups never collide)
282
+ registry_used_ports() {
283
+ REG_FILE="$REG" node -e '
284
+ const fs = require("fs");
285
+ try {
286
+ const j = JSON.parse(fs.readFileSync(process.env.REG_FILE, "utf8"));
287
+ (j.worktrees || []).forEach(w => { if (w && Number.isFinite(w.port)) console.log(w.port); });
288
+ } catch (_) {}
289
+ ' 2>/dev/null
290
+ }
291
+
292
+ # ===========================================================================
293
+ # 1. .gitignore safety (auto-heal, idempotent, non-blocking)
294
+ if [ ! -f "$MAIN/.gitignore" ] || ! grep -qE '^\.worktrees/?$' "$MAIN/.gitignore"; then
295
+ printf '\n.worktrees/\n' >> "$MAIN/.gitignore"
296
+ err "WARN: appended '.worktrees/' to $MAIN/.gitignore (was missing)."
297
+ fi
298
+
299
+ # 2. Fetch trunk (read-only; never touches the main repo HEAD)
300
+ git -C "$MAIN" fetch origin "$TRUNK" --quiet 2>>"$LOG" || err "WARN: git fetch origin $TRUNK failed — branching from possibly-stale local ref"
301
+
302
+ # 3. Collision guard — a code worktree is NOT silently re-creatable. The caller's
303
+ # 4a2 pre-check should have reset any orphan; if something is still here, fail
304
+ # loud rather than clobber.
305
+ EXISTING="$(git -C "$MAIN" worktree list --porcelain | awk -v b="refs/heads/$BRANCH" '$1=="worktree"{p=$2} $1=="branch"&&$2==b{print p}')"
306
+ if [ -n "$EXISTING" ]; then
307
+ fail "branch '$BRANCH' already has a worktree at '$EXISTING' (caller's idempotency pre-check should reset/resume before invoking this script)" 3
308
+ fi
309
+ if [ -e "$WT_ABS" ]; then
310
+ fail "worktree path '$WT_ABS' already exists on disk (stale orphan — caller must reset it first)" 3
311
+ fi
312
+
313
+ # 4. Create the worktree off origin/$TRUNK + stamp created_at AT creation
314
+ git -C "$MAIN" worktree add "$WT_REL" -b "$BRANCH" "origin/$TRUNK" >>"$LOG" 2>&1 \
315
+ || fail "git worktree add failed (see $LOG)" 4
316
+ M_CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
317
+ # Provisional NON-terminal manifest — written the instant the worktree exists, so
318
+ # that if this process is KILLED mid-install/build (e.g. the caller's background
319
+ # timeout fires, or no `timeout` binary exists on this host so the build runs
320
+ # unbounded) the manifest on disk still reads status:error / baseline:- . The
321
+ # caller's disk gate (setup.md 6a) treats anything that is NOT a terminal
322
+ # `status: ok` + `baseline: pass` as NOT-verified → fallback, never a false pass
323
+ # on a half-built worktree whose node_modules merely happens to exist.
324
+ M_STATUS="error"; M_ERROR="setup incomplete (worktree created, install/build not finished — process killed or interrupted)"
325
+ write_manifest
326
+
327
+ # 4b. Sync untracked backlog cards from main (created during /prd, not yet on trunk)
328
+ if [ -d "$MAIN/$BACKLOG_DIR" ]; then
329
+ mkdir -p "$WT_ABS/$BACKLOG_DIR" 2>/dev/null || true
330
+ for CARD_FILE in "$MAIN/$BACKLOG_DIR"/*.yml; do
331
+ [ -e "$CARD_FILE" ] || continue
332
+ BN="$(basename "$CARD_FILE")"
333
+ [ -f "$WT_ABS/$BACKLOG_DIR/$BN" ] || cp "$CARD_FILE" "$WT_ABS/$BACKLOG_DIR/$BN" 2>/dev/null || true
334
+ done
335
+ fi
336
+
337
+ cd "$WT_ABS" || fail "cannot cd into worktree '$WT_ABS'" 4
338
+
339
+ # 5. Install dependencies — NON-INTERACTIVE (stdin closed, CI=1). An install that
340
+ # blocks on a prompt is the bash equivalent of the model "going idle" — the
341
+ # exact failure this script eliminates; never reintroduce it via a prompt.
342
+ err "… installing dependencies ($INSTALL_CMD)"
343
+ if ! CI=1 bash -c "$INSTALL_CMD" </dev/null >>"$LOG" 2>&1; then
344
+ M_BASELINE="fail"; M_BLOG="$LOG"
345
+ fail "dependency install failed ($INSTALL_CMD) — see $LOG" 5
346
+ fi
347
+
348
+ # 6. Copy gitignored env artifacts (stack.env_files). FILES via plain cp
349
+ # (dereferences symlinks; never cp -P), DIRS via cp -R. Missing FILE → WARN,
350
+ # never abort (the baseline build gate is the real enforcement).
351
+ primary_env=""
352
+ copied_any=0
353
+ for ef in "${ENV_FILES[@]:-}"; do
354
+ [ -n "$ef" ] || continue
355
+ src="$MAIN/$ef"; dest_dir="$(dirname "$ef")"
356
+ if [ -d "$src" ]; then
357
+ mkdir -p "$dest_dir"; rm -rf "$ef"
358
+ cp -R "$src" "$dest_dir"/ 2>/dev/null || true
359
+ [ -e "$ef" ] && copied_any=1 || err "WARN: env dir '$ef' present in main but copy failed."
360
+ elif [ -f "$src" ]; then
361
+ mkdir -p "$dest_dir"
362
+ if cp "$src" "$ef" 2>>"$LOG"; then copied_any=1; [ -z "$primary_env" ] && primary_env="$ef"; fi
363
+ else
364
+ err "WARN: env artifact '$ef' (stack.env_files) not found in $MAIN — the build may fail if it is required."
365
+ fi
366
+ done
367
+ ENVSYNCED="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
368
+ if [ "$copied_any" = 0 ] && [ "${#ENV_FILES[@]}" -gt 0 ]; then
369
+ err "WARN: NO env artifacts copied (none of: ${ENV_FILES[*]:-} exist in $MAIN)."
370
+ fi
371
+
372
+ # 7. Allocate a free port UNDER THE LOCK (registry-held ports + live listeners),
373
+ # then write a PROVISIONAL registry entry (buildVerified:false) so the port is
374
+ # reserved against concurrent /nw before the long build runs. The lock is held
375
+ # only across the cheap pick+write — never across npm install / build.
376
+ PORT=""
377
+ if acquire_lock; then trap release_lock EXIT; fi
378
+ USED="$(registry_used_ports | tr '\n' ' ')"
379
+ p="$PORT_MIN"
380
+ while [ "$p" -le "$PORT_MAX" ]; do
381
+ if printf ' %s ' "$USED" | grep -q " $p " ; then p=$(( p + 1 )); continue; fi
382
+ if lsof -i :"$p" -sTCP:LISTEN >/dev/null 2>&1; then p=$(( p + 1 )); continue; fi
383
+ PORT="$p"; break
384
+ done
385
+ if [ -z "$PORT" ]; then
386
+ release_lock; trap - EXIT
387
+ M_BASELINE="fail"; M_BLOG="$LOG"
388
+ fail "no free port in ${PORT_MIN}-${PORT_MAX} (deterministic — recreating cannot fix)" 6
389
+ fi
390
+ M_PORT="$PORT"
391
+ registry_upsert "$PORT" "$ENVSYNCED" "false"
392
+ release_lock; trap - EXIT
393
+
394
+ # 7b. Persist PORT into the primary copied env file (the dev server reads it).
395
+ PORT_ENV_FILE="${primary_env:-.env.local}"
396
+ if grep -q "^PORT=" "$PORT_ENV_FILE" 2>/dev/null; then
397
+ sed -i '' "s/^PORT=.*/PORT=$PORT/" "$PORT_ENV_FILE" 2>/dev/null \
398
+ || sed -i "s/^PORT=.*/PORT=$PORT/" "$PORT_ENV_FILE" 2>/dev/null || true
399
+ else
400
+ echo "PORT=$PORT" >> "$PORT_ENV_FILE" 2>/dev/null || true
401
+ fi
402
+
403
+ # 8. Assert git hooks are ACTIVE (best-effort WARN, never blocks)
404
+ HOOK_SRC=""
405
+ for d in .husky .githooks scripts/git-hooks githooks; do
406
+ if [ -d "$d" ] && ls "$d" 2>/dev/null | grep -qE '^(pre-commit|pre-push|commit-msg)$'; then HOOK_SRC="$d"; break; fi
407
+ done
408
+ if [ -n "$HOOK_SRC" ]; then
409
+ ACTIVE="$(git config --get core.hooksPath || echo .git/hooks)"
410
+ case "$ACTIVE" in
411
+ "$HOOK_SRC"|"$HOOK_SRC"/*|.husky/_) : ;;
412
+ *) err "WARN: git hooks INACTIVE — versioned hooks in '$HOOK_SRC' but core.hooksPath='$ACTIVE'. Fix: git config core.hooksPath $HOOK_SRC" ;;
413
+ esac
414
+ fi
415
+
416
+ # 9. Baseline — tsc/lint are report-but-continue; build is STOP-on-fail.
417
+ # The build runs under a HARD timeout so a hung/interactive build cannot stall
418
+ # the caller's pre-flight barrier forever.
419
+ err "… baseline typecheck ($TC_TC)"
420
+ CI=1 bash -c "$TC_TC" </dev/null >>"$LOG" 2>&1 || err "WARN: typecheck failed (report-but-continue — trunk should be clean; see $LOG)"
421
+ err "… baseline lint ($TC_LINT)"
422
+ CI=1 bash -c "$TC_LINT" </dev/null >>"$LOG" 2>&1 || err "WARN: lint failed (report-but-continue; see $LOG)"
423
+
424
+ # Resolve a timeout binary (coreutils 'timeout', or gtimeout on macOS). If none,
425
+ # run without one (best-effort) and WARN — better than aborting setup.
426
+ TIMEOUT_BIN=""
427
+ command -v timeout >/dev/null 2>&1 && TIMEOUT_BIN="timeout"
428
+ [ -z "$TIMEOUT_BIN" ] && command -v gtimeout >/dev/null 2>&1 && TIMEOUT_BIN="gtimeout"
429
+
430
+ err "… baseline build ($TC_BUILD, timeout ${BUILD_TIMEOUT}s)"
431
+ if [ -n "$TIMEOUT_BIN" ]; then
432
+ CI=1 "$TIMEOUT_BIN" "$BUILD_TIMEOUT" bash -c "$TC_BUILD" </dev/null >>"$LOG" 2>&1
433
+ BUILD_RC=$?
434
+ else
435
+ err "WARN: no 'timeout'/'gtimeout' binary — running build WITHOUT a hard timeout."
436
+ CI=1 bash -c "$TC_BUILD" </dev/null >>"$LOG" 2>&1
437
+ BUILD_RC=$?
438
+ fi
439
+
440
+ if [ "$BUILD_RC" -eq 124 ] && [ -n "$TIMEOUT_BIN" ]; then
441
+ M_BASELINE="timeout"; M_BLOG="$LOG"
442
+ # leave buildVerified:false in the registry (already provisional)
443
+ fail "baseline build TIMED OUT after ${BUILD_TIMEOUT}s — partial log at $LOG" 7
444
+ elif [ "$BUILD_RC" -ne 0 ]; then
445
+ M_BASELINE="fail"; M_BLOG="$LOG"
446
+ fail "baseline build FAILED (do NOT fix here — the coder repairs it) — see $LOG" 7
447
+ fi
448
+
449
+ # 10. Build passed → flip buildVerified:true in the registry and finish clean.
450
+ if acquire_lock; then trap release_lock EXIT; fi
451
+ registry_upsert "$PORT" "$ENVSYNCED" "true"
452
+ release_lock; trap - EXIT
453
+
454
+ M_STATUS="ok"; M_ERROR="-"; M_BASELINE="pass"; M_BLOG="-"
455
+ write_manifest
456
+ err "✓ worktree ready: $WT_ABS (branch $BRANCH, port $PORT, baseline pass)"
457
+ exit 0
@@ -268,7 +268,7 @@ try {
268
268
  `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` +
269
269
  `DETERMINISTIC GATE POLICIES (NO user prompts):\n` +
270
270
  `• 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` +
271
- `• 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` +
271
+ `• Worktree (setup.md step 4): the ENTIRE build sequence (worktree add install env copy port registry baseline) is the SSOT script \`scripts/setup-worktree.sh\` the SAME script /nw and classic /new run. Do NOT hand-roll the bash (a re-authored copy is the duplication CLAUDE.md forbids, and the script cannot fabricate a baseline or stall). Resolve the branch from the lead/parent card's git_strategy.branch (fallback feat/<PARENT-ID|lead>-<slug>) + a kebab slug, then derive the git-authoritative idempotency pre-check (existing worktree for the branch reset the half-built orphan before running, never re-create into a fail-loud collision). Resolve the script: \`SETUP_SH="$(ls .framework/framework/.claude/skills/worktree-manager/scripts/setup-worktree.sh .claude/skills/worktree-manager/scripts/setup-worktree.sh 2>/dev/null | head -1)"\`. Run it (foregroundyou ARE the background context here): \`bash "$SETUP_SH" --main ${MAIN} --trunk ${TRUNK} --branch <branch> --slug <slug> --cards "${cardIds.join(',')}" --group-parent "<PARENT|>" --env-files "${ENV_FILES.join(',')}" --manifest /tmp/new2-wt-${firstCard}.txt\`. It installs non-interactively, copies stack.env_files (FILES plain cp / DIRS cp -R / missing FILE → WARN, never abort; never .env.example, never bulk-copy untracked), allocates a locked port, writes the registry, and runs the baseline under timeout 600 (tsc/lint report-but-continue, build STOP-on-fail). Read the manifest block (status/baseline/worktree_path/port/created_at/baseline_log): map baseline → your baseline field (pass|fail|timeout) and baseline_log baselineLog. E2: a \`baseline:fail\`/\`status:error\` build break is NOT yours to fix (role boundary — the coder repairs it); just return it precisely. Then, INDEPENDENTLY of the manifest, VERIFY the worktree on disk and return EVIDENCE (the workflow E2.5 gate string-matches it, never the script's word): set worktreeVerified:true ONLY after running \`git -C ${MAIN} worktree list --porcelain\` (the worktree path MUST appear) 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 }. If the script is ABSENT (older subtree), fall back to running the sequence inline per setup.md step 4 prose. NEVER report worktreeVerified:true without actually running the verify commands.\n` +
272
272
  codexResolveBullet +
273
273
  g3Bullet +
274
274
  `• 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` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.52.3",
3
+ "version": "4.53.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"