Skip to content

Commit b72bcd9

Browse files
committed
Merge remote-tracking branch 'origin/main' into zizmor-1280-adoption
2 parents 3787419 + f7e1269 commit b72bcd9

14 files changed

Lines changed: 4131 additions & 256 deletions

‎.gitignore‎

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,22 +87,16 @@ canary-*.json
8787
# Local reference notes pointing at machine-specific Claude Code transcript paths — never commit
8888
TRANSCRIPTS.md
8989

90-
# Local planning/marketing/security working docs — keep out of the repo
90+
# Local planning/marketing working docs — keep out of the repo
9191
# (docs/DUAL_LICENSING_PLAN.md is now a committed artifact — see ADR 0017 / #13 licensing)
9292
/docs/marketing/
93-
/docs/security/CISO-REVIEW.md
94-
/docs/security/DEPENDENCY-POSTURE-REVIEW.md
95-
/docs/security/DEPENDENCY-RESPONSE-PLAN.md
96-
/docs/security/DEPENDENCY-CUSTOMER-OFFERINGS.md
97-
/docs/security/DEPENDENCY-RESPONSE-HANDOFF.md
98-
/docs/security/DEPENDENCY-RESPONSE-A3-HANDOFF.md
99-
/docs/security/DEPENDENCY-RESPONSE-A4-HANDOFF.md
100-
# vuln_metrics.py local CSV output (the CI run uploads it as an artifact; not committed)
101-
/docs/security/metrics/
93+
# Individual docs/security/* working files (and vuln_metrics.py's local CSV output) were once
94+
# enumerated here. They are all covered by the blanket /docs/security/ rule further down, so the
95+
# list was redundant — and enumerating private filenames in a public file discloses the shape of
96+
# the private set for no gain. Do not re-add per-file entries; extend the blanket rule instead.
10297

103-
# Operator-local load/throughput profiles — tuned to a specific deployment's volume; kept OUT of the
104-
# OSS repo + public mirror (the plan itself lives on the operator share, WIN2025-LOAD-THROUGHPUT-MATRIX.md).
105-
# Still usable by name (they sit in PROFILES_DIR) and copy-on to the test box.
98+
# Operator-local load/throughput profiles — sized for one operator's own environment, not a
99+
# shipped artifact. Still usable by name (they sit in PROFILES_DIR).
106100
/harness/load/profiles/hospital-baseline.toml
107101
/harness/load/profiles/soak-12h.toml
108102
# Deep-research survey of alternative OSS DB backends for the shared-DB commit-wall — local working
@@ -150,9 +144,11 @@ scripts/security/scan-tokens.local.txt
150144
/docs/security/
151145
/docs/reviews/
152146
/docs/marketing/
153-
# Deliberately still private, each for a different reason:
154-
# docs/security/ — 32 files of posture/risk-register detail; an attacker roadmap.
155-
# docs/reviews/ — review findings, same reason.
147+
# Deliberately still private, each for a different reason. What decides which side a security
148+
# document lands on is stated publicly in docs/SECURITY-DOCS-POLICY.md — do not restate the
149+
# reasoning, the inventory or the file count here:
150+
# docs/security/ — maintainer-internal working documents; see the policy above.
151+
# docs/reviews/ — point-in-time review findings, withheld on the same test.
156152
# docs/CI-TOPOLOGY.md — STALE: it documents the retired private-repo/public-mirror split and
157153
# scripts/publish/, which the cutover deleted. Publishing it would actively
158154
# mislead. Rewrite for the post-cutover topology or delete it.

‎docs/BACKLOG.md‎

Lines changed: 912 additions & 3 deletions
Large diffs are not rendered by default.

