Skip to content

T-ML6A-arm-ephemeral-commit-worktrees

Status: closed/done · Impact: high · Complexity: medium

Every consumer of ephemeral commit-worktrees fails at the commit step on this host: the worktree is created bare, Bun’s isolated linker links workspace deps (commander, yaml) only at apps/sdlc/node_modules, and the root node_modules carries only root deps — so the pre-commit hooks’ bun invocations die with “Cannot find package ‘commander’”. This breaks sdlc backlog create and every commitToMainViaWorktree caller (start-task, ensure-ready, close-commit, PR-record). Captured as B-AAQ5-backlog-ephemeral-worktree-node-modules.

LocationRole today
apps/sdlc/lib/services/git/commit-to-main.ts#commitToMainViaWorktreeCreates the bare detached worktree off origin/main; its comment assumes node_modules resolves upward — false under Bun’s isolated linker
apps/sdlc/cli/backlog_cli/create.ts#manageBranchSame bare git worktree add --detach; writeCommitPush then commits with hook execution that needs the workspace deps
apps/sdlc/lib/config/load.ts#SdlcConfigSchemaDeclares the quality_checks and worktree_init verb lists; no key covers ephemeral commit-worktrees
apps/sdlc/lib/config/sdlc_yaml.ts#SCHEMA_OWNED_KEYSSchema-owned verb-list keys: quality_checks, worktree_init
apps/sdlc/lib/services/config/verb-list.tsShared verb-list loader/executor used by the worktree-init and quality flows
sdlc.yamlDeclares worktree_init: [bun install] — the full install, too slow to pay on every ephemeral commit
  • New optional top-level commit_worktree_init: key in sdlc.yaml, same list-of-shell-verbs shape as worktree_init:. Fallback rule: key absent or null resolves to the worktree_init: verbs; an explicitly empty list means no-op. The key joins SCHEMA_OWNED_KEYS.
  • New shared helper apps/sdlc/lib/services/git/arm-worktree.ts: commitWorktreeInitVerbs(projectRoot) resolves the effective verb list; armCommitWorktree(projectRoot, worktreeRoot, exec) runs each verb sequentially with the worktree root as cwd through an injected exec seam so tests can record ordering. A failing verb aborts loudly, naming the verb — never falls through to the hook’s resolution error.
  • Both creation sites arm the worktree immediately after git worktree add, before any hook-bearing git operation.
  • This repo’s sdlc.yaml declares commit_worktree_init as mise trust followed by bun install --frozen-lockfile --filter '@sksizer/sdlc' (measured ~0.7s / 184 packages during the manual replications of 2026-07-18).
  1. Schema: add commit_worktree_init to SdlcConfigSchema in apps/sdlc/lib/config/load.ts (optional/nullable, mirroring the worktree_init field’s shape and describe-prose pattern); add the key to SCHEMA_OWNED_KEYS in apps/sdlc/lib/config/sdlc_yaml.ts.
  2. Write arm-worktree.ts with the two exports and the fallback rule from Proposed. Verbs run in declared order, cwd = worktree root; a non-zero verb throws with the verb text and its stderr.
  3. Wire commitToMainViaWorktree: arm once, right after the worktree add succeeds and before the attempt loop. Once per worktree is enough — the loop’s git reset --hard does not remove node_modules.
  4. Wire manageBranch in apps/sdlc/cli/backlog_cli/create.ts: arm after its worktree add, before writeCommitPush.
  5. Document the key in apps/sdlc/conventions/sdlc-yaml.md: shape, fallback rule, and why ephemeral commits want a faster list than task worktrees (sub-second filtered install vs full install).
  6. Declare the key in this repo’s sdlc.yaml per Proposed.
  7. Tests: recorded-exec ordering in both wiring sites; config fallback matrix in the config tests. The first post-merge lifecycle state commit is the live proof — the observed failure mode disappears.
LocationKindChange
apps/sdlc/lib/services/git/arm-worktree.tsnewcommitWorktreeInitVerbs + armCommitWorktree, exec-seam injected
apps/sdlc/lib/services/git/commit-to-main.ts#commitToMainViaWorktreemodifyArm immediately after worktree add, before the attempt loop
apps/sdlc/cli/backlog_cli/create.ts#manageBranchmodifyArm after worktree add, before writeCommitPush
apps/sdlc/lib/config/load.ts#SdlcConfigSchemamodifyAdd optional commit_worktree_init verb list
apps/sdlc/lib/config/sdlc_yaml.ts#SCHEMA_OWNED_KEYSmodifyAdd commit_worktree_init
sdlc.yamlmodifyDeclare commit_worktree_init (mise trust + filtered install)
apps/sdlc/conventions/sdlc-yaml.mdmodifyDocument the key, its fallback rule, and the speed rationale
apps/sdlc/lib/services/git/tests/modifyOrdering test: arm verbs run between worktree add and the first commit attempt
apps/sdlc/cli/backlog_cli/tests/create.test.tsmodifyOrdering test for the backlog capture path
apps/sdlc/lib/config/tests/load.test.tsmodifySchema accepts the key; fallback matrix
  • AC-1: SdlcConfigSchema accepts commit_worktree_init as an optional list of non-empty strings and keeps the existing schema-violation diagnostic format.
  • AC-2: fallback semantics unit-tested — key absent or null resolves to the worktree_init verbs; present-but-empty resolves to zero verbs; present resolves to its own verbs.
  • AC-3: recorded-exec test on commitToMainViaWorktree shows the arm verbs execute after git worktree add and before the first git commit.
  • AC-4: recorded-exec test on the backlog capture path shows the arm verbs execute after git worktree add and before the first git invocation of writeCommitPush.
  • AC-5: this repo’s sdlc.yaml declares the key per Proposed and apps/sdlc/conventions/sdlc-yaml.md documents it.
  • AC-6: a failing arm verb aborts the flow with an error naming the verb (unit-tested), instead of surfacing the hook’s module-resolution error.
  • task-work Step 4’s worktree_init flow for long-lived task worktrees — unchanged.
  • Changing Bun’s linker mode or hoisting workspace deps — the isolated linker stays.
  • /sdlc:find-worktree-init interactive coverage for the new key.
  • Retro-arming worktrees that already exist.
  • none
  • B-AAQ5-backlog-ephemeral-worktree-node-modules, captured 2026-07-18 when a backlog capture died at the commit step. The fix verbs and their timing were proven the same day by four manual armed-worktree replications during T-2KK8’s lifecycle.

Captured by /sdlc:task-work on 2026-07-18. PR: pending.

  • AC-1: auto — bun test apps/sdlc/lib/config (load.test.ts present/absent/null/empty/invalid cases; schema.test.ts confirms item-minLength and expected-array diagnostics match the worktree_init format verbatim).
  • AC-2: auto — bun test apps/sdlc/lib/config (fallback matrix in load.test.ts: absent/null → worktree_init verbs; present-empty → zero; present → own).
  • AC-3: auto — bun test apps/sdlc/lib/services/git (commit-to-main.test.ts recorded-exec: arm verbs run after git worktree add, before the first git commit, in order, armed once).
  • AC-4: auto — bun test apps/sdlc/cli/backlog_cli (create.test.ts recorded-Runner: arm verbs run after git worktree add, before writeCommitPush’s first git call).
  • AC-5: auto — sdlc.yaml declares commit_worktree_init (mise trust + bun install --frozen-lockfile --filter '@sksizer/sdlc'); apps/sdlc/conventions/sdlc-yaml.md documents shape, fallback rule, and the filtered-vs-full-install rationale; docs-drift + rumdl gates green.
  • AC-6: auto — bun test apps/sdlc/lib/services/git (arm-worktree.test.ts: a failing verb throws ArmWorktreeError naming the verb + stderr, skips later verbs, and never surfaces Cannot find package).

Whole-branch gate: sdlc quality run --diff-against-baselineOK 10/10, 0 new-drift.

  • The task spec passed the readiness gate (gap-report + corpus scan) on first read — no definition round needed.
  • The recorded-exec seam already used in the git-service tests was a direct fit for the AC-3/AC-4 ordering assertions.
  • The baseline-gated quality gate cleanly separated this branch’s (zero) new drift from pre-existing findings, so review attention isn’t wasted on unrelated corpus state.
  • task-work’s own Step 5a/5b commit-to-main path is broken by the exact bug this task fixes, so the fix could not self-host: landing the verify/start commits on main required a manual scoped LEFTHOOK=0 skip. The bootstrapping gap closes once this merges, but commitToMainViaWorktree should surface an actionable error (pointing at commit_worktree_init) when a worktree’s pre-commit hook fails to resolve deps, instead of the opaque Cannot find package 'commander' bubbling up from git commit.
  • sdlc quality run --diff-against-baseline invoked from the worktree (Step 7’s documented invocation) defaults --baseline-dir to the worktree’s .sdlc/quality-baselines/, but Step 3a captured the baseline in the main checkout’s .sdlc/ — so the gate errors baseline not found (exit 1) unless --baseline-dir is passed explicitly. Fix: resolve the baseline dir against the main checkout (git-common-dir’s parent) so Step 7 works from a worktree without the extra flag.
  • sdlc lease heartbeat-loop start --help omits the required task_id positional, so following the help text fails with arguments are required: task_id on the first call — the positional should be documented in the subcommand help.
  • The spec said to arm through the git: CommandRunner seam, but that runner is git-bound and cannot run bun install/mise trust; the implementer introduced a parallel injected ArmExec seam instead. Minor spec imprecision, resolved during implementation.

← Back to Tasks