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.
| Location | Role today |
|---|---|
| apps/sdlc/lib/services/git/commit-to-main.ts#commitToMainViaWorktree | Creates 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#manageBranch | Same bare git worktree add --detach; writeCommitPush then commits with hook execution that needs the workspace deps |
| apps/sdlc/lib/config/load.ts#SdlcConfigSchema | Declares the quality_checks and worktree_init verb lists; no key covers ephemeral commit-worktrees |
| apps/sdlc/lib/config/sdlc_yaml.ts#SCHEMA_OWNED_KEYS | Schema-owned verb-list keys: quality_checks, worktree_init |
| apps/sdlc/lib/services/config/verb-list.ts | Shared verb-list loader/executor used by the worktree-init and quality flows |
| sdlc.yaml | Declares worktree_init: [bun install] — the full install, too slow to pay on every ephemeral commit |
Proposed
Section titled “Proposed”- New optional top-level
commit_worktree_init:key insdlc.yaml, same list-of-shell-verbs shape asworktree_init:. Fallback rule: key absent or null resolves to theworktree_init:verbs; an explicitly empty list means no-op. The key joinsSCHEMA_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.yamldeclarescommit_worktree_initasmise trustfollowed bybun install --frozen-lockfile --filter '@sksizer/sdlc'(measured ~0.7s / 184 packages during the manual replications of 2026-07-18).
Approach
Section titled “Approach”- Schema: add
commit_worktree_inittoSdlcConfigSchemainapps/sdlc/lib/config/load.ts(optional/nullable, mirroring theworktree_initfield’s shape and describe-prose pattern); add the key toSCHEMA_OWNED_KEYSinapps/sdlc/lib/config/sdlc_yaml.ts. - Write
arm-worktree.tswith 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. - Wire
commitToMainViaWorktree: arm once, right after theworktree addsucceeds and before the attempt loop. Once per worktree is enough — the loop’sgit reset --harddoes not removenode_modules. - Wire
manageBranchinapps/sdlc/cli/backlog_cli/create.ts: arm after itsworktree add, beforewriteCommitPush. - 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). - Declare the key in this repo’s
sdlc.yamlper Proposed. - 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.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
| apps/sdlc/lib/services/git/arm-worktree.ts | new | commitWorktreeInitVerbs + armCommitWorktree, exec-seam injected |
| apps/sdlc/lib/services/git/commit-to-main.ts#commitToMainViaWorktree | modify | Arm immediately after worktree add, before the attempt loop |
| apps/sdlc/cli/backlog_cli/create.ts#manageBranch | modify | Arm after worktree add, before writeCommitPush |
| apps/sdlc/lib/config/load.ts#SdlcConfigSchema | modify | Add optional commit_worktree_init verb list |
| apps/sdlc/lib/config/sdlc_yaml.ts#SCHEMA_OWNED_KEYS | modify | Add commit_worktree_init |
| sdlc.yaml | modify | Declare commit_worktree_init (mise trust + filtered install) |
| apps/sdlc/conventions/sdlc-yaml.md | modify | Document the key, its fallback rule, and the speed rationale |
| apps/sdlc/lib/services/git/tests/ | modify | Ordering test: arm verbs run between worktree add and the first commit attempt |
| apps/sdlc/cli/backlog_cli/tests/create.test.ts | modify | Ordering test for the backlog capture path |
| apps/sdlc/lib/config/tests/load.test.ts | modify | Schema accepts the key; fallback matrix |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
SdlcConfigSchemaacceptscommit_worktree_initas 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_initverbs; present-but-empty resolves to zero verbs; present resolves to its own verbs. - AC-3: recorded-exec test on
commitToMainViaWorktreeshows the arm verbs execute aftergit worktree addand before the firstgit commit. - AC-4: recorded-exec test on the backlog capture path shows the arm
verbs execute after
git worktree addand before the first git invocation ofwriteCommitPush. - AC-5: this repo’s
sdlc.yamldeclares the key per Proposed andapps/sdlc/conventions/sdlc-yaml.mddocuments 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.
Out of scope
Section titled “Out of scope”- task-work Step 4’s
worktree_initflow for long-lived task worktrees — unchanged. - Changing Bun’s linker mode or hoisting workspace deps — the isolated linker stays.
/sdlc:find-worktree-initinteractive coverage for the new key.- Retro-arming worktrees that already exist.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”- 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.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-07-18. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
bun test apps/sdlc/lib/config(load.test.tspresent/absent/null/empty/invalid cases;schema.test.tsconfirms item-minLength and expected-array diagnostics match theworktree_initformat verbatim). - AC-2: auto —
bun test apps/sdlc/lib/config(fallback matrix inload.test.ts: absent/null →worktree_initverbs; present-empty → zero; present → own). - AC-3: auto —
bun test apps/sdlc/lib/services/git(commit-to-main.test.tsrecorded-exec: arm verbs run aftergit worktree add, before the firstgit commit, in order, armed once). - AC-4: auto —
bun test apps/sdlc/cli/backlog_cli(create.test.tsrecorded-Runner: arm verbs run aftergit worktree add, beforewriteCommitPush’s first git call). - AC-5: auto —
sdlc.yamldeclarescommit_worktree_init(mise trust+bun install --frozen-lockfile --filter '@sksizer/sdlc');apps/sdlc/conventions/sdlc-yaml.mddocuments 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 throwsArmWorktreeErrornaming the verb + stderr, skips later verbs, and never surfacesCannot find package).
Whole-branch gate: sdlc quality run --diff-against-baseline → OK 10/10, 0 new-drift.
What worked
Section titled “What worked”- 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.
Friction and automation gaps
Section titled “Friction and automation gaps”- 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
mainrequired a manual scopedLEFTHOOK=0skip. The bootstrapping gap closes once this merges, butcommitToMainViaWorktreeshould surface an actionable error (pointing atcommit_worktree_init) when a worktree’s pre-commit hook fails to resolve deps, instead of the opaqueCannot find package 'commander'bubbling up fromgit commit. sdlc quality run --diff-against-baselineinvoked from the worktree (Step 7’s documented invocation) defaults--baseline-dirto the worktree’s.sdlc/quality-baselines/, but Step 3a captured the baseline in the main checkout’s.sdlc/— so the gate errorsbaseline not found(exit 1) unless--baseline-diris 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 --helpomits the requiredtask_idpositional, so following the help text fails witharguments are required: task_idon the first call — the positional should be documented in the subcommand help.- The spec said to arm through the
git: CommandRunnerseam, but that runner is git-bound and cannot runbun install/mise trust; the implementer introduced a parallel injectedArmExecseam instead. Minor spec imprecision, resolved during implementation.