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.
| Location | Role today |
|---|---|
apps/sdlc/lib/services/gate/ops/worktree-scope.ts#ALLOWED_PREFIXES | The 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#findCollisions | Walks 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_ROOTS | The 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.md | The 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.ts | Suite 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.lock | Root Bun lockfile. Checked out in every linked worktree by construction, so a staged root-lockfile fix on main always collides. |
Cargo.lock | Root Cargo lockfile. Same structural collision as bun.lock. |
package.json | Root workspace manifest. Same structural collision as the lockfiles. |
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”- In
apps/sdlc/lib/services/gate/ops/worktree-scope.ts, add aROOT_SHARED_FILESconstant next toALLOWED_PREFIXESholding 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. - Add an
isCarvedOut(relPath)helper in the same module that returns true when the path starts with anALLOWED_PREFIXESentry OR equals aROOT_SHARED_FILESentry, and call it fromfindCollisionsin place of the inlineALLOWED_PREFIXES.some(...)test. Use exact equality (notstartsWith) for the root-file arm so nested manifests keep being guarded. - Update the op’s module docstring — the
carve-out:bullet under “Behaviour mirrors the script exactly” — to name the third carve-out class. - Extend
apps/sdlc/lib/services/gate/tests/worktree_scope.test.tswith two tests, written in the existingwrite/writeInWorktree/runGuardstyle: one staging the three root shared files with sibling-worktree copies and asserting exit 0; one stagingapps/sdlc/package.jsonwith a sibling-worktree copy and asserting exit 1 with the path on stderr. - Add a
## Carve-outsbullet toapps/sdlc/conventions/worktree-scope-guard.mdnaming the three carved paths, the exact-match rule, why the collision is structural, and the residual risk; leave theWORKTREE_SCOPE_GUARD=skipbullet in place as the escape hatch for everything else. - Verify by running
bun testagainstapps/sdlc/lib/services/gate/tests/worktree_scope.test.ts, then rehearse the real path: stage a rootbun.lockedit on the main checkout while a worktree is active and confirm the pre-commit gate passes.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
apps/sdlc/lib/services/gate/ops/worktree-scope.ts | modify | Add 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.ts | modify | Add the root-shared-file carve-out test and the nested-manifest still-flagged test. |
apps/sdlc/conventions/worktree-scope-guard.md | modify | Document the root-shared-file carve-out, its exact-match rule, and the residual risk under ## Carve-outs. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: With an active worktree present,
sdlc gate worktree-scopeexits 0 when the staged set is limited tobun.lock,Cargo.lock, andpackage.jsonand sibling-worktree copies of those paths exist. - AC-2:
sdlc gate worktree-scopestill 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.jsonthat 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.tspasses underbun test, and contains one test asserting AC-1 and one asserting AC-3. - AC-5: The
## Carve-outssection ofapps/sdlc/conventions/worktree-scope-guard.mdnames 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.
Out of scope
Section titled “Out of scope”- 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 rootpackage.jsonare in scope; the rest keep usingWORKTREE_SCOPE_GUARD=skipwhen 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 thelefthook.ymlheader comment.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-07-20 UTC from T-1YSW-amend-d0001-solutions-tier in https://github.com/sksizer/dev.