Skip to content

feat(gauntlet): push lints only; full gauntlet once at merge with a tree-hash pass cache (GH #735) - #740

Merged
wasikarn merged 10 commits into
developfrom
claude/gh735-fast-push
Oct 30, 2026
Merged

wasikarn merged 10 commits into
developfrom
claude/gh735-fast-push

Conversation

@wasikarn

Copy link
Copy Markdown
Owner

Closes #735

Summary

 git push
   pre-push: parse refs, refuse develop, develop-overlap check
-  gauntlet lock + full gauntlet                      # minutes, queued
+  lint the pushed commits' files (scripts/_lib/gauntlet-lint.sh), no lock   # seconds
+  MH_PREPUSH_FULL=1 -> old lock + full gauntlet

 merge-pr.sh <PR>, each round
   sync develop + bump
-  push -> pre-push runs the gauntlet
+  HEAD == $head -> gauntlet_full (cache hit, or unsharded run under the lock)
+  -> status clean and HEAD still $head -> push $head -> merge --match-head-commit

 merge-pr.sh --verify (cloud)
+  refuse unless a pass is cached for <head>^{tree}
 sync-pr-head.sh
+  gauntlet_full in its temp worktree before the push (also for an already-synced head)

 run-gauntlet.sh
+  record ~/.cache/mh/gauntlet-pass/<HEAD^{tree}> only if: all layers pass, unsharded,
+  run from the repo top, checkout clean (untracked included) with the same tree at start and end

Evidence

  • Fast pre-push, measured by the validators: 2.15 s (existing branch) and 5.6 s (new branch, with the develop fetch and overlap check), at load 8-15, while a peer held the gauntlet lock: it never waited.
  • Full gauntlet at 64bb153: PASS; exactly one pass file appeared, named by HEAD^{tree}.
  • Tests: merge-pr 370/0, sync-pr-head 28/0, pre-push-refs 19/0, pre-push-develop-overlap 13/0, gauntlet-pass-cache 14/0, gauntlet-lock 18/0; audit 0 Critical.
  • Two validator rounds. Round 1 found a pass recorded for an enclosing checkout's tree (gauntlet run from a non-repo copy inside an ignored dir), plus 5 small gaps (HEAD re-check after the run, a leftover GAUNTLET_SHARD, status.showUntrackedFiles=no, an awk regex escape, doc wording). All fixed in 35501e5; each fix's test fails when the fix is reverted.

Merge Danger

Door: two-way (revert restores the full pre-push gauntlet)

Blast Radius: merge-gate

The only full-gauntlet gate moves from pre-push to merge-pr.sh / sync-pr-head.sh. A merge made outside those (e.g. the GitHub web UI) gets only the lint, as it already got nothing from a push made elsewhere. Pre-existing, not added here: between the last develop fetch and gh pr merge, a merge that bypasses the merge lock can move develop; GitHub's "require branches to be up to date" setting would close that and is the operator's call.

core.hooksPath must stay relative (git-hooks) for each worktree to run its own hook; it was found absolute (pointing at the main checkout, then on an old develop) on 2026-10-27 and was reset.

…e by tree (GH #735)

pre-push keeps ref parsing, the develop refusal and the overlap check, then lints only the files the
pushed commit changed (scripts/_lib/gauntlet-lint.sh, shared with run-gauntlet.sh's lint layer): no
tests, no gauntlet queue. MH_PREPUSH_FULL=1 runs the old queued full gauntlet.

merge-pr.sh and sync-pr-head.sh run the full gauntlet themselves, under the gauntlet lock, on the exact
tree they push and merge, before the push (sync-pr-head.sh also on an already-synced head). A full,
clean, passing run-gauntlet.sh records ~/.cache/mh/gauntlet-pass/<HEAD^{tree}>; both scripts skip the
run on a hit. GAUNTLET_PASS_CACHE=0 neither reads nor writes it.
…push

# Conflicts:
#	scripts/gate-differential.sh
#	tests/scripts/test-pre-push-refs.sh
…pass for its tree (GH #735)

A push only lints now, so the cloud path's guarantee that the merged tree passed the full gauntlet
rests on sync-pr-head.sh. --verify makes it enforced: it refuses unless a pass is cached for
<head>^{tree} (GAUNTLET_PASS_DIR), naming sync-pr-head.sh or a clean run-gauntlet.sh as the way to get
one. GAUNTLET_PASS_CACHE=0 means no cache satisfies it.
- gauntlet_pass_start records nothing unless the cwd is the checkout's top (a non-repo copy in an
  ignored dir keyed on the enclosing checkout's tree), and counts untracked files even with
  status.showUntrackedFiles=no (also merge-pr.sh's two clean checks).
- merge-pr.sh refuses when HEAD moved off $head during the gauntlet (status stays clean on a commit).
- gauntlet_full unsets GAUNTLET_SHARD so a leftover shard cannot pass a partial run.
- gauntlet-lint.sh matches a literal dot as [.]: awk -v turned '\.sh$' into '.sh$'.
- lock-owner docs: the child skip applies with MH_PREPUSH_FULL=1, not to a plain push.
…push

# Conflicts:
#	docs/reference/repo-gotchas.md
#	scripts/gate-differential.sh
gauntlet_pass_has shared by gauntlet_full and merge-pr.sh --verify; _lint_existing; gauntlet_pass_start
returns on a missing HEAD with a reason; codex-integration-map and the pipefail lint comment name the
new lint-at-push, gauntlet-at-merge split and _lint_pick.
@wasikarn

Copy link
Copy Markdown
Owner Author

Fresh-context re-review of the clean-up commit 5cb2638, done by a separate agent on a clean checkout. The commit changes 5 files (+11/-8) and makes only the five listed clean-ups:

  • gauntlet_pass_has <tree>: the shared check now used by gauntlet_full and merge-pr.sh --verify. Behaviour is unchanged: --verify still refuses when GAUNTLET_PASS_CACHE=0 or when no pass exists for that exact tree.
  • existing is renamed to _lint_existing. Its only caller is updated, and a repo-wide grep finds no leftover call.
  • gauntlet_pass_start: the || return 0 form behaves the same as the old one.
  • Docs and comments: codex-integration-map.md (where lint and the full gauntlet run) and the _lint_pick pointer in test-pipefail-grep-q-lint.sh.
  • Tests: merge-pr 370/0, gauntlet-pass-cache 14/0, pre-push-refs 19/0.

Earlier full-gauntlet PASS and two-axis MERGE reviews were done at 64bb153. This commit has no behaviour change on top of that.

Fresh-context review: MERGE @ 5cb2638

@wasikarn
wasikarn merged commit 2b39bc2 into develop Oct 30, 2026
@wasikarn
wasikarn deleted the claude/gh735-fast-push branch October 30, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Push in under 30 s: fast pre-push, full gauntlet once at merge, pass cache by tree hash

1 participant