---
name: worktrees-under-project-root
description: "BINDING: all git worktrees go in .gitignore'd .worktrees/ inside spt-core root — never siblings; sibling skeletons get pinned by leaked-daemon conhost cwd"
metadata: 
  node_type: memory
  type: feedback
  originSessionId: 40d8e3ff-276c-4aeb-abc0-8d827bb9da60
  modified: 2026-07-21T14:35:53.876Z
---

Operator directive 2026-07-05: every `git worktree` (gate rigs, anything) goes under `.worktrees/` in the spt-core project root — `.gitignore`d (committed @8a54b2c, also in CLAUDE.md conventions). NEVER as siblings of the project dir.

**Why:** eight `gate-<sha>` sibling folders + `spt-a1gate` + `spt-core-gate-c39` accumulated in `~/Documents/projects` over one wave. Worse than clutter: each full-suite gate leaks ~4 spt.exe e2e daemons from the gate target, and their console children hold `crates/spt-daemon` (nextest's test cwd) as working directory — the skeleton dirs become UNDELETABLE until the holders die. 20 leaked daemons were found still running from five shed gate targets.

**Multi-agent shared-checkout ping-pong (doyle+todlando 2026-07-08, BINDING going-forward):** doyle, todlando, deployah ALL operate ONE physical checkout (`C:\Users\decid\Documents\projects\spt-core`). So any agent's `git merge` / branch-switch / `checkout` PING-PONGS every other agent's working dir + HEAD under them. Concrete bite: deployah's v0.30.0 (PR #63) merge switched the shared dir to `main` mid-session — todlando's next `git commit` (meant for a feature branch) landed on LOCAL main by accident (reconciled: branch it off + `git branch -f main origin/main`, nothing pushed = no pollution). FIX: **any concurrent branch work — especially a BUILD — runs in a per-agent `.worktrees/` worktree off your branch (isolated HEAD), NOT the shared root checkout.** `git worktree add .worktrees/<task>-<branch> <branch>`, work there, remove on done. Design/doc commits on a quick branch are lower-risk but a build MUST be worktree-isolated. This is the collision the leaked-daemon pins never explained — it's HEAD contention, not just cwd pins.

**⚠ RECURRED 2026-07-21 — SAME AGENT (deployah), SAME FAILURE, 13 days later.** Cutting the runbook-fix branch I ran `git reset --hard origin/main` on the shared checkout and switched it away — and the reflog showed it had been sitting on `docs/frames-encoding`, **todlando's LIVE branch, mid-wave**. Nothing was lost (his `d588999` was already == origin; the reset was a no-op since main already == origin/main; tree clean, stash empty) — but that is LUCK, not process: an uncommitted commit there would have been destroyed with no copy anywhere. The rule above already existed, written from MY OWN v0.30.0 bite, and I still did it — so remembering is not the fix.

**MECHANISM (adopt, do not re-derive):** the release lane owns a STANDING worktree — `git worktree add .worktrees/release-lane --detach origin/main` — and every cut (bump, CHANGELOG, gen, tag prep) happens there. Never in the shared root, never "just this once because it is only a docs branch." Cheap, permanent, removes the judgement call entirely. Instance of [[rule-failed-its-author-needs-a-mechanism]].

**OCCUPANCY PROTOCOL (deployah+todlando, symmetric, BINDING 2026-07-21):** a shared checkout sitting on a branch that is not yours is **OCCUPIED until its owner says otherwise** — never inferred stale from the branch name or from the tree looking idle. You cannot distinguish "abandoned" from "live mid-wave" by looking, which is the same single-sample error as runner idle-vs-between-jobs ([[v0392-published]], [[absence-needs-sibling-probe]]). If you must move it: check `git reflog` for whose branch it was, verify their work is on origin BEFORE claiming no harm, and restore the checkout to the branch you found it on rather than leaving it "wherever."

**Run-window corollary (learned the hard way, same day):** the [twohost] window rule includes a PUSH FREEZE — any push to the PR branch moves the PR head and the ci-<ref> cancel-in-progress concurrency group KILLS the live run (my convention push cancelled attempt 3 mid-flight). No pushes to the branch from anyone while a [twohost] run lives; docs commits wait too.

**How to apply:** `git worktree add --detach .worktrees/gate-<sha> <sha>` then gate inside it; on bless, `git worktree remove --force` + `git worktree prune` + kill any spt.exe with ExecutablePath under the worktree BEFORE removal (kill-then-remove, not remove-then-retry — the leaked daemons are why removal fails). If `crates/spt-daemon` still pins after the daemon kill: holders are ORPHANED CONHOSTS (confirmed 2026-07-05, 50 of them) — each dead gate daemon leaves its conhost.exe idle with the inherited cwd. Find: enumerate `conhost.exe` via Win32_Process, keep those whose ParentProcessId is no longer alive, Stop-Process the orphans (safe: dead client = idle host; cross-account/runner conhosts fail the kill harmlessly, so scope is self-limiting even mid-CI-run). Then Remove-Item succeeds. Never kill live-parent conhosts.

**Precise pin-finder (proven 2026-07-06):** instead of guessing among all conhosts, read each process's actual cwd via PEB — P/Invoke `NtQueryInformationProcess` → PEB+0x20 (ProcessParameters) → +0x38 (CurrentDirectory UNICODE_STRING), filter cwd matching the pinned path. Found exactly the 3 pinning conhosts among ~40 on the box; verified parents dead; targeted kill of only those PIDs. Script pattern: scratchpad `find-cwd-pin.ps1` (Add-Type C# + ReadProcessMemory, x64 offsets). Externally-killed bg bash tasks are one source of these orphans — bash dies, its conhost keeps the inherited worktree cwd.
