Development record
Local record gates validate the WORKING TREE, not the committed/pushed tree, so a partial commit passes them while the committed tree fails CI. Demonstrated live this session: git mv staged a rename (iss-370->377) but the follow-up sed edit to the frontmatter id was left unstaged; git commit captured only the rename, so the committed file had filename iss-377 with frontmatter iss-370 — a record_schema blocker. record-lint had passed at commit time because it reads the working tree (which held the sed edit), and the pre-push make preflight passes for the same reason (the unstaged edit is still present); only CI, checking out the commit, sees the divergence. Fix direction: run the push-time gate against the tree that will actually ship (e.g. a clean worktree of HEAD, or git stash --keep-index before linting), so a working-tree/index divergence cannot pass locally and fail CI. Sibling of iss-147 (guard-load reads guard.json from the working tree).
Local record gates validate the WORKING TREE, not the committed/pushed tree, so a partial commit passes them while the committed tree fails CI. Demonstrated live this session: git mv staged a rename (iss-370->377) but the follow-up sed edit to the frontmatter id was left unstaged; git commit captured only the rename, so the committed file had filename iss-377 with frontmatter iss-370 — a record_schema blocker. record-lint had passed at commit time because it reads the working tree (which held the sed edit), and the pre-push make preflight passes for the same reason (the unstaged edit is still present); only CI, checking out the commit, sees the divergence. Fix direction: run the push-time gate against the tree that will actually ship (e.g. a clean worktree of HEAD, or git stash --keep-index before linting), so a working-tree/index divergence cannot pass locally and fail CI. Sibling of iss-147 (guard-load reads guard.json from the working tree).
Grounds
- pursued: a commit whose working tree diverged from it during its preflight cannot be pushed through the hook; a divergence that still earns a receipt, such as one git status --porcelain does not report, would show it wrong
- pursued: review met the falsifier above — an edit to a file flagged skip-worktree or assume-unchanged is invisible to git status and earned a receipt; the receipt is now withheld while any such flag is set, and the divergences it still cannot see (an ignored file a gate reads such as go.work, HEAD or the tree moved and restored between its two reads) are stated in scripts/preflight-receipt.sh and dcn-2609250000000004 rather than closed
Frontmatter
| id | iss-2608210738378295 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2608210738378295-local-gates-lint-working-tree-not-commit.md
open on GitHub ↗ · commit history ↗
mentions7
- dcn-26092500000000032026-09-25 — A push is gated before it opens its connection, by receipt (iss-2608290810036869, iss-2608210738378295; product thinker's ruling M16 of 2026-09-23, "check before connect"). The committed `.githooks/pre-push` no longer runs `make preflight`: git opens the connection before the hook, and a preflight inside it outlasted the transport's idle timeout, so a push reported success and moved nothing. `make preflight` ends by minting a receipt for HEAD under the checkout's local tier (`scripts/preflight-receipt.sh`), and only when the working tree matched HEAD (nothing staged, unstaged or untracked) both when the run began, read while the Makefile is parsed, and when it ended, with HEAD unmoved, so the gates read the tree CI checks out. The hook refuses a push whose commit is new to the remote and carries no receipt from any worktree of the repository; a commit the remote already holds (a tag on a merged commit) passes. Alternatives not taken: a push wrapper that runs the preflight and then `git push --no-verify` (it normalises `--no-verify`, and a plain `git push` would go ungated); keepalive settings on the transport (the hook would still hold the connection for ten minutes); a preflight on a clean export of HEAD (a second full tree and build per push, where refusing a divergent tree costs nothing). `git push --no-verify` skips the hook exactly as before, and CI stays the authority (lane hooks, autonomous run A).
- dcn-26092500000000042026-09-25 — Reach of the push-receipt entry above, stated after review (iss-2608210738378295). The receipt's clean-tree test is `git status` read when the preflight begins and when it ends, and a tree `git status` cannot see is not vouched for. So `scripts/preflight-receipt.sh` also refuses to mint while any tracked file is flagged skip-worktree or assume-unchanged (`git ls-files -v` tags it `S` or in lower case), because either flag hides that file's edits from the status read; a sparse checkout sets skip-worktree and so never mints. What the receipt still cannot see, stated in the script's header: files git ignores, which are outside the commit yet can be read by a gate (a `go.work`, which `.gitignore` lists, changes every Go gate's module resolution); HEAD moved and moved back, or the tree changed and restored, between the two reads; and anything a gate reads from outside the checkout. The receipt stays a local convenience gate and CI the authority; closing those would mean running the gates on a clean export of HEAD, the alternative the entry above did not take (lane hooks fix round, autonomous run A).
- iss-147guard.Load reads .abcd/guard.json from the WORKING TREE, so a disabled:true (or a retiered blocker) takes effect on the very next command, before anyone reviews it — spc-16 and the shipped docs both say the only escape is a committed, reviewable override, and nothing enforces the committed half. Reachable in one move: an agent writes .abcd/guard.json and the guard itself allows that write. Mitigated at the front door in itd-103 wiring (a disabled registry now warns UNGUARDED on every command, and abcd ahoy reports OFF) but not enforced. Proper fix is core-side: refuse a disabled:true that is not in HEAD, or drop the committed claim from spc-16. Found by the security reviewer on the itd-103 wiring branch.
- iss-370Agent worktrees commit without the private name-guard: .abcd/.work.local/ is per-worktree, so every isolated-worktree agent commit runs with the banlist layer absent — loudly warned, per design, but the isolated-agent pattern now systematically bypasses a protection the main checkout has. Candidate remedies: the worktree-creation path seeds a pointer to the primary checkout's store, or the hook falls back to reading the primary worktree's local tier
- iss-377Demote hooks/bootstrap.sh to a cold-start trampoline once abcd update ships (itd-130 follow-on rung). Trigger: itd-130 shipped. When any abcd binary is resolvable, the hook delegates provisioning to the Go core (abcd update); the raw curl fetch survives only for the no-binary-anywhere cold start — the one state a Go updater cannot serve. Seam hardening carried from itd-130's Decisions: delegate only to a binary passing an ownership check, keep the raw fetch as the fallback when delegation fails, and freeze the release-asset layout as a contract so an old PATH binary provisioning a new plugin root keeps working across cuts. Goal: one canonical downloader instead of three (bootstrap.sh, the README one-liner, the Go verb).
- itd-148Every change starts in its own worktree in abcd's store: the primary checkout is a read-only surface, and abcd blocks mutations there for every session but a declared coordinator
- spc-42every-change-starts-in-its-own-worktree-the-primary-checkout