You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
10Tree cached — an identical re-push skips straight to the end. (cache writes)
0 observed Pre-push gates: this exact tree already passed (cache hits)
median push wall-clock 45.6s, worst 216.5s — inconsistent with hits being common
Every merge, conflict re-resolve, git commit --amend, and regenerated-artifact commit produces a new HEAD^{tree}, so the key changes on literally every push in that loop. The cache pays its bookkeeping and returns nothing.
Root cause
.husky/pre-push:250-256 keys the entire cache on one whole-tree hash:
It is all-or-nothing: any byte anywhere in the tree invalidates every gate, including gates whose inputs did not change. A docs-only amend after a red CI run re-pays the full suite.
What the gates actually cost
Measured in curious-frolicking-kite, 2026-08-16, warm:
gate
cold
warm
pro-test (npm ci + vite build)
54.9s
7.1s
resilience tests
23.8s
make generate (proto freshness)
10.5s
typecheck:api
7.8s
lint:rate-limit-policies
7.9s
test:dom (36 files)
6.7s
check-unicode-safety
5.0s
lint:md
4.4s
typecheck + convex + 6 other lints
~11s
Sum with everything firing ≈ 165s, which matches the observed 149s and 216s pushes.
Proposed fix — key each expensive gate on its own inputs
Add a per-gate cache alongside (not replacing) the whole-tree one. Key = sha256(gate-name + content hash of that gate's declared inputs). Store under $(git rev-parse --git-common-dir)/wm-prepush-gate-cache/.
Hash the worktree bytes, not HEAD:<path> — the gates run the worktree, and conflating the two is the exact class of bug the attestation machinery in scripts/prepush-attest.sh exists to prevent. git hash-object over the input list is the cheap primitive.
Hard constraints
Honour the existing attestation rules. A per-gate entry must never be written when the run cannot vouch for what it tested — mirror the ATTESTABLE / DIFF_RESOLVED refusals in scripts/prepush-attest.sh (cache-write). A cache entry is a "bad run that suppresses future runs", which is why those rules exist.
Only cache side-effect-free gates. This is not hypothetical: PR chore(agents): add preflight and PR snapshot gates #6748 briefly added npm run inventory:factsinside the edge-bundle gate. Had that landed, caching-and-skipping that gate would also have skipped a generation later steps depend on. If a gate mutates the worktree, either exclude it or split the mutation out first.
Put the read/write decisions in scripts/prepush-attest.sh, not inline in the hook. That file exists precisely so a test can execute these decisions against real git fixtures instead of grepping the hook's source text.
Acceptance criteria
Re-pushing an amended commit that changed only, say, a Markdown file skips the pro-test, proto, and typecheck gates and re-runs only the markdown lint.
Changing any declared input of a gate invalidates that gate and no others.
A gate is never skipped on a run that would be refused by the existing cache-write rules (unresolved branch diff, worktree not byte-identical to HEAD).
No gate with worktree side effects is in the cacheable set.
Cache entries are keyed so two worktrees with identical inputs share a hit (163 worktrees exist today; this is most of the value).
Measured: a docs-only amend re-push drops from ~150s to under ~15s.
How to verify
Extend tests/prepush-attest.test.mjs with real git fixtures covering: hit, miss on changed input, no-cross-gate-invalidation, and refusal under a dirty worktree. Then measure a real amend-and-repush cycle before/after.
Landmines
Never analyse or push from the main checkout — use a worktree or origin/main.
Editing .husky/pre-push triggers its contract tests (path_matches at .husky/pre-push:467).
Read the comment block at .husky/pre-push:127-141 and :258-296 before touching this. It documents why the ordering and the drift measurement are where they are.
Heavy checks in a worktree run sequentially — running them in parallel gets you exit 137.
Non-goals
Do not change the whole-tree cache's semantics or remove it; add the per-gate layer beneath it.
This series
Four issues from one measured diagnosis of pre-push latency (2026-08-15/16). Independent unless noted.
Ordering:#6764 and #6765 both edit .husky/pre-push and share the diff plumbing — take them together, or land #6764 first. #6766 and #6767 are independent of both.
Sizing context: the original "pre-push exceeds ten minutes" report was partly misattributed. Both 10-minute timeouts in the source transcript were compound commands (marker-guard.py; git add -A && git commit …; checkers.sh; git push), not the hook alone. The hook's real cost is median 45.6s, worst 216.5s over 66 push invocations. Still worth fixing — but size the work against those numbers, not against ten minutes.
Symptom
The pre-push green-tree cache never hits in the workflow that needs it most — the merge / amend / re-push loop.
Evidence from session
reflective-squishing-aurora(Canada merge campaign, ~24h, 66 push invocations):Tree cached — an identical re-push skips straight to the end.(cache writes)Pre-push gates: this exact tree already passed(cache hits)Every merge, conflict re-resolve,
git commit --amend, and regenerated-artifact commit produces a newHEAD^{tree}, so the key changes on literally every push in that loop. The cache pays its bookkeeping and returns nothing.Root cause
.husky/pre-push:250-256keys the entire cache on one whole-tree hash:It is all-or-nothing: any byte anywhere in the tree invalidates every gate, including gates whose inputs did not change. A docs-only amend after a red CI run re-pays the full suite.
What the gates actually cost
Measured in
curious-frolicking-kite, 2026-08-16, warm:npm ci+ vite build)make generate(proto freshness)typecheck:apilint:rate-limit-policiestest:dom(36 files)check-unicode-safetylint:mdSum with everything firing ≈ 165s, which matches the observed 149s and 216s pushes.
Proposed fix — key each expensive gate on its own inputs
Add a per-gate cache alongside (not replacing) the whole-tree one. Key =
sha256(gate-name + content hash of that gate's declared inputs). Store under$(git rev-parse --git-common-dir)/wm-prepush-gate-cache/.Starting set, highest payoff first:
:660-706)pro-test/src/**,pro-test/package-lock.json,convex/config/productCatalog.ts,scripts/generate-product-config.mjs:593-640)proto/**,Makefile,scripts/generate-request-validation.mjstypecheck/typecheck:apilint:md/ MDX lint.md/.mdxfilesHash the worktree bytes, not
HEAD:<path>— the gates run the worktree, and conflating the two is the exact class of bug the attestation machinery inscripts/prepush-attest.shexists to prevent.git hash-objectover the input list is the cheap primitive.Hard constraints
ATTESTABLE/DIFF_RESOLVEDrefusals inscripts/prepush-attest.sh(cache-write). A cache entry is a "bad run that suppresses future runs", which is why those rules exist.npm run inventory:factsinside the edge-bundle gate. Had that landed, caching-and-skipping that gate would also have skipped a generation later steps depend on. If a gate mutates the worktree, either exclude it or split the mutation out first.scripts/prepush-attest.sh, not inline in the hook. That file exists precisely so a test can execute these decisions against real git fixtures instead of grepping the hook's source text.Acceptance criteria
cache-writerules (unresolved branch diff, worktree not byte-identical to HEAD).How to verify
Extend
tests/prepush-attest.test.mjswith real git fixtures covering: hit, miss on changed input, no-cross-gate-invalidation, and refusal under a dirty worktree. Then measure a real amend-and-repush cycle before/after.Landmines
origin/main..husky/pre-pushtriggers its contract tests (path_matchesat.husky/pre-push:467)..husky/pre-push:127-141and:258-296before touching this. It documents why the ordering and the drift measurement are where they are.Non-goals
Do not change the whole-tree cache's semantics or remove it; add the per-gate layer beneath it.
This series
Four issues from one measured diagnosis of pre-push latency (2026-08-15/16). Independent unless noted.
git fetch(2s typical, up to 72s)Ordering: #6764 and #6765 both edit
.husky/pre-pushand share the diff plumbing — take them together, or land #6764 first. #6766 and #6767 are independent of both.Sizing context: the original "pre-push exceeds ten minutes" report was partly misattributed. Both 10-minute timeouts in the source transcript were compound commands (
marker-guard.py; git add -A && git commit …; checkers.sh; git push), not the hook alone. The hook's real cost is median 45.6s, worst 216.5s over 66 push invocations. Still worth fixing — but size the work against those numbers, not against ten minutes.