Repository navigation
chore: fmt hook — skip delete pushes; gate auto-commit on HEAD-in-push - #509
Conversation
…push Tightens the pre-push hook's auto-commit behavior. Previous hook ran cargo fmt --check on every push regardless of what was being pushed; could commit a chore fmt fix to the current branch even when the push was a deletion of a different remote branch (observed after #497 merge — committed to track-witness-taxonomy during `git push origin --delete` of a merged PR branch). New decision table: - **Delete-only push** (all ref SHAs are zeros) → skip entirely. No content being pushed; nothing to fmt-check. - **Push includes current HEAD** → existing behavior. Clean-tree check + cargo fmt --all + chore commit on top of HEAD. - **Push excludes current HEAD** → fmt --check only; no auto-commit. Auto-committing to HEAD when HEAD isn't being pushed would land the fix on a branch that doesn't reach the remote anyway. Fails with an actionable message: switch branches and re-push, or commit the fmt fix explicitly. Reads stdin per git's pre-push hook contract: one line per pushed ref, format "<local_ref> <local_sha> <remote_ref> <remote_sha>". Zero SHA on local indicates a deletion. No new setup required — scripts/install-hooks.sh still works unchanged. Existing .git/hooks/pre-push installations can either re-run the install script (copies the updated .githooks/pre-push) or copy the file manually.
ChatGPT ReviewPrinciple audit. Fail-closed. This is implementation-local hook logic, not substrate, and the new control flow is mostly fail-closed in the modeling-discipline sense: delete-only pushes exit intentionally; dirty trees only block when an auto-commit could accidentally sweep them up; and fmt drift outside the HEAD-in-push case now fails with an explicit, actionable message instead of silently creating a commit that will not be pushed. That fits the thesis’s “validate the causal chain before acting” lens. chatgpt-review-1ba9a301-24da-4f… chatgpt-review-c1f4a072-3f21-45… Illegal states unrepresentable. No substrate or cross-pass data model changed here, so there is no new illegal-state surface to bank debt into. For shell, the Facts flow forward. This is the one place I have a real comment. The diff is an improvement because it finally consumes push-shape data from Git’s pre-push stdin instead of treating every push as “push current HEAD.” But at Coproduct dissolution. No new Rust enum or substrate coproduct appears in this diff, so the dissolution ledger/trigger requirement does not apply. This is ordinary implementation code. Single authority. Better than before: the push description now comes from Git’s stdin, which is the right authority, rather than from an implicit “current branch push” assumption. The only remaining weakness is the same one above: the hook still does not consume the full authoritative tuple when deciding whether auto-commit is safe. API-level enforcement. The new policy is encoded in control flow, not left as commentary: delete-only pushes skip, HEAD-not-in-push refuses auto-commit, HEAD-in-push keeps the old auto-fix path. That is the right shape for implementation code. Design question. Is the safety gate really “some pushed object ID equals HEAD,” or is it “the pushed source is a movable ref currently at HEAD”? The latter seems closer to the comment’s promise that the chore commit will “land on the ref being pushed.” Git’s pre-push contract exposes both Verdict. APPROVE_WITH_COMMENTS. This is a good, implementation-local tightening of the hook: it fixes two real misfires without introducing scaffolding or parallel authority. My only comment is the ref-identity edge case above, which I’d treat as NON-BLOCKING unless this repo often pushes tags or explicit refspec/object sources through the same hook. LOOP HEALTH: converging — this round consumes more of Git’s real push-shape facts and removes two concrete hook misbehaviors, with only a narrow ref-identity nuance still left unmodeled. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35d67a7d3d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if [[ "$local_sha" == "$current_head" ]]; then | ||
| current_head_in_push=1 |
There was a problem hiding this comment.
Gate auto-commit on pushed ref, not only matching SHA
current_head_in_push is flipped when local_sha == current_head, but that is also true when pushing a different ref that happens to point at the same commit (for example, pushing feature that shares the current tip, or a lightweight tag at HEAD). In that case the hook takes the auto-commit path and creates chore: apply cargo fmt on the checked-out branch even though that new commit is not in the pushed refset, which reintroduces the unintended “commit on unrelated branch” behavior this change is meant to prevent.
Useful? React with 👍 / 👎.
briansrls
left a comment
There was a problem hiding this comment.
codex · gpt-5.4 · 35d67a7d
BLOCKING (1)
Root Cause
.githooks/pre-pushThe design assumes a pre-push hook can mutate the ref being pushed, but ref selection is already fixed when the hook executes -> make this hook check-only/fail-closed, or move auto-fix+commit earlier than git push ref resolution.
Non-blocking — Strengths
.githooks/pre-pushThe delete-only short-circuit and the explicit cross-branch failure message do address the unrelated-branch auto-commit case that motivated this PR.
| exit 1 | ||
| fi | ||
|
|
||
| # HEAD is being pushed → safe to auto-commit on top of it. |
There was a problem hiding this comment.
BLOCKING: The "HEAD is being pushed -> safe to auto-commit" branch is incorrect because Git snapshots the refs-to-push before pre-push runs, so the new fmt commit is not sent and the hook succeeds with unresolved drift (Fail-Closed).
|
BLOCKING (1) Root Cause
Non-blocking — Strengths
|
…te pack) Codex review 35d67a7 caught a real bug: the hook claimed "push continuing with the new commit" but git had already built the push pack with the pre-hook SHA before calling the hook. Any commit the hook creates advances the local ref but doesn't reach the remote on that push. The "successful auto-fix that never reaches the remote" was the exact failure mode. Fix: keep the auto-fix + auto-commit convenience (so the fmt fix is already prepared), but EXIT 1 after the chore commit. The push aborts; the developer re-runs `git push` and the chore commit ships alongside their original work. Message now honestly tells the user: fmt fixes committed locally as 'chore: apply cargo fmt'. Git has already built the push pack with the pre-hook SHA, so the new commit would NOT reach the remote on this push. Run 'git push' again to push the fmt commit alongside your original work. Also: the edge case where cargo fmt --check reports drift but --all produces no diff now fails loud instead of silently exiting 0. Real bugs should surface, not hide. CLAUDE.md updated to describe the two-step behavior.
Addressed BLOCKING (commit 4267cf0)Reviewer correctly caught that `git push` builds the push pack from resolved local SHAs before calling the pre-push hook. A commit created inside the hook advances the local ref, but git has already captured the pre-hook SHA in the pack — the new commit stays local and doesn't reach the remote. The "[pre-push] committed fmt fixes — push continuing with the new commit" message was lying. FixKeep the auto-fix + auto-commit convenience (so the fmt fix is already prepared as a chore commit — user doesn't have to type `cargo fmt --all && git add -u && git commit`), but exit 1 after the commit to abort the push. User re-runs `git push` and the chore commit ships alongside the original work. New decision table
New abort message (honest about the git contract)``` Run 'git push' again to push the fmt commit alongside your original work. One other small fixThe `cargo fmt --check says drift but --all produces no diff` edge case previously exited 0 (swallowing the anomaly). Now fails loud with a message — real bugs should surface. Test plan
Thanks for catching this — the hook was silently wrong about what it was shipping. |
ChatGPT ReviewPrinciple audit. Fail-closed. This is materially better than the previous version. The main Codex issue is fixed: on the HEAD path the hook now auto-fixes, commits locally, and aborts, rather than pretending the new commit rides the in-flight push. Delete-only pushes are also handled explicitly, which matches Git’s pre-push input contract for delete refs. My one NON-BLOCKING concern is that the success path still is not tied to the ref(s) being pushed: Illegal states unrepresentable. For a Bash hook, this is fine. Facts flow forward. This is where I still see the real design miss. The hook receives the authoritative pushed-ref facts on stdin at Coproduct dissolution. No substrate enum work here, and the branching is implementation-local shell logic. I do not see a coproduct concern. Single-authority metadata. Same root cause in a sharper form: API-level enforcement. Reasonably good for a hook, but this is still enforced by convention more than mechanism. The comments and docs say cross-branch pushes skip the auto-commit path, but the implementation only approximates that through SHA equality. A tighter boundary would either make this hook explicitly HEAD-branch-only, or inspect the pushed Design question. Is this hook supposed to validate the checked-out worktree, or the refs named by pre-push stdin? That is the deepest structural question here. Right now it is a hybrid: it uses stdin only to derive Verdict. APPROVE_WITH_COMMENTS. The core false assumption from the prior round is fixed, and the new diagnostics are honest and helpful. My only real concern is NON-BLOCKING: the cross-ref / alias-ref path still collapses “pushed ref” into “current HEAD” too early, so the hook can judge or auto-commit against the wrong authority. LOOP HEALTH: converging — this round fixes the original “pre-push can mutate the in-flight pack” mistake, and the remaining issue is a narrower authority mismatch rather than a new spread of debt. |
Hand-written shell scripts (.githooks/pre-push from PRs #503/#509, scripts/install-hooks.sh, scripts/check-stage0-freshness.sh, scripts/regenerate-stage0.sh) bypass the compiler's dependency- analysis machinery. Per the compiler-as-dependency-analyzer thesis (tonight's framing), they should be .dag programs composing existing service operations from extdeps/ and emitted as standalone shell scripts at build time. Recon done on existing substrate: - dsl/extdeps/git.dag: service git.Core with 7 operations + shell transport + mock_response pattern. 181 lines. - dsl/extdeps/cargo.dag: Build, Test, Clippy, Doc, Run. MISSING Fmt. - dsl/extdeps/shell.dag: POSIX Find, Env. Adequate. - dsl/extdeps/github/: auth, pulls, gists, actions (not on critical path for pre-push hook). Separable prerequisite deferrals named: - cargo.dag Fmt operations (S) — mechanical extension - Shell-emission target (M, needs design) — v3 emits Rust/Go/Python today; shell is used as transport but not as emission target. Needs a DB clarifying what "emit to shell" means structurally (likely E-9-pattern with output as standalone executable text). - "Hook-as-program" pattern (M, needs design) — how a .dag program declares its invocation contract (stdin format, env, exit codes). - Test coverage via DB-15 R2's MockBackedInvariant predicate. Yellow-flag threshold: triggers actively when a *second* hand- written workflow script needs the same modeling. Until then, the hand-written pre-push hook (PRs #503 + #509) is tolerated as the one instance. Added under Active Deferrals §"Cross-cutting — workflow scripts modeled in .dag". When PR #507 (Scheduled Deletions) merges, the hand-written .githooks/pre-push also gets a row there with trigger "emitted pre-push hook replaces it."
|
Meta-review in progress... (view conversation) Loop-health check: is this review cycle making forward progress, or shifting debt? Posts in ~5-15 minutes. |
Round-2 reviewer (chatgpt on PR #509) correctly caught that matching pushed refs to HEAD via SHA equality alone can false-positive on aliased refs. Two scenarios: - Tag pushes whose tag happens to point at HEAD: local_ref = refs/tags/<name>, local_sha = HEAD's sha. Old hook treated this as "HEAD is being pushed" and tried to auto-commit onto HEAD. New hook: local_ref != refs/heads/<current>, so treated as cross-ref (fmt-check only, no auto-commit). - Branch B pointing at same commit as branch A (checked out): push of B has local_ref = refs/heads/B, not refs/heads/A. Old hook matched on SHA → wrong auto-commit target. New hook matches on ref name → correctly treated as cross-branch. Fix: resolve HEAD's full ref via git symbolic-ref ("refs/heads/<current>"), match pushed local_ref against that. Detached HEAD is handled explicitly — no current branch means no auto-commit is possible; the hook fails with an actionable message pointing at the detached state. Rename: current_head_in_push → head_branch_in_push. The fact being carried is "the current branch's ref is in the push list", not "some pushed ref's SHA equals HEAD". The rename makes the variable's meaning match what the code does. Authority-flow per the reviewer: git's pre-push stdin is ref- oriented (<local_ref> <local_sha> <remote_ref> <remote_sha>). Reducing that to a single boolean collapsed the ref identity the later decision needed. New version preserves ref-name identity through the decision path.
Addressed round-2 NON-BLOCKING — ref name, not SHA, is the authority (commit 7d1c057)Reviewer correctly caught: matching pushed refs to HEAD by SHA equality alone false-positives on aliased refs. Two concrete scenarios the old version got wrong:
In both cases the auto-commit would have landed on the checked-out branch while the push carried a different ref — exactly the cross-ref bug the reviewer described. FixResolve HEAD's full ref via `git symbolic-ref HEAD` (returns `refs/heads/`, empty on detached HEAD) and match pushed `local_ref` against that. Ref identity is preserved through the decision path; reduction to boolean happens only after the authoritative comparison. Also renamed the variable `current_head_in_push` → `head_branch_in_push` so the name matches what the check does: "the current branch's ref is in the push list," not "some pushed SHA equals HEAD." Detached HEAD explicitly handled`git symbolic-ref HEAD` fails (empty result) in detached state. Auto-commit can't fire (no branch to commit onto). The fmt-drift branch now splits: ``` If HEAD is on branch but that branch isn't being pushed: Test plan
Authority-flow observation the reviewer madeGit's pre-push stdin is ref-oriented; the hook was collapsing that authority too early by reducing to a boolean via SHA matching. The fix preserves the ref-name identity through the decision path. That's the kind of "facts flow forward" issue we've been disciplined about in substrate design; nice to see it apply to shell-script hooks too. |
non-blockings Three reviewer concerns addressed: 1. BLOCKING (codex): yellow-flag threshold understated. Four hand- written scripts already exist, so the deferral is active now — not 'tolerated until a second instance.' Reframed: marked ACTIVE; THESIS.md's meta-process claim is the justifying authority, not an arbitrary 'second script' trigger. 2. NON-BLOCKING (codex): 'First concrete use case' over-described the pre-push hook. At PR #509 HEAD the hook fmt-checks / fmt-fixes / commits; the stdin/delete/HEAD contract is implementation detail that belongs in the .dag design work, not the ROADMAP entry. Trimmed. 3. NON-BLOCKING (chatgpt facts-flow-forward): Track 15 (CLI tool modeling — bare command names are hidden PATH dependencies) wasn't threaded into the prerequisite list. Added as a prerequisite deferral referencing ROADMAP.md:810-826 directly. 4. SINGLE-AUTHORITY pressure (chatgpt): Shape A vs Shape B was an implicit lean toward Shape A ('shell-emission target'). Per Track 16 (ROADMAP.md:920-935), the thesis puts shell scripts as Shape B — .dag programs build them via concat/fold/match, parallel to Track 16's YAML emission. Updated entry to make Shape B explicit, remove the 'shell-emission target' framing, and align with tools/ratchet.dag's grep-command generation as precedent. Prerequisite list rewritten: - cargo.dag Fmt operations (S, mechanical) - Track 15 tool resolution (already-tracked prerequisite) - Shape B emission via existing Track 16 pattern (no new compiler concept) - Hook invocation contract as structural declaration (small type in a .dag program) - Test coverage via DB-15 R2 MockBackedInvariant Scope for the other three scripts (install-hooks.sh, check-stage0-freshness.sh, regenerate-stage0.sh) noted: same Shape B pattern, dissolves individually once pre-push proves it.
… as first use case) (#510) * docs: ROADMAP — track the commit-pipeline-in-dag modeling work Hand-written shell scripts (.githooks/pre-push from PRs #503/#509, scripts/install-hooks.sh, scripts/check-stage0-freshness.sh, scripts/regenerate-stage0.sh) bypass the compiler's dependency- analysis machinery. Per the compiler-as-dependency-analyzer thesis (tonight's framing), they should be .dag programs composing existing service operations from extdeps/ and emitted as standalone shell scripts at build time. Recon done on existing substrate: - dsl/extdeps/git.dag: service git.Core with 7 operations + shell transport + mock_response pattern. 181 lines. - dsl/extdeps/cargo.dag: Build, Test, Clippy, Doc, Run. MISSING Fmt. - dsl/extdeps/shell.dag: POSIX Find, Env. Adequate. - dsl/extdeps/github/: auth, pulls, gists, actions (not on critical path for pre-push hook). Separable prerequisite deferrals named: - cargo.dag Fmt operations (S) — mechanical extension - Shell-emission target (M, needs design) — v3 emits Rust/Go/Python today; shell is used as transport but not as emission target. Needs a DB clarifying what "emit to shell" means structurally (likely E-9-pattern with output as standalone executable text). - "Hook-as-program" pattern (M, needs design) — how a .dag program declares its invocation contract (stdin format, env, exit codes). - Test coverage via DB-15 R2's MockBackedInvariant predicate. Yellow-flag threshold: triggers actively when a *second* hand- written workflow script needs the same modeling. Until then, the hand-written pre-push hook (PRs #503 + #509) is tolerated as the one instance. Added under Active Deferrals §"Cross-cutting — workflow scripts modeled in .dag". When PR #507 (Scheduled Deletions) merges, the hand-written .githooks/pre-push also gets a row there with trigger "emitted pre-push hook replaces it." * docs: commit-pipeline deferral — address codex BLOCKING + chatgpt non-blockings Three reviewer concerns addressed: 1. BLOCKING (codex): yellow-flag threshold understated. Four hand- written scripts already exist, so the deferral is active now — not 'tolerated until a second instance.' Reframed: marked ACTIVE; THESIS.md's meta-process claim is the justifying authority, not an arbitrary 'second script' trigger. 2. NON-BLOCKING (codex): 'First concrete use case' over-described the pre-push hook. At PR #509 HEAD the hook fmt-checks / fmt-fixes / commits; the stdin/delete/HEAD contract is implementation detail that belongs in the .dag design work, not the ROADMAP entry. Trimmed. 3. NON-BLOCKING (chatgpt facts-flow-forward): Track 15 (CLI tool modeling — bare command names are hidden PATH dependencies) wasn't threaded into the prerequisite list. Added as a prerequisite deferral referencing ROADMAP.md:810-826 directly. 4. SINGLE-AUTHORITY pressure (chatgpt): Shape A vs Shape B was an implicit lean toward Shape A ('shell-emission target'). Per Track 16 (ROADMAP.md:920-935), the thesis puts shell scripts as Shape B — .dag programs build them via concat/fold/match, parallel to Track 16's YAML emission. Updated entry to make Shape B explicit, remove the 'shell-emission target' framing, and align with tools/ratchet.dag's grep-command generation as precedent. Prerequisite list rewritten: - cargo.dag Fmt operations (S, mechanical) - Track 15 tool resolution (already-tracked prerequisite) - Shape B emission via existing Track 16 pattern (no new compiler concept) - Hook invocation contract as structural declaration (small type in a .dag program) - Test coverage via DB-15 R2 MockBackedInvariant Scope for the other three scripts (install-hooks.sh, check-stage0-freshness.sh, regenerate-stage0.sh) noted: same Shape B pattern, dissolves individually once pre-push proves it.
Summary
Follow-up to #503. Tightens the pre-push hook so it doesn't auto-commit to an unrelated branch when the push is a deletion or a cross-branch push.
What was wrong
The previous hook checked fmt on every push regardless of what was being pushed. Observed after #497 merge: `git push origin --delete docs/db-14-substrate-external-primitives` fired the hook from a worktree on an unrelated branch; fmt drift on that branch got auto-committed as `chore: apply cargo fmt` on the worktree's branch — even though no content was being pushed, and the push target was just a remote-ref deletion.
Harmless in that case (the branch was local-only) but surfaces a real UX wart: auto-commit should only happen when the commit will land on a ref being pushed.
New decision table
Reading stdin per git's pre-push contract (one line per ref being pushed, format `<local_ref> <local_sha> <remote_ref> <remote_sha>`, zero local SHA = deletion):
Test plan
Already installed
I've also updated my local `.git/hooks/pre-push` to the new version (the `.git/hooks/` install survives across branch changes since it's outside version control). When this merges, re-running `scripts/install-hooks.sh` picks up the new `.githooks/pre-push` automatically for anyone using the `core.hooksPath .githooks` install pattern.
🤖 Generated with Claude Code