This session's kickoff was handed docs/handoffs/chicorytv-issue-queue.mdpasted out of /Users/timothy/ersatztv while that tree was 81 commits behind. The copy still instructed the session to read tracker #237 — a protocol #520 retired the day before, and which main now explicitly forbids reading for queue state. Nothing broke only because selection went through scripts/select-queue.sh.
Every existing guard around that tree is written for a session reading git state ("never read its HEAD/git log"). No git command was ever run against it here — the staleness arrived as a file. That is a route no discipline check can cover, and docs are exactly the content a kickoff depends on.
The shared-tree lore bullet already prescribed the shape of the fix for its earlier failure modes: "a design flaw, not a discipline failure — do not fix it by telling sessions to check harder. A check does not stay true." So this removes the stale condition instead of adding another check.
What
scripts/refresh-shared-checkout.sh, wired into the session-end protocol as step 6. It fast-forwards the shared tree to origin/main and reinstalls web/node_modules when the lockfile moved.
Deliberately timid, because the tree is shared and another session may be mid-flight in it. It leaves the working tree untouched — loudly, exit 0 — when the tree is not on main, is dirty (tracked, staged or untracked), has local commits, is detached, is mid-rebase/merge/cherry-pick/revert/bisect, or is not a repo root. It never switches branches, never stashes, never discards. A NO-OP is a normal outcome.
Verification
Executed against 7 scratch-repo states (never against the real tree): already-current, dirty, wrong-branch, detached HEAD, untracked-only, local-commits-ahead, missing dir, wrong-repo, plus the happy path and an idempotent re-run. Confirmed clean-at-exit each time.
Two failure modes were caught by testing rather than by reading:
The first version used npm install, which rewrote package-lock.json and left the shared tree dirty — precisely the state the next run refuses on, so the tool would have disabled itself after a single use. Now npm ci, which installs strictly from the lockfile and never writes it.
Cold review reproduced the worst case: [ -d "$shared/.git" ] passing on an invalid.git directory nested inside an enclosing repo, where git walks up and every command operates on the ancestor — a script that fast-forwards and reinstalls, pointed at the wrong repo. Now asserts git rev-parse --show-toplevel equals the resolved cwd; re-tested, the outer repo is untouched.
Independent cold adversarial review: MERGEABLE, no Blocker/High. Its remaining findings are folded in (the REVERT_HEAD state, set -e future-proofing on the &&-tail loop, an unmasked rev-list failure, and an honesty fix — a refusal after the fetch does advance remote-tracking refs, so "changes nothing" was a slight overclaim). Two accepted trade-offs are documented in the script: npm ci deletes node_modules before reinstalling (bounded — everything removed is regenerable, and it only runs when a merge actually moved the lockfile), and gitignored-path collisions are git-inherent.
Docs
New decision record session.shared-checkout-refresh; catalog regenerated; validator OK. The handoff-doc change is 8 lines of instruction — deliberately not the incident narrative, which lives in the decision record. That restraint is itself the subject of #542, filed this session: an inventory found ~30 of ~38 lore bullets have no decision record at all, so that doc is currently the only copy of much of our operating knowledge.
fixes #541
## Why
This session's kickoff was handed `docs/handoffs/chicorytv-issue-queue.md` **pasted out of `/Users/timothy/ersatztv` while that tree was 81 commits behind**. The copy still instructed the session to read tracker #237 — a protocol #520 retired the day before, and which `main` now explicitly forbids reading for queue state. Nothing broke only because selection went through `scripts/select-queue.sh`.
Every existing guard around that tree is written for a session **reading git state** ("never read its HEAD/`git log`"). No `git` command was ever run against it here — the staleness arrived as a **file**. That is a route no discipline check can cover, and docs are exactly the content a kickoff depends on.
The shared-tree lore bullet already prescribed the shape of the fix for its earlier failure modes: *"a design flaw, not a discipline failure — do not fix it by telling sessions to check harder. A check does not stay true."* So this removes the stale condition instead of adding another check.
## What
`scripts/refresh-shared-checkout.sh`, wired into the session-end protocol as step 6. It fast-forwards the shared tree to `origin/main` and reinstalls `web/node_modules` when the lockfile moved.
Deliberately timid, because the tree is shared and another session may be mid-flight in it. It leaves the working tree untouched — loudly, exit 0 — when the tree is not on `main`, is dirty (tracked, staged **or** untracked), has local commits, is detached, is mid-rebase/merge/cherry-pick/revert/bisect, or is not a repo root. It never switches branches, never stashes, never discards. A NO-OP is a normal outcome.
## Verification
Executed against 7 scratch-repo states (never against the real tree): already-current, dirty, wrong-branch, detached HEAD, untracked-only, local-commits-ahead, missing dir, wrong-repo, plus the happy path and an idempotent re-run. Confirmed clean-at-exit each time.
**Two failure modes were caught by testing rather than by reading:**
1. The first version used `npm install`, which **rewrote `package-lock.json`** and left the shared tree dirty — precisely the state the next run refuses on, so the tool would have disabled itself after a single use. Now `npm ci`, which installs strictly from the lockfile and never writes it.
2. Cold review reproduced the worst case: `[ -d "$shared/.git" ]` passing on an *invalid* `.git` directory nested inside an enclosing repo, where git walks **up** and every command operates on the ancestor — a script that fast-forwards and reinstalls, pointed at the wrong repo. Now asserts `git rev-parse --show-toplevel` equals the resolved cwd; re-tested, the outer repo is untouched.
Independent cold adversarial review: **MERGEABLE**, no Blocker/High. Its remaining findings are folded in (the `REVERT_HEAD` state, `set -e` future-proofing on the `&&`-tail loop, an unmasked rev-list failure, and an honesty fix — a refusal *after* the fetch does advance remote-tracking refs, so "changes nothing" was a slight overclaim). Two accepted trade-offs are documented in the script: `npm ci` deletes `node_modules` before reinstalling (bounded — everything removed is regenerable, and it only runs when a merge actually moved the lockfile), and gitignored-path collisions are git-inherent.
## Docs
New decision record `session.shared-checkout-refresh`; catalog regenerated; validator OK. The handoff-doc change is 8 lines of instruction — deliberately **not** the incident narrative, which lives in the decision record. That restraint is itself the subject of **#542**, filed this session: an inventory found ~30 of ~38 lore bullets have no decision record at all, so that doc is currently the only copy of much of our operating knowledge.
Independent cold adversarial review of the full diff — verdict MERGEABLE, no Blocker/High.
It enumerated every state the shared tree could be in and executed the script against 18 scratch repos (never the real tree), confirming a correct refusal for: detached HEAD, dirty tracked / staged-only / untracked-only, local commits ahead or diverged, mid-merge, mid-bisect, mid-rebase, unborn branch, linked worktree, missing dir, permission denied, and fetch failure. It also verified that cd "$shared" || skip exits, so no later command can run in the script's own repo, and that every git call runs in $shared with npm ci confined to $shared/web.
The finding that mattered — reproduced, not theorised: [ -d "$shared/.git" ] passes on an invalid.git directory, and if any ancestor is a repo, git rev-parse walks up, so a script that fast-forwards and reinstalls would silently operate on the wrong repository. Fixed by asserting git rev-parse --show-toplevel equals the resolved cwd; re-tested, the outer repo is untouched. Also folded in: REVERT_HEAD added to the in-progress states, || true so the &&-tail loop survives a future set -e, an unmasked rev-list failure that previously reported "already current", and an honesty fix — a refusal reached after the fetch does advance remote-tracking refs, so "changes nothing" became "leaves the working tree untouched".
Accepted and documented rather than fixed: npm ci deletes node_modules before reinstalling, so a session copying it at that instant gets a partial copy (bounded — everything removed is regenerable from the lockfile, no human work can be lost, and it only runs when a merge actually moved the lockfile); and gitignored-path collisions during a fast-forward are git-inherent.
CI green on all jobs (image push correctly skipped for a PR).
Independent cold adversarial review of the full diff — verdict **MERGEABLE**, no Blocker/High.
It enumerated every state the shared tree could be in and executed the script against 18 scratch repos (never the real tree), confirming a correct refusal for: detached HEAD, dirty tracked / staged-only / untracked-only, local commits ahead or diverged, mid-merge, mid-bisect, mid-rebase, unborn branch, linked worktree, missing dir, permission denied, and fetch failure. It also verified that `cd "$shared" || skip` exits, so no later command can run in the script's own repo, and that every git call runs in `$shared` with `npm ci` confined to `$shared/web`.
**The finding that mattered** — reproduced, not theorised: `[ -d "$shared/.git" ]` passes on an *invalid* `.git` directory, and if any ancestor is a repo, `git rev-parse` walks **up**, so a script that fast-forwards and reinstalls would silently operate on the wrong repository. Fixed by asserting `git rev-parse --show-toplevel` equals the resolved cwd; re-tested, the outer repo is untouched. Also folded in: `REVERT_HEAD` added to the in-progress states, `|| true` so the `&&`-tail loop survives a future `set -e`, an unmasked rev-list failure that previously reported "already current", and an honesty fix — a refusal reached *after* the fetch does advance remote-tracking refs, so "changes nothing" became "leaves the working tree untouched".
Accepted and documented rather than fixed: `npm ci` deletes `node_modules` before reinstalling, so a session copying it at that instant gets a partial copy (bounded — everything removed is regenerable from the lockfile, no human work can be lost, and it only runs when a merge actually moved the lockfile); and gitignored-path collisions during a fast-forward are git-inherent.
CI green on all jobs (image push correctly skipped for a PR).
Review-verdict: MERGEABLE @ 5ebf1715741385ec9d2145fc00dc0f7dddf9988d
Rebased onto origin/main (d9eb307d) after #540 landed — not merged in, per H11. The only conflict was the usual append-vs-append at the tail of docs/decisions.md: #540's scheduling.ondemand-guide-refresh-on-thaw and this PR's session.shared-checkout-refresh both append. Resolved by keeping both, theirs first; docs/decisions/README.md was regenerated via build_decisions_catalog.py, never hand-merged. Validator OK, and git diff origin/main...HEAD is exactly this PR's four files (151 insertions, 0 deletions) — nothing of #540's carried in.
The content is byte-identical to the reviewed head; only the base moved. The MERGEABLE verdict above therefore still stands, re-stated against the new head sha so the gate reads a current verdict rather than a stale one.
Rebased onto `origin/main` (`d9eb307d`) after #540 landed — not merged in, per H11. The only conflict was the usual append-vs-append at the tail of `docs/decisions.md`: #540's `scheduling.ondemand-guide-refresh-on-thaw` and this PR's `session.shared-checkout-refresh` both append. Resolved by keeping **both**, theirs first; `docs/decisions/README.md` was **regenerated** via `build_decisions_catalog.py`, never hand-merged. Validator OK, and `git diff origin/main...HEAD` is exactly this PR's four files (151 insertions, 0 deletions) — nothing of #540's carried in.
The content is byte-identical to the reviewed head; only the base moved. The MERGEABLE verdict above therefore still stands, re-stated against the new head sha so the gate reads a current verdict rather than a stale one.
Review-verdict: MERGEABLE @ 92bf8b083d85cd90b4928d1b790d1162ff1fa2a0
timothy
scheduled this pull request to auto merge when all checks succeed 2026-07-21 20:03:47 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
fixes #541
Why
This session's kickoff was handed
docs/handoffs/chicorytv-issue-queue.mdpasted out of/Users/timothy/ersatztvwhile that tree was 81 commits behind. The copy still instructed the session to read tracker #237 — a protocol #520 retired the day before, and whichmainnow explicitly forbids reading for queue state. Nothing broke only because selection went throughscripts/select-queue.sh.Every existing guard around that tree is written for a session reading git state ("never read its HEAD/
git log"). Nogitcommand was ever run against it here — the staleness arrived as a file. That is a route no discipline check can cover, and docs are exactly the content a kickoff depends on.The shared-tree lore bullet already prescribed the shape of the fix for its earlier failure modes: "a design flaw, not a discipline failure — do not fix it by telling sessions to check harder. A check does not stay true." So this removes the stale condition instead of adding another check.
What
scripts/refresh-shared-checkout.sh, wired into the session-end protocol as step 6. It fast-forwards the shared tree toorigin/mainand reinstallsweb/node_moduleswhen the lockfile moved.Deliberately timid, because the tree is shared and another session may be mid-flight in it. It leaves the working tree untouched — loudly, exit 0 — when the tree is not on
main, is dirty (tracked, staged or untracked), has local commits, is detached, is mid-rebase/merge/cherry-pick/revert/bisect, or is not a repo root. It never switches branches, never stashes, never discards. A NO-OP is a normal outcome.Verification
Executed against 7 scratch-repo states (never against the real tree): already-current, dirty, wrong-branch, detached HEAD, untracked-only, local-commits-ahead, missing dir, wrong-repo, plus the happy path and an idempotent re-run. Confirmed clean-at-exit each time.
Two failure modes were caught by testing rather than by reading:
npm install, which rewrotepackage-lock.jsonand left the shared tree dirty — precisely the state the next run refuses on, so the tool would have disabled itself after a single use. Nownpm ci, which installs strictly from the lockfile and never writes it.[ -d "$shared/.git" ]passing on an invalid.gitdirectory nested inside an enclosing repo, where git walks up and every command operates on the ancestor — a script that fast-forwards and reinstalls, pointed at the wrong repo. Now assertsgit rev-parse --show-toplevelequals the resolved cwd; re-tested, the outer repo is untouched.Independent cold adversarial review: MERGEABLE, no Blocker/High. Its remaining findings are folded in (the
REVERT_HEADstate,set -efuture-proofing on the&&-tail loop, an unmasked rev-list failure, and an honesty fix — a refusal after the fetch does advance remote-tracking refs, so "changes nothing" was a slight overclaim). Two accepted trade-offs are documented in the script:npm cideletesnode_modulesbefore reinstalling (bounded — everything removed is regenerable, and it only runs when a merge actually moved the lockfile), and gitignored-path collisions are git-inherent.Docs
New decision record
session.shared-checkout-refresh; catalog regenerated; validator OK. The handoff-doc change is 8 lines of instruction — deliberately not the incident narrative, which lives in the decision record. That restraint is itself the subject of #542, filed this session: an inventory found ~30 of ~38 lore bullets have no decision record at all, so that doc is currently the only copy of much of our operating knowledge.Independent cold adversarial review of the full diff — verdict MERGEABLE, no Blocker/High.
It enumerated every state the shared tree could be in and executed the script against 18 scratch repos (never the real tree), confirming a correct refusal for: detached HEAD, dirty tracked / staged-only / untracked-only, local commits ahead or diverged, mid-merge, mid-bisect, mid-rebase, unborn branch, linked worktree, missing dir, permission denied, and fetch failure. It also verified that
cd "$shared" || skipexits, so no later command can run in the script's own repo, and that every git call runs in$sharedwithnpm ciconfined to$shared/web.The finding that mattered — reproduced, not theorised:
[ -d "$shared/.git" ]passes on an invalid.gitdirectory, and if any ancestor is a repo,git rev-parsewalks up, so a script that fast-forwards and reinstalls would silently operate on the wrong repository. Fixed by assertinggit rev-parse --show-toplevelequals the resolved cwd; re-tested, the outer repo is untouched. Also folded in:REVERT_HEADadded to the in-progress states,|| trueso the&&-tail loop survives a futureset -e, an unmasked rev-list failure that previously reported "already current", and an honesty fix — a refusal reached after the fetch does advance remote-tracking refs, so "changes nothing" became "leaves the working tree untouched".Accepted and documented rather than fixed:
npm cideletesnode_modulesbefore reinstalling, so a session copying it at that instant gets a partial copy (bounded — everything removed is regenerable from the lockfile, no human work can be lost, and it only runs when a merge actually moved the lockfile); and gitignored-path collisions during a fast-forward are git-inherent.CI green on all jobs (image push correctly skipped for a PR).
Review-verdict: MERGEABLE @ 5ebf1715741385ec9d2145fc00dc0f7dddf9988d
5ebf171574to92bf8b083dRebased onto
origin/main(d9eb307d) after #540 landed — not merged in, per H11. The only conflict was the usual append-vs-append at the tail ofdocs/decisions.md: #540'sscheduling.ondemand-guide-refresh-on-thawand this PR'ssession.shared-checkout-refreshboth append. Resolved by keeping both, theirs first;docs/decisions/README.mdwas regenerated viabuild_decisions_catalog.py, never hand-merged. Validator OK, andgit diff origin/main...HEADis exactly this PR's four files (151 insertions, 0 deletions) — nothing of #540's carried in.The content is byte-identical to the reviewed head; only the base moved. The MERGEABLE verdict above therefore still stands, re-stated against the new head sha so the gate reads a current verdict rather than a stale one.
Review-verdict: MERGEABLE @
92bf8b083d