Pre-push verification gate debugging
Repos with heavy local gates block git push until every lane passes (lefthook pre-push hook
calling a verify script that runs docs, unit, E2E, infra and frontend lanes). This skill is for
operating and debugging that gate without burning whole-gate cycles. The methods here
are generic; keep a repo-specific playbook beside the gate script (lane list + commands,
log layout, repro steps) as this skill's reference when one exists.
Detect and understand the gate
lefthook.yml → pre-push → script + runner (+ root). Read the script's header
comments FIRST: usage, lane names, exit codes, env vars (VERIFY_MODE, VERIFY_SKIP),
self-test, and the "reproduce / bypass / logs" failure contract.
- A blocked push ends with
🥊 quality gate (Ns) and exit status 1. The hook buffers lane
output — never diagnose from the push output alone.
Workflow
- Read the gate's own logs first. The failure block prints:
reproduce: bun <script> <lane> — run exactly this to re-run one lane (minutes, not the whole gate).
logs: <dir>/<lane>.log — the lane's full output; <dir>/progress.log is the run timeline
(mode, which lanes ran, PASS/FAIL per lane).
- Log dir is per-worktree (hash of the worktree path) — find it by mtime (
ls -t), not by
recomputing the hash.
- Run one lane for fast iteration:
bun <script> <lane> (e.g. api-e2e). Only re-run the
full gate (<script> pre-push) when the lane is green.
- The gate tests the WORKING TREE, not the pushed ref. If you push from a different
checkout than the one you want verified, the lanes verify the wrong bytes. Gate-test the
tree you intend to push.
- E2E/infra lanes:
- They start their own AppHost (Docker: emulator containers) OR reuse a live dev stack
that is already ready (probed first). Reuse is convenient but carries state forward.
- Cross-run state contamination: emulator data persists across suite runs on a reused
stack. Durable orchestration instance IDs are PERMANENTLY TAKEN after a terminal state,
so a later run scheduling the same deterministic ID 409s or silently dedupes. Symptom: a
test passes alone, then a DIFFERENT test fails on a later full run with no code change.
Fix: fresh ephemeral volumes (kill the reused stack, let the lane start its own).
- Worktree gotchas: a bare
git worktree add has NO node_modules → tooling lanes fail
(tsc TS2688 "Cannot find type definition file for 'bun'", bun test resolution errors).
Fix: symlink the main checkout's node_modules into the worktree. Never commit node_modules.
- Manual repro harness (when the lane's test output hides the real error — e.g. an
orchestration ends
failed with an EMPTY error field):
- Start the infra exactly as the lane does (see the reference / lane source for the AppHost
command and env).
- Build the host under test and run it with the E2E settings (the lane copies a
local.settings.e2e.json-style file into the build output before the tests run).
- Drive the failing flow with curl, sending the app's auth header (see reference).
- Read the HOST's own logs: orchestration/activity failures appear there with the full
exception ("Task 'X' failed with an unhandled exception: ..."), never in the HTTP status
payload.
failed + empty error in the API body = the detail is in the host logs.
- Auto-repair: some gates try to regenerate drifted artefacts (baseline.json, index.json)
before failing. If repair "declined", the cause is NOT drift — read the lane log.
Gotchas
- A push can LOOK hung while the pre-push hook is simply running.
git push 2>&1 | tail buffers everything, so the lefthook banner (🥊 lefthook ... hook: pre-push) never shows and the full gate (which can take 5+ minutes) looks like a network stall — zero output, no timeout. Diagnose by re-running the push UNPIPED or in background and reading the first lines: a banner means the gate is working, not hanging. If the user has said "skip gates" / "just push", git push --no-verify is the sanctioned bypass and returns instantly — do not keep waiting on a gate the user already waived.
full vs limited mode changes which lanes run; limited skips infra/e2e and prints that
coverage is still owed. Unset VERIFY_MODE for the real gate.
VERIFY_SKIP=<lane> and --no-verify bypass the gate entirely — prefer fixing the lane.
- A 401 with "Missing or invalid x-functions-key" can really mean a missing auth header on your
curl, not a missing key — check the app's principal/auth scheme before assuming.
- Don't re-run the full gate to test one fix — single-lane runs are minutes faster.
- Force-pushing a rebased PR branch re-triggers remote CI; the local gate still runs against
your working tree, so keep the local tree on the branch you are pushing.
- A green full-tree lint does NOT prove the changed-file lint lane passes. If the gate's
lint lane lints only changed files (git-diff scope) while your manual run covered every
tracked file, the lane can flag findings the full run reports clean — e.g. a W0108
unnecessary-lambda on a freshly added resolve=lambda: f() that a whole-repo
pylint $(git ls-files ...) rated 10.00/10. Reproduce with the actual lane
(<script> <lane>, e.g. verify.sh pylint) before pushing, not with a full-tree equivalent.
- The interpreter you run the gate with determines what the gate's subprocesses can import.
Gates that spawn
sys.executable (scaffold/regeneration lanes) inherit the python that
launched the gate. On macOS the bare python3 is often the CommandLineTools build (no
site-packages, e.g. ModuleNotFoundError: No module named 'jsonschema') — always run lanes
with the repo's venv python, or rely on the gate's own _resolve_python fallback.
- A worktree's own broken
.venv shadows the main checkout's good one. If the gate's
python resolution prefers $PWD/.venv and the worktree has a bare/broken venv (e.g. no pip,
no deps), every lane fails with import errors even though the main checkout's venv is
populated. Fix: delete the worktree .venv so resolution falls back to the main checkout's
venv. A stray venv you did not create is the first suspect when a fresh worktree's lanes
fail on missing modules.
git rebase --continue hangs: the pre-commit hook wedges in rebase context. Lefthook's
pre-commit can hang indefinitely while a rebase is being continued (observed with the
code-review-graph hook; the git process times out waiting on it and the rebase state survives
the kill). Replay the rest of the rebase with hooks off:
GIT_EDITOR=true git -c core.hooksPath=/dev/null rebase --continue. This is safe because the
pre-push gate re-runs every relevant lane on the pushed tree — enforcement happens at push,
not at rebase. A normal commit on a clean branch is unaffected; only the rebase context hangs.
- The gate runs SCOPED lanes, not the full list.
progress.log shows e.g. PUSH lanes: docs terraform
when a branch only touches docs+infra — the gate picks lanes by changed paths. Don't read a
scoped run as proof the E2E/dotnet lanes passed, and don't be surprised a scoped run finished in
seconds. self-test tooling tests ride inside the docs lane.
- Playwright-style e2e lanes reuse a live dev server on a fixed port (
reuseExistingServer: !CI). A wholesale failure pattern — every page test timing out while the [WebServer] log floods
Cannot find module / TS2307 for imports — is a SERVER-level failure, not your change. The
reused server may be a zombie from a removed worktree (its node_modules symlink is gone), or the
gate's own server hit a transient gap while a concurrent npm install in the main checkout
churned package dirs. Diagnose in this order: (1) who owns the port — lsof -nP -iTCP:<port> -sTCP:LISTEN, then lsof -p <pid> | grep cwd (never kill a process whose cwd is the owner's
checkout); (2) does the failing module exist on disk; (3) re-run the e2e lane alone for fast
feedback; (4) re-push. Keep a repo-specific playbook (port, lane commands, worktree
symlink quirks) beside the gate when the project has one.
- After an auto-repair commit, push AGAIN. The gate's repair path regenerates drifted
artifacts (e.g.
docs/meta/baseline.json, trust-index.json), commits
chore(gate): regenerate stale artifacts, and exits non-zero with "push again" — the first
push deliberately fails. Re-push the same ref (plain push; after a rebase, --force-with-lease).
- A lane can be red because the gate tests a working tree polluted by ANOTHER task's
uncommitted files — the repair path refuses to touch them, so the lane stays red until that
task commits. Repair declines with "not regenerating: has uncommitted changes, and
the repair may only commit bytes it wrote itself". This is not your diff. Prove it before
bypassing: (1) your branch diff contains no file in the failing lane's scope
(
git show --stat HEAD / git diff origin/main...HEAD); (2) the lane's own output shows
your files clean. Then the gate's failure block itself names the
sanctioned escape: VERIFY_SKIP=<lane> git push — stage-scoped, NOT --no-verify
(skips every lane) and NOT fixing the other task's files. The skipped lane still runs on
GitHub CI only if CI has it; a docs lane is often local-only, so the PR is unaffected.
git push --delete <branch> also runs the pre-push gate and can fail on it; bypass with
git -c core.hooksPath=/dev/null push origin --delete <branch> — deleting a remote ref needs
no verification.
References
The gate script's own header + logs are the primary reference (usage, lane names, exit
codes, reproduce/bypass commands). Keep a repo-specific playbook beside the gate script
with the lane list and commands, log layout, AppHost command, E2E auth header + config,
manual repro steps, and any repo-local drift/state files — and update it when the gate
changes.
1---2name: pre-push-gate-debugging3description: Use when a pre-push quality gate blocks git push or a lane fails: read the gate's own logs first (reproduce one lane), run single lanes for fast iteration, test the working tree you intend to push, handle E2E/infra cross-run state contamination, worktree node_modules gotchas, and build a manual repro harness when lane output hides the real error.4license: MIT5---67# Pre-push verification gate debugging89Repos with heavy local gates block `git push` until every lane passes (lefthook pre-push hook10calling a verify script that runs docs, unit, E2E, infra and frontend lanes). This skill is for11operating and debugging that gate without burning whole-gate cycles. The methods here12are generic; keep a repo-specific playbook beside the gate script (lane list + commands,13log layout, repro steps) as this skill's reference when one exists.1415## Detect and understand the gate1617- `lefthook.yml` → `pre-push` → `script` + `runner` (+ `root`). Read the script's header18 comments FIRST: usage, lane names, exit codes, env vars (`VERIFY_MODE`, `VERIFY_SKIP`),19 self-test, and the "reproduce / bypass / logs" failure contract.20- A blocked push ends with `🥊 quality gate (Ns)` and `exit status 1`. The hook buffers lane21 output — never diagnose from the push output alone.2223## Workflow24251. **Read the gate's own logs first.** The failure block prints:26 - `reproduce: bun <script> <lane>` — run exactly this to re-run one lane (minutes, not the whole gate).27 - `logs: <dir>/<lane>.log` — the lane's full output; `<dir>/progress.log` is the run timeline28 (mode, which lanes ran, PASS/FAIL per lane).29 - Log dir is per-worktree (hash of the worktree path) — find it by mtime (`ls -t`), not by30 recomputing the hash.312. **Run one lane** for fast iteration: `bun <script> <lane>` (e.g. `api-e2e`). Only re-run the32 full gate (`<script> pre-push`) when the lane is green.333. **The gate tests the WORKING TREE, not the pushed ref.** If you push from a different34 checkout than the one you want verified, the lanes verify the wrong bytes. Gate-test the35 tree you intend to push.364. **E2E/infra lanes**:37 - They start their own AppHost (Docker: emulator containers) OR **reuse a live dev stack**38 that is already ready (probed first). Reuse is convenient but carries state forward.39 - **Cross-run state contamination**: emulator data persists across suite runs on a reused40 stack. Durable orchestration instance IDs are PERMANENTLY TAKEN after a terminal state,41 so a later run scheduling the same deterministic ID 409s or silently dedupes. Symptom: a42 test passes alone, then a DIFFERENT test fails on a later full run with no code change.43 Fix: fresh ephemeral volumes (kill the reused stack, let the lane start its own).445. **Worktree gotchas**: a bare `git worktree add` has NO node_modules → tooling lanes fail45 (`tsc` TS2688 "Cannot find type definition file for 'bun'", bun test resolution errors).46 Fix: symlink the main checkout's node_modules into the worktree. Never commit node_modules.476. **Manual repro harness** (when the lane's test output hides the real error — e.g. an48 orchestration ends `failed` with an EMPTY error field):49 - Start the infra exactly as the lane does (see the reference / lane source for the AppHost50 command and env).51 - Build the host under test and run it with the E2E settings (the lane copies a52 `local.settings.e2e.json`-style file into the build output before the tests run).53 - Drive the failing flow with curl, sending the app's auth header (see reference).54 - Read the HOST's own logs: orchestration/activity failures appear there with the full55 exception ("Task 'X' failed with an unhandled exception: ..."), never in the HTTP status56 payload. `failed` + empty `error` in the API body = the detail is in the host logs.577. **Auto-repair**: some gates try to regenerate drifted artefacts (baseline.json, index.json)58 before failing. If repair "declined", the cause is NOT drift — read the lane log.5960## Gotchas61- **A push can LOOK hung while the pre-push hook is simply running.** `git push 2>&1 | tail` buffers everything, so the lefthook banner (`🥊 lefthook ... hook: pre-push`) never shows and the full gate (which can take 5+ minutes) looks like a network stall — zero output, no timeout. Diagnose by re-running the push UNPIPED or in background and reading the first lines: a banner means the gate is working, not hanging. If the user has said "skip gates" / "just push", `git push --no-verify` is the sanctioned bypass and returns instantly — do not keep waiting on a gate the user already waived.62- `full` vs `limited` mode changes which lanes run; limited skips infra/e2e and prints that63 coverage is still owed. Unset `VERIFY_MODE` for the real gate.64- `VERIFY_SKIP=<lane>` and `--no-verify` bypass the gate entirely — prefer fixing the lane.65- A 401 with "Missing or invalid x-functions-key" can really mean a missing auth header on your66 curl, not a missing key — check the app's principal/auth scheme before assuming.67- Don't re-run the full gate to test one fix — single-lane runs are minutes faster.68- Force-pushing a rebased PR branch re-triggers remote CI; the local gate still runs against69 your working tree, so keep the local tree on the branch you are pushing.70- **A green full-tree lint does NOT prove the changed-file lint lane passes.** If the gate's71 lint lane lints only changed files (git-diff scope) while your manual run covered every72 tracked file, the lane can flag findings the full run reports clean — e.g. a W010873 `unnecessary-lambda` on a freshly added `resolve=lambda: f()` that a whole-repo74 `pylint $(git ls-files ...)` rated 10.00/10. Reproduce with the actual lane75 (`<script> <lane>`, e.g. `verify.sh pylint`) before pushing, not with a full-tree equivalent.76- **The interpreter you run the gate with determines what the gate's subprocesses can import.**77 Gates that spawn `sys.executable` (scaffold/regeneration lanes) inherit the python that78 launched the gate. On macOS the bare `python3` is often the CommandLineTools build (no79 site-packages, e.g. `ModuleNotFoundError: No module named 'jsonschema'`) — always run lanes80 with the repo's venv python, or rely on the gate's own `_resolve_python` fallback.81- **A worktree's own broken `.venv` shadows the main checkout's good one.** If the gate's82 python resolution prefers `$PWD/.venv` and the worktree has a bare/broken venv (e.g. no pip,83 no deps), every lane fails with import errors even though the main checkout's venv is84 populated. Fix: delete the worktree `.venv` so resolution falls back to the main checkout's85 venv. A stray venv you did not create is the first suspect when a fresh worktree's lanes86 fail on missing modules.87- **`git rebase --continue` hangs: the pre-commit hook wedges in rebase context.** Lefthook's88 pre-commit can hang indefinitely while a rebase is being continued (observed with the89 code-review-graph hook; the git process times out waiting on it and the rebase state survives90 the kill). Replay the rest of the rebase with hooks off:91 `GIT_EDITOR=true git -c core.hooksPath=/dev/null rebase --continue`. This is safe because the92 pre-push gate re-runs every relevant lane on the pushed tree — enforcement happens at push,93 not at rebase. A normal commit on a clean branch is unaffected; only the rebase context hangs.94- **The gate runs SCOPED lanes, not the full list.** `progress.log` shows e.g. `PUSH lanes: docs terraform`95 when a branch only touches docs+infra — the gate picks lanes by changed paths. Don't read a96 scoped run as proof the E2E/dotnet lanes passed, and don't be surprised a scoped run finished in97 seconds. `self-test` tooling tests ride inside the `docs` lane.98- **Playwright-style e2e lanes reuse a live dev server on a fixed port** (`reuseExistingServer:99 !CI`). A wholesale failure pattern — every page test timing out while the [WebServer] log floods100 `Cannot find module` / TS2307 for imports — is a SERVER-level failure, not your change. The101 reused server may be a zombie from a removed worktree (its node_modules symlink is gone), or the102 gate's own server hit a transient gap while a concurrent `npm install` in the main checkout103 churned package dirs. Diagnose in this order: (1) who owns the port — `lsof -nP -iTCP:<port>104 -sTCP:LISTEN`, then `lsof -p <pid> | grep cwd` (never kill a process whose cwd is the owner's105 checkout); (2) does the failing module exist on disk; (3) re-run the e2e lane alone for fast106 feedback; (4) re-push. Keep a repo-specific playbook (port, lane commands, worktree107 symlink quirks) beside the gate when the project has one.108- **After an auto-repair commit, push AGAIN.** The gate's repair path regenerates drifted109 artifacts (e.g. `docs/meta/baseline.json`, `trust-index.json`), commits110 `chore(gate): regenerate stale artifacts`, and exits non-zero with "push again" — the first111 push deliberately fails. Re-push the same ref (plain push; after a rebase, `--force-with-lease`).112- **A lane can be red because the gate tests a working tree polluted by ANOTHER task's113 uncommitted files — the repair path refuses to touch them, so the lane stays red until that114 task commits.** Repair declines with "not regenerating: <path> has uncommitted changes, and115 the repair may only commit bytes it wrote itself". This is not your diff. Prove it before116 bypassing: (1) your branch diff contains no file in the failing lane's scope117 (`git show --stat HEAD` / `git diff origin/main...HEAD`); (2) the lane's own output shows118 your files clean. Then the gate's failure block itself names the119 sanctioned escape: `VERIFY_SKIP=<lane> git push` — stage-scoped, NOT `--no-verify`120 (skips every lane) and NOT fixing the other task's files. The skipped lane still runs on121 GitHub CI only if CI has it; a docs lane is often local-only, so the PR is unaffected.122- **`git push --delete <branch>` also runs the pre-push gate** and can fail on it; bypass with123 `git -c core.hooksPath=/dev/null push origin --delete <branch>` — deleting a remote ref needs124 no verification.125126## References127128The gate script's own header + logs are the primary reference (usage, lane names, exit129codes, reproduce/bypass commands). Keep a repo-specific playbook beside the gate script130with the lane list and commands, log layout, AppHost command, E2E auth header + config,131manual repro steps, and any repo-local drift/state files — and update it when the gate132changes.