Skip to content

T-QWJW-scope-guard-root-shared-file-carve-out

Status: open/ready · Impact: medium · Complexity: small

AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-07-20 because the task is autonomy: autonomous/pr. Review the Goal, Approach, Today, Files-to-touch, and Acceptance-criteria carefully before trusting it.

The worktree-scope-guard pre-commit gate rejects any staged path that also exists inside an active worktree. Root-level shared files are present in every worktree by construction, so commits touching them always false-positive and no root-lockfile fix can land on main. This task adds a root-shared-file carve-out (or a documented escape hatch) so those commits are landable.

The worktree-scope-guard pre-commit gate rejects any staged path that also exists inside an active worktree. Root-level shared files — bun.lock, Cargo.lock, and the root package.json — exist in all 92 worktrees, so every such commit false-positives and no root-lockfile fix can land on main without a documented escape hatch. This bit T-1YSW: a stale root bun.lock wedged every commit-to-main path and the fix was unlandable through the guard. The guard’s carve-out list already covers docs/planning/tasks/ and generated docs; add a root-shared-file carve-out (root-level lockfiles and root package.json) or a documented, discoverable escape hatch. Touchpoints: apps/sdlc/lib/services/gate/ops/worktree-scope.ts and apps/sdlc/conventions/worktree-scope-guard.md.

T-1YSW-amend-d0001-solutions-tier

LocationRole today
apps/sdlc/lib/services/gate/ops/worktree-scope.ts#ALLOWED_PREFIXESThe whole carve-out list: docs/planning/tasks/ plus the spread of GENERATED_DOCS_ROOTS. Matched by startsWith only — there is no exact-path arm.
apps/sdlc/lib/services/gate/ops/worktree-scope.ts#findCollisionsWalks the staged paths, skips a path when a carve-out prefix matches, and otherwise reports a collision for any worktree basename under which the path exists on disk.
apps/sdlc/lib/util/generated-docs.ts#GENERATED_DOCS_ROOTSThe generated-docs carve-out set (docs/index.md, docs/glossary.md, docs/references.md), shared with the regenerate-and-stage helper so the two sides cannot drift.
apps/sdlc/conventions/worktree-scope-guard.mdThe guard’s contract doc. Its ## Carve-outs section documents the two existing carve-outs plus the WORKTREE_SCOPE_GUARD=skip bypass.
apps/sdlc/lib/services/gate/tests/worktree_scope.test.tsSuite driving the op through the real CLI against an ephemeral repo with a fake sibling worktree; covers both carve-outs and the flagged / clean collision cases.
bun.lockRoot Bun lockfile. Checked out in every linked worktree by construction, so a staged root-lockfile fix on main always collides.
Cargo.lockRoot Cargo lockfile. Same structural collision as bun.lock.
package.jsonRoot workspace manifest. Same structural collision as the lockfiles.

The guard gains a third carve-out class alongside docs/planning/tasks/ and the generated-docs roots: an exact-match list of root-level shared dependency files (bun.lock, Cargo.lock, package.json). Because these are present in every worktree by construction, their collision is structural rather than the stray hand-edit the guard exists to catch, so skipping them removes a guaranteed false positive instead of weakening a real check. Matching is exact equality on the project-root-relative path, so a nested manifest such as apps/sdlc/package.json stays inside the guard’s scope. The convention doc records the new carve-out, its exact-match rule, and the residual risk it accepts; WORKTREE_SCOPE_GUARD=skip remains the documented escape hatch for anything not on the list.

  1. In apps/sdlc/lib/services/gate/ops/worktree-scope.ts, add a ROOT_SHARED_FILES constant next to ALLOWED_PREFIXES holding exactly ["bun.lock", "Cargo.lock", "package.json"], with a comment stating the rationale (present in every worktree by construction → the collision is structural, never a stray hand-edit) and the residual risk it accepts.
  2. Add an isCarvedOut(relPath) helper in the same module that returns true when the path starts with an ALLOWED_PREFIXES entry OR equals a ROOT_SHARED_FILES entry, and call it from findCollisions in place of the inline ALLOWED_PREFIXES.some(...) test. Use exact equality (not startsWith) for the root-file arm so nested manifests keep being guarded.
  3. Update the op’s module docstring — the carve-out: bullet under “Behaviour mirrors the script exactly” — to name the third carve-out class.
  4. Extend apps/sdlc/lib/services/gate/tests/worktree_scope.test.ts with two tests, written in the existing write / writeInWorktree / runGuard style: one staging the three root shared files with sibling-worktree copies and asserting exit 0; one staging apps/sdlc/package.json with a sibling-worktree copy and asserting exit 1 with the path on stderr.
  5. Add a ## Carve-outs bullet to apps/sdlc/conventions/worktree-scope-guard.md naming the three carved paths, the exact-match rule, why the collision is structural, and the residual risk; leave the WORKTREE_SCOPE_GUARD=skip bullet in place as the escape hatch for everything else.
  6. Verify by running bun test against apps/sdlc/lib/services/gate/tests/worktree_scope.test.ts, then rehearse the real path: stage a root bun.lock edit on the main checkout while a worktree is active and confirm the pre-commit gate passes.
LocationKindChange
apps/sdlc/lib/services/gate/ops/worktree-scope.tsmodifyAdd ROOT_SHARED_FILES + isCarvedOut; route findCollisions through the helper; refresh the module docstring’s carve-out bullet.
apps/sdlc/lib/services/gate/tests/worktree_scope.test.tsmodifyAdd the root-shared-file carve-out test and the nested-manifest still-flagged test.
apps/sdlc/conventions/worktree-scope-guard.mdmodifyDocument the root-shared-file carve-out, its exact-match rule, and the residual risk under ## Carve-outs.
  • AC-1: With an active worktree present, sdlc gate worktree-scope exits 0 when the staged set is limited to bun.lock, Cargo.lock, and package.json and sibling-worktree copies of those paths exist.
  • AC-2: sdlc gate worktree-scope still exits 1 and prints both the path and the worktree basename on stderr when a non-carved-out path collides with an active worktree.
  • AC-3: A staged apps/sdlc/package.json that collides with a sibling worktree is still reported as a collision — the carve-out matches the project-root-relative path exactly, not by prefix or basename.
  • AC-4: The suite at apps/sdlc/lib/services/gate/tests/worktree_scope.test.ts passes under bun test, and contains one test asserting AC-1 and one asserting AC-3.
  • AC-5: The ## Carve-outs section of apps/sdlc/conventions/worktree-scope-guard.md names the three carved root paths, states the exact-match rule, and states the residual risk that a stray edit to one of those files on main is no longer caught.
  • Carving out other root-level files (Cargo.toml, tsconfig.json, mise.toml, moon.yml, justfile, sdlc.yaml, lefthook.yml). Only the two lockfiles and the root package.json are in scope; the rest keep using WORKTREE_SCOPE_GUARD=skip when a genuine need arises.
  • Promoting the new constant into apps/sdlc/lib/util/ — it has one consumer today, so it stays co-located in the op module until a second caller appears.
  • Making the carve-out list configurable from sdlc.yaml.
  • Changing the guard’s collision algorithm, its worktree discovery, its linked-worktree self-skip, or its exit-code contract.
  • Refreshing the stale .claude/worktrees/ references in the lefthook.yml header comment.
  • none

Spawned by /sdlc:spawn-task-pr on 2026-07-20 UTC from T-1YSW-amend-d0001-solutions-tier in https://github.com/sksizer/dev.


← Back to Tasks