‎docs/SESSION-DRIFT-CONTROLS.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,23 @@ and `prune-merged.ps1` are sibling-only, so the nested population — where ever
307307
— has creation but no scripted teardown, and `prune-merged.ps1` run from a worktree prints a green
308308
"No sibling worktrees to consider" and exits 0. A wrong-cwd run reports a clean bill of health.
309309
310+
> **Half fixed, 2026-07-30.** `prune-merged.ps1` now **refuses** from anywhere but the primary (exit 2,
311+
> naming both paths) instead of reporting a green no-op, and it refuses to remove any sibling that
312+
> *contains* a nested worktree — a nested checkout is gitignored inside its parent, so the parent reads
313+
> perfectly clean and `--force` used to delete both, leaving the nested one registered with no
314+
> directory. A session in a nested tree now vetoes its ancestor too. Still true: the candidate set is
315+
> sibling-only, so the nested population has no scripted teardown.
316+
>
317+
> **Correction, same day.** "The candidate set is sibling-only" was *stated* but not *true*. The set was
318+
> a bare `<primary>-` **prefix match**, and `<primary>-pins/.claude/worktrees/x` starts with
319+
> `<primary>-` — so a Claude-managed nested worktree under a **sibling** was a candidate in its own
320+
> right and was removed, with its branch, while the containment veto above protected only its parent.
321+
> Nested trees under the **primary** were excluded by the accident that `<primary>/` is not
322+
> `<primary>-`, which is the only case anyone had tested. Fixed: anything nested inside another
323+
> registered worktree, or carrying a `.claude/worktrees/` path segment, is excluded from the candidate
324+
> set and unreachable by `-Name`. The teardown gap itself is unchanged — nested worktrees still have no
325+
> scripted removal, they are now merely safe from this script.
326+
310327
### G12 — The gate has never produced a receipt — **FIXED**
311328
312329
`Write-Deny` writes JSON to stdout and exits 0. There is no log, no counter, no audit file. Nothing can
@@ -449,7 +466,7 @@ analysis must be redone.
449466
| B7 | **Half done** | Rule 4 is opt-in, not retired — preserving the owner's decision while removing the trap where re-installing would activate it. |
450467
| B10 | **Done** | One allowlist, shared by the gate and the backstop, with the legacy path kept as a fallback so a version-skewed installed copy cannot silently disarm the backstop. `install-selfheal.ps1` gained the `CLAUDECODE` refusal its sibling always had — the *higher*-privilege installer was the unprotected one. |
451468
| B6 | **Started** | `.worktreeinclude` added, so the leak gate's gitignored token list reaches every worktree Claude Code creates itself (`--worktree`, desktop sessions, `isolation: worktree` subagents) — previously only `new.ps1`'s own worktrees got it, and a fresh first-party worktree could not commit at all. **Not** done: worktree-first as the documented default entry point, and `new.ps1` calling `git worktree lock` while a session is live. |
452-
| B11 | **Started** | Rule 3d closes the worst of G9: `git worktree remove` / `move` on another session's checkout. **Not** done: the rest of the absent verbs (`rm`, `mv`, `sparse-checkout`, `checkout-index`, `bisect`, `branch -f`, `update-ref`, `read-tree`), `gh pr checkout`, and G11's teardown half — `prune-merged.ps1` is still sibling-only and still prints a green "nothing to consider" when run from the wrong cwd. |
469+
| B11 | **Started** | Rule 3d closes the worst of G9: `git worktree remove` / `move` on another session's checkout. **Not** done: the rest of the absent verbs (`rm`, `mv`, `sparse-checkout`, `checkout-index`, `bisect`, `branch -f`, `update-ref`, `read-tree`), `gh pr checkout`, and G11's teardown half — `prune-merged.ps1` is still sibling-only. Its wrong-cwd green no-op is **fixed** (2026-07-30): it refuses with exit 2 and names both paths, and it will not `--force`-remove a sibling containing a nested worktree. |
453470
454471
Remaining order: the rest of B6, the rest of B11, then the B1/B3/B4 remainders.
455472

‎docs/WORKTREES.md‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,127 @@ scripts\worktree\remove.ps1 -Name alerts -Force # discard uncommitted tracke
5555
The untracked `.venv` / `node_modules` are expected and removed automatically; only uncommitted
5656
**tracked** changes block removal (unless `-Force`).
5757

58+
## Prune the finished ones — `prune-merged.ps1`
59+
60+
Worktrees pile up. [`prune-merged.ps1`](../scripts/worktree/prune-merged.ps1) sweeps the finished
61+
`<repo>-<name>` siblings. Run it from the **primary** (it refuses loudly anywhere else):
62+
63+
```powershell
64+
scripts\worktree\prune-merged.ps1 # dry run: the decision table, no action
65+
scripts\worktree\prune-merged.ps1 -Apply # remove the ones that pass every check
66+
scripts\worktree\prune-merged.ps1 -Apply -Name pins # also confirm that one past the activity veto
67+
scripts\worktree\prune-merged.ps1 -Json # machine-readable decisions + the fence receipt
68+
```
69+
70+
### The rule is `merged AND clean AND NOT occupied`
71+
72+
**Clean + merged does not mean unoccupied.** A brand-new worktree has zero commits, so it is an
73+
ancestor of `origin/main` and perfectly clean from the second it is created — and a session can be
74+
sitting in it with nothing uncommitted. That is exactly the state that got destroyed once: `git
75+
worktree remove --force` deleted the `.git` pointer and deregistered the tree, then failed to delete
76+
the directory, leaving a folder git no longer recognised, so every subsequent git command in that
77+
session failed. The bias is therefore fixed: **a false SKIP is a minor annoyance, a false PRUNE
78+
destroys a session.** Anything the script cannot answer confidently, it SKIPs.
79+
80+
Occupancy is checked by two independent signals, and **either one vetoes**:
81+
82+
1. **The liveness fence** — [`scripts/coord/occupancy.ps1`](../scripts/coord/occupancy.ps1), the same
83+
matcher `presence.ps1` uses. It maps each registered session's cwd onto a worktree and fences it on
84+
pid + process start time. A session in a **nested** worktree vetoes its ancestor too.
85+
2. **Recent activity** (`-IdleHours`, default **36**) — the newest mtime of the worktree's *private*
86+
git metadata (`index`, `HEAD`, `logs/HEAD`, …), not the working files. This is the signal that does
87+
**not** depend on a recorded cwd.
88+
89+
Both are re-read **immediately before each removal**, not just when the table was built — a gh round
90+
trip per candidate plus every prior removal is a real window, and it is the window the incident
91+
description blames.
92+
93+
**Liveness may only ever VETO, never PERMIT.** There is no heartbeat anywhere on this host, so nothing
94+
can *prove* a session is gone — a `DEAD`/`STALE`/absent verdict is the absence of a veto, not a
95+
permission. And **if the fence cannot look at all, nothing is pruned**: an empty roster and an
96+
unreadable one produce the same empty answer, so availability is asserted explicitly — at least one
97+
config root with a registry, at least one readable record, **and no record that cannot be placed**.
98+
That last one matters more than it sounds. Two shapes qualify — a file that will not parse, and one
99+
that parses but carries no `cwd` — and both used to be dropped by a silent `continue`, appearing in no
100+
count at all. Neither can be placed in *or* cleared from any candidate, and a file caught
101+
*half-written* is exactly what a session that launched a second ago looks like. An unavailable fence
102+
turns every candidate into a SKIP and exits **2**. There is deliberately no override flag.
103+
104+
### The candidate set is siblings only — and "sibling" is not a prefix match
105+
106+
`<primary>-<name>` used to be matched by prefix alone, which silently includes
107+
`<primary>-pins/.claude/worktrees/x` — a **Claude-managed nested worktree**, the exact place
108+
`EnterWorktree` relocates a live session into. Nested trees under the *primary* escaped only by the
109+
accident that `<primary>/` is not `<primary>-`, so the case that was tested was the case that worked.
110+
Anything living inside another registered worktree, and anything with a `.claude/worktrees/` path
111+
segment, is now excluded outright and listed as a non-candidate; `-Name` cannot reach them either.
112+
113+
### Why signal 2 is not a nicety
114+
115+
Signal 1 only sees where a session was **launched**. Measured on this repo: 29% of writes come from a
116+
session sitting in the primary and landing in a sibling by absolute path — and on 2026-07-30, with 5
117+
live sessions across 9 worktrees, signal 1 vetoed **none** of the four `<primary>-<slug>` siblings,
118+
including one a session was demonstrably building in. Signal 2 was the only thing standing between
119+
that session and this script. The run therefore prints how many candidates signal 1 actually vetoed,
120+
rather than letting "the fence ran" imply "the fence covered it".
121+
122+
What neither signal sees, printed on every run: a cwd recorded as a UNC or 8.3 short path; a session
123+
that never registered; and a session that only edits files and runs no git command (invisible to
124+
signal 2 as well, since it touches none of the metadata files).
125+
126+
Everything that **narrows** either signal is named in red on the run and in the JSON receipt, because
127+
an operator who believes they are fenced when they are not is worse off than one who knows they
128+
aren't: `-IdleHours 0`; any `-IdleHours` **below the 12h floor** (an occupied worktree has been
129+
measured at 10.4h idle, so `-IdleHours 0.5` typed for "half an hour" disarms signal 2 completely); an
130+
explicit `-ConfigRoot`; a **failed fetch** (merge decisions then rest on stale refs); a **gh PR probe
131+
that errored**; and every **`-Name`-confirmed worktree**. `-Name` is worth spelling out — it is
132+
`-IdleHours 0` scoped to one tree, and since signal 1 has been measured vetoing 0 of 4 real siblings,
133+
`-Apply -Name <slug>` can leave a candidate with no working occupancy signal at all. A negative
134+
`-IdleHours` is refused outright, because it would put the cut-off in the future and disarm signal 2
135+
while appearing to set it.
136+
137+
### Outcomes, not intentions
138+
139+
The summary counts what actually **happened** — `Done. removed N, failed N, skipped N` — not what the
140+
script intended (it used to print the count of candidates it planned to remove, which over-reported
141+
after a failed removal). A removal is only counted once the directory is verified gone *and*
142+
deregistered. A failed removal is diagnosed on the spot: git deregisters a worktree even when it
143+
cannot finish deleting the files, so the script reports whether the directory, its `.git` pointer, and
144+
its registration survived, and prints the recovery recipe for the **orphaned** case — move the
145+
directory aside, then `git worktree add` it back (neither `worktree repair` nor `worktree add --force`
146+
recovers it on its own). `git worktree prune` is never run: it deregisters *any* worktree whose
147+
directory is momentarily missing, including the `.claude/worktrees` ones, and it would finish off the
148+
destruction a failed removal left half done.
149+
150+
**An orphan outlives the run that made it, so it is remembered.** Once git has deregistered a worktree
151+
it is no longer in `git worktree list`, so it drops out of the candidate set and the *next* run used to
152+
print a green all-clear over a directory this script had broken. Orphans are now recorded in
153+
`<git-common-dir>/prune-merged-orphans.json` and re-reported with the recipe on every later run until
154+
the directory is gone or re-registered — as is any unregistered `<repo>-*` directory whose `.git`
155+
pointer still names this repo.
156+
157+
Exit codes, **highest severity wins**: `0` nothing wrong; `1` something was attempted and failed
158+
without destroying anything; `2` **refused** — nothing was attempted because safety could not be
159+
established (wrong cwd, unavailable fence, a `-Name` that matched nothing); `3` **orphaned** — a
160+
directory is broken on disk right now. `3` outranks `2` because damage on disk outranks a refusal to
161+
act. In the JSON receipt `counts.orphaned` is a *subset* of `counts.failed` (`failedNonOrphan` is
162+
spelled out alongside it); `removed + failed + skipped` covers every candidate exactly once.
163+
164+
**A branch is never force-deleted on a stale verdict.** `git branch -d` refuses a branch merged only
165+
into `origin/main` whenever the local `main` lags — which it usually does — so `-D` used to be the
166+
routine path and git's last protection was overridden every time. Now `-d` is tried first, and `-D`
167+
only after re-verifying *at that moment* that `origin/main..<branch>` is empty. Otherwise the branch is
168+
**kept** and reported. A stale ref costs nothing; a destroyed commit costs a session.
169+
170+
Tests: [`tests/test_worktree_prune_merged.py`](../tests/test_worktree_prune_merged.py) drives the real
171+
script against a synthetic repo family. Tests assert the *decision and the reason* rather than survival
172+
— a script that has lost its primary fence still leaves the directory intact, because the `-Apply`
173+
re-check catches it, so a survival-only assertion proves nothing — and carry a positive control in the
174+
same invocation wherever one is possible (a refusal test refuses the whole run by design). The
175+
`-Apply` re-check itself is driven by a **gh shim on PATH** whose merge probe performs a side effect —
176+
a session arrives, the fence dies, the metadata is touched — before answering, which reproduces the
177+
race deterministically with no threads and no sleeps.
178+
58179
## Your PR won't merge — triage before you touch anything
59180

60181
With several sessions merging into one `main`, a PR that was green ten minutes ago routinely stops

‎docs/releases/BACKLOG-MULTISESSION-PLAN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ The three lanes are **file-disjoint by construction**. `ledger` owns `docs/BACKL
138138

139139
**Build steps**
140140

141-
1. **Write every banner fresh from the code you verify yourself. Never paste vault prose.** Measured: `vault/main:docs/BACKLOG.md` fails `scripts/security/scan_forbidden.py` with **102 hits** (customer/vendor tokens ×98, an estate token, a site-code pattern, two worktree slugs). Hits are concentrated in the vault's `#232-#307` range **but are not confined to it** — lines 594, 595, 5815, 5824, 6698 are inside the `#1-#231` range, and 5815/5824 sit inside `## 231.` itself, the very item this lane must correct. `docs/BACKLOG.md` is **not** in the leak gate's `docs/security/*` allowlist.
141+
1. **Write every banner fresh from the code you verify yourself. Never paste vault prose.** The vault's copy of `docs/BACKLOG.md` fails `scripts/security/scan_forbidden.py`, and the hits are **not** confined to the high-numbered range you would expect — several sit inside the `#1-#231` range, including inside `## 231.` itself, the very item this lane must correct. Assume any vault line may carry a token; the gate, not a memorised range, is the authority. `docs/BACKLOG.md` is **not** in the leak gate's `docs/security/*` allowlist.
142142
2. **Edit only the leading `> <glyph>` banner block** of each `## N.` section. Leave the prose below it untouched.
143143
3. **Banner invariant (`scripts/docs/backlog_status_check.py`).** The banner block runs from the heading through every blank-or-`>` line and terminates at the first non-blockquote line. Exactly one such block per item; a CLOSED glyph (`✅ ⛔ 🪦`) must **never** coexist with an OPEN one (`🔢 🚧`); two OPENs are legal. Several items carry two banner lines (#117 has `🛠`+`🔢`, #48 has `🔢`+`🔶`) — remove the stale OPEN one; `🛠`/`🔶` are not status glyphs and may stay. Re-run the checker after **every** commit.
144144
4. **Every banner cites `path:line` or an ADR id.** The checker is structural — its own docstring says *"it cannot know whether a banner is truthful, only that a claim exists and does not contradict itself."* `origin/main` passes it **today**, with all 30 stale banners in place. A fabricated citation also passes. See the DoD.

0 commit comments

Comments
 (0)