chore(541): fast-forward the shared checkout at session end (H13) #543

Merged
timothy merged 1 commits from chore/shared-checkout-refresh into main 2026-07-21 20:23:18 +02:00
Owner

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.

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.
Author
Owner

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

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
timothy force-pushed chore/shared-checkout-refresh from 5ebf171574 to 92bf8b083d 2026-07-21 20:03:15 +02:00 Compare
Author
Owner

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 @ 92bf8b083d

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
timothy merged commit dc130f2526 into main 2026-07-21 20:23:18 +02:00
timothy deleted branch chore/shared-checkout-refresh 2026-07-21 20:23:19 +02:00
Sign in to join this conversation.