baldart 4.52.4 → 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 +14 -0
- package/VERSION +1 -1
- package/framework/.claude/skills/new/SKILL.md +1 -1
- package/framework/.claude/skills/new/references/setup.md +24 -32
- package/framework/.claude/skills/worktree-manager/SKILL.md +55 -188
- package/framework/.claude/skills/worktree-manager/scripts/setup-worktree.sh +457 -0
- package/framework/.claude/workflows/new2.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ 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
|
+
|
|
8
22
|
## [4.52.4] - 2026-06-18
|
|
9
23
|
|
|
10
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).
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.53.0
|
|
@@ -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
|
|
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
|
|
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. **
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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 pathology — haiku 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
|
|
313
|
-
- **
|
|
314
|
-
- **
|
|
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
|
|
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
|
|
577
|
+
### 3-6. Create + isolate + baseline + registry — `scripts/setup-worktree.sh` (SSOT)
|
|
578
578
|
|
|
579
|
-
|
|
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
|
-
|
|
589
|
+
Invoke it from the **main checkout** (cwd = `$MAIN`):
|
|
582
590
|
|
|
583
591
|
```bash
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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: <
|
|
656
|
+
Path: <worktree_path>
|
|
790
657
|
Branch: feat/<CARD-ID>-<slug> (or feat/<slug> if no card)
|
|
791
|
-
Port: <
|
|
658
|
+
Port: <port>
|
|
792
659
|
Card: <CARD-ID> (or "none" if slug-only)
|
|
793
|
-
Build: verified ✓
|
|
794
|
-
Dev: http://localhost:<
|
|
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):
|
|
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 (foreground — you 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` +
|