Appendix A - 6. Pre-commit hook (3-stanza, tree-sha markers)
The Structure of Pre-commit hook (3-stanza, tree-sha markers) — its shape at a glance:
Three stanzas fire in order at commit time, then a downstream merge-check reads the tree-keyed marker: a pass on this tree becomes an auditable fact, not a trust assumption.
flowchart LR
Commit[/Commit attempt/] --> Lint[Changed-file lints]
Lint --> Test[Unit-tier tests]
Test --> Marker[(Write tree-sha marker)]
Marker --> Check{merge-check: valid marker?}
Check -->|yes| Advance([Advance to merge])
Check -->|no| Reject([Reject commit])
Accessible description: a commit attempt runs changed-file lints, then unit-tier tests, then writes a marker keyed to the tree's sha; a downstream merge-check advances the commit only if a valid marker exists for its tree and rejects it otherwise.
Projected from the catalogue entry gates-and-merge-train / Pre-commit hook (3-stanza, tree-sha markers).
On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns
Intent
Intent — A three-stanza pre-commit hook that runs changed-file lints and unit-tier tests and writes tree-sha-keyed marker files, so an agent's commit cannot advance to merge unless the cheap checks actually passed on exactly this tree.
Motivation
Agents commit fast and often. Without a cheap gate at commit time, a broken change flows downstream to the expensive gates (merge-train, deploy) where the context of what the agent was doing is gone and the cost of diagnosis is highest. Worse, because the merge-train batches many agents' work, one un-checked broken commit can poison an entire batch. The failure recurs on every commit and compounds with fleet size: the later the break is found, the more work is entangled with it.
Applicability
- Changed-file-scoped lints fast enough to run on every commit; a whole-tree sweep here would make commit unbearable (that is what the aggregate-lint mediator is for, later in the staircase).
- A unit-tier test set that runs in seconds (the "1-second rule" discipline).
- A marker store keyed to tree identity, so a pass cannot be replayed onto a different tree.
- A downstream verifier (
merge-check) that refuses unmarked commits; without it the markers are advisory and the gate is skippable.
Structure
The Structure diagram appears at the top of this page.
Sample Code
The gate keys its pass-marker to the tree sha, so a marker from one tree can never vouch for another. The downstream verifier re-derives the tree sha and refuses any commit lacking a matching marker — the early check becomes an independently auditable fact.
import hashlib, os, sys
MARKER_DIR = ".gate-markers"
def tree_sha(files: list[str]) -> str:
h = hashlib.sha256()
for f in sorted(files):
h.update(f.encode()); h.update(open(f, "rb").read())
return h.hexdigest()
def pre_commit(files: list[str], run_lints, run_unit_tests) -> int:
if run_lints(files) or run_unit_tests(): # each returns findings; non-empty = fail
return 1 # block the commit; no marker written
os.makedirs(MARKER_DIR, exist_ok=True)
open(os.path.join(MARKER_DIR, tree_sha(files)), "w").close() # marker keyed to THIS tree
return 0
def merge_check(files: list[str]) -> int:
ok = os.path.exists(os.path.join(MARKER_DIR, tree_sha(files)))
return 0 if ok else 1 # refuse to advance a commit whose tree was never gated
Consequences
- Per-commit latency tax. Every commit pays the lint + unit-tier cost; mitigated by changed-file scoping and unit-tier-only, but a slow unit-tier erodes commit cadence and creates pressure to bypass. The gate's cost is a direct incentive against it.
- Bypass prefixes are a real hole.
sentinel:/tombstone:/chore(worktree):commits skip entirely; correctness there rests on those prefixes being used honestly (auditable, not prevented). - Marker logic is subtle and easy to break. If the tree-sha keying drifts, markers silently stop matching and either block good commits or vouch for the wrong tree.
Example use within DocAble
- The 3-stanza hook + the worktree merge-check verification.
- The
pre-commit-skipmarker for codemod-class waves (skips the lint stanza; unit-tier still runs). - The Commit-of
i/Ntrailer the hook requires on agent commits.
Related Patterns
- Layer — the first stair of the commit → cron → merge-train → deploy staircase; escalates into merge-train-mis-batching and staged-deploy-gates.
- Layer — sentinel-first-commit runs at the same commit-time stage, catching substrate failure where this catches content failure.
- See also (complement) — role-typed-dispatch: the commit path this hook guards consumes the role fixed at dispatch (the
commit-slaverole is shaped so the hook fires). - Counterpart — lifecycle-hooks: the other kind of hook. This one fires on a commit and guards what gets written; that one fires on a runtime lifecycle event (turn-stop, compaction, session-start) and guards what the operator's loop does.