Quality Gates: The Contract That Lets You Move Fast
Lint, types, tests, coverage, audits — the deterministic constraints that make agent-driven work safe to ship · ~21 min read ~– min read · Suggested by Bob engineer
When N agents are editing in parallel, taste alone cannot be the contract. The contract is the gates — lint, types, tests, coverage, vulnerability scans. Inside those gates, the agents are free; outside them, the build fails. This cairn covers Strike's exact thresholds, the pre-commit hook that ties them together, and the cardinal rules that keep contributors from working around the gates.
The argument, in one paragraph
Subjective preferences fade when the gate is deterministic. “Functions should be short” is a preference; funlen 60 is a build error. “Errors should be handled” is a preference; errcheck is a build error. The agents are free inside the gates; the build fails outside them; humans review what the gates cannot check — architecture, naming, the question of whether this change should have happened at all.
Inside the gates, agent moves are cheap because the safety net is automatic. Outside the gates, every agent move costs a human reading the diff line by line.
Why the limits are stricter than the human-team norm
A reasonable reaction to function 60, file 350, complexity 10 is that’s tighter than any team I’ve worked on enforced. That is correct. SRP alone does not stop an agent from producing a 2,000-line file with twelve tangled responsibilities; hard caps force the extraction conversation, and extracting is where the real boundaries become visible. Tight caps also keep files small enough that two worktrees on adjacent concerns do not collide, and small enough that pulling one into context does not eat the budget for the actual change. Prettier and the no-compression rule are paired by design — the shortcut of jamming two lines into one does not survive the next commit, so the only stable fix is structural.
Go gates
Go work in packages/api runs through just api check — formatting, linting, race-tests, coverage, and a vulnerability scan. The thresholds from .golangci.yml: funlen 60, file-length 350, cyclop 10, cognitive-complexity 15, with test files exempt. errcheck, gosec, and govulncheck are all on; any unchecked error, security warning, or CVE fails the build. Coverage targets 85% and is currently enforced at 83% while a package is restored (bead os-hjz) — when documentation and enforcement disagree, update one. Reach for just api fix first: it auto-fixes ~80% of typical complaints, and a hand-fixing loop on something the auto-fixer would have handled is always a redirect.
TypeScript gates
TypeScript work in packages/web and packages/mcp runs through just web check and just mcp check — codegen, typecheck, ESLint, tests, audit-ci, and (for web) the build. ESLint thresholds: max-lines 400, max-lines-per-function 60, complexity 10, with skipComments: true and skipBlankLines: true. Strict TypeScript is non-negotiable: strict: true, noUncheckedIndexedAccess: true, no-floating-promises as error, every no-unsafe-* as error, no explicit any without justification. Auto-fix first via just web fix and just mcp fix.
Comments do not count toward line limits. Removing or shortening comments to satisfy a length cap is one of the easiest unhealthy reactions to a lint failure, and it is exactly the wrong move.
The pre-commit hook
The hook at .githooks/pre-commit does three things in order: flush and stage the beads JSONL, run lint-staged Prettier on packages/web, and gate on timbers ledger state. It is shared via core.hooksPath = .githooks/, set by just hooks — not installed into .git/hooks/. Two commands are forbidden because they write to the wrong place and silently do nothing: bd hooks install and timbers hooks install. If you need to change the hook, edit .githooks/<name> directly and commit.
Audit gates
audit-ci runs in just web check and just mcp check, scanning the npm tree for high-or-higher advisories on production deps; any hit fails the build. The fix is almost always upgrading the direct dependency that pulls in the vulnerable transitive version, or overriding it when the direct dependency has not caught up. Treat audit failures as their own bead on a dedicated branch — burying a security upgrade inside an unrelated feature commit makes both harder to revert. The Go side has govulncheck inside just api check with the same shape. The iterate verb is just check-fast (lint, type, unit, plus just mcp check so MCP audit hits surface there); the pre-push verb is just check, which adds the slow web audit and the integration suite.
Cardinal rules
A handful of rules are non-negotiable. No nolint without a justification comment — every directive carries a one-line reason immediately above it; Strike’s 100+ existing directives all have real ones. Fix the code, not the metric — when you hit a length cap, extract; do not compress, collapse formatting, or shorten variable names, because Prettier will undo it on the next commit. Comments do not count toward limits, by design — never shorten or remove them to pass a gate. Hit the limit, extract — a file at 350 or a function at 60 is a signal that something is overgrown; the limits drive the architecture, and the architecture is the point.
The gates do not exist to be satisfied. They exist to drive a particular shape of architecture: small, focused, decomposable, testable. Code that games the gates gets all the friction and none of the benefit.
Why the agent should never bypass
Two specific bypasses are forbidden. git commit --no-verify is forbidden — skipping the hook means the beads JSONL flush does not happen, lint-staged does not run, and the timbers gate does not fire, which sets up out-of-sync state and undocumented commits downstream. git commit --amend after a hook failure is dangerous — when the hook blocks a commit, the commit did not happen, so --amend rewrites the previous commit instead of landing the fix. The right pattern: fix the failure, re-stage, create a new commit. Both bypasses appear without project-specific guidance; both are recoverable when caught and expensive when not.
- How We Build Here — The trail's opening cairn. The "deterministic constraints over taste" argument is operationalized here.
- Just: One Place to Discover — The justfile cairn.
just check,just fix, and per-package variants are the surface invoked throughout. - golangci-lint — The complete list of linters and configuration options that
.golangci.ymldraws from. - typescript-eslint — The complete list of TypeScript-aware ESLint rules used in
packages/webandpackages/mcp. - audit-ci — The npm advisory gate that runs in
just check-fast. Background reading when an audit failure fires.
If you have read How We Build Here, you have already met the argument: when a senior engineer plus a competent agent can move through several screens of code in the time it used to take to write one, the human-review-as-trust-mechanism breaks. Reading every diff line by line at that throughput is unsafe even when you intend to. The gates are the trust mechanism that lets you move that fast safely.
This cairn is the operating manual. The exact thresholds, the precise commands, the specific rules. It is the most pedantic cairn in the trail on purpose — when you are working with the gates day-to-day, “approximately right” is not enough. The numbers below are real, the commands are real, and the cardinal rules at the bottom are the ones we are not flexible about.
The argument, in one paragraph
Subjective preferences fade when the gate is deterministic. “Functions should be short” is a preference; funlen 60 is a build error. “Errors should be handled” is a preference; errcheck is a build error. “Security matters” is a preference; gosec is a build error. Outcome-oriented, not code-oriented — restated from How We Build Here. The agents are free inside the gates; the build fails outside them; humans review what the gates cannot check (architecture, naming, the question of whether this change should have happened at all).
Inside the gates, agent moves are cheap because the safety net is automatic. Outside the gates, every agent move costs a human reading the diff line by line. The gates are the difference between a workflow that scales and one that does not.
Why the limits are stricter than the human-team norm
A reasonable reaction to “function 60, file 350, complexity 10” is that’s tighter than any team I’ve worked on enforced. That is correct, and the reasoning matters.
SRP is not enough on its own. Tell an agent “follow Single Responsibility” and you will still see a 2,000-line file appear with twelve responsibilities tangled together. The argument inside the agent is something like “these all feel related,” and without a deterministic limit, that argument wins. Hard caps force the conversation: when a function exceeds 60 lines, the agent has to extract — and extracting is the moment the real responsibility boundary becomes visible.
Giant files punish parallel work. A 2,000-line file is a 2,000-line context cost every time the agent reads it, and a near-guaranteed merge conflict every time two streams of work touch it. Tight caps keep files small enough that two agents on two worktrees can edit adjacent concerns without colliding, and small enough that pulling one into context does not consume the budget the agent needs for the actual change.
Prettier and the no-compression rule are paired by design. The obvious shortcut for “satisfy a line cap” is to compress two lines into one. Prettier expands the result on the next commit, so the shortcut does not survive. The cardinal rule says it explicitly: hit the limit, extract — do not compress. The combination of strict limits and aggressive auto-formatting is what closes that escape hatch.
Excluding comments and whitespace from the count is deliberate. It removes any incentive to delete documentation to pass the gate. The agent gets a continuing reason to write why in comments, both for the next agent and for the human reviewer. Stripping comments to satisfy a length cap is the wrong move twice — it does not fix the structural problem, and it deletes the explanation that would have helped the next reader.
These tools were built for human teams; they pay more dividends now. Before the trail’s author tightened limits to their current values, agent-generated files were full of shortcuts, copy-paste, and inconsistent style — code that humans found unpleasant to maintain, that occasionally was not safe in production, and that was unpredictable enough to undermine the rest of the system. Strict limits, applied uniformly, fixed all three categories. The tools predate agent-assisted development; the leverage they provide is dramatically higher under it.
Strict beats subjective. Human teams with strong opinions can almost never agree to enforce caps this tight without months of argument and the inevitable “but this case is special” exceptions. Agent code has no momentum and no ego; once the rule is written down, it is enforced uniformly and forever, which is exactly what makes deterministic constraints work as a trust mechanism.
Go gates
Go work in packages/api runs through just api check. That recipe chains formatting, linting, tests with race detection, coverage enforcement, and a vulnerability scan. The thresholds, all from .golangci.yml:
- Function length:
funlenceiling at 60 lines. Test files are exempt because table-driven tests naturally run long. - File length:
file-length-limit(revive) ceiling at 350 lines for production code. Test files are exempt. - Cyclomatic complexity:
cyclopceiling at 10. Test files are exempt. - Cognitive complexity: revive’s
cognitive-complexityandgocognit, both at 15. Test files are exempt. - All errors handled:
errcheckis enabled; an unchecked error is a build error. - Security:
gosecis enabled; security warnings fail the build. - Coverage: target 85%. Currently enforced at 83% while a specific package (dispatched-contact) is restored — see bead
os-hjz. The target stays 85%; the floor temporarily rests one point lower while a real cleanup finishes.When you see “85% in CLAUDE.md” and “83% inscripts/coverage-check.sh” you are seeing exactly that gap. Update either if you change which is real; do not let the documentation and the enforcement drift. - Vulnerability scan:
govulncheckruns on every check. CVEs in the dependency tree fail the build.
The recipes the agent runs on your behalf:
just api fix # auto-fix lint, formatting, imports — agent's first move
just api check # full gate: fmt, lint, test, coverage, audit
just api fix chains golangci-lint --fix with goimports and the formatter. The agent should reach for it before manually editing to satisfy a lint complaint — most issues fix themselves, and the savings show up as fewer turns and less wasted reasoning. If you ever see the agent get into a hand-fixing loop on something auto-fix would have handled, that is a redirect.
Auto-fix is real. just api fix handles roughly 80% of typical lint complaints — formatting, imports, simple style fixes. The 20% that need human or agent reasoning are the ones worth attention. The agent should always try fix before manual work.
TypeScript gates
TypeScript work in packages/web and packages/mcp runs through just web check and just mcp check. These chain codegen, typecheck, ESLint, tests, audit-ci, and (for web) the build. End-to-end tests live in a separate just web e2e recipe (Playwright) that is not part of check. Strict-everything across the board. The ESLint thresholds, all from the package configs:
- File length:
max-linesceiling at 400 lines. Comments and blank lines excluded. - Function length:
max-lines-per-functionceiling at 60 lines. Comments and blank lines excluded. - Complexity:
complexityceiling at 10. - No floating promises:
@typescript-eslint/no-floating-promisesiserror. Every promise must be awaited or explicitlyvoid-ed. - No unsafe anything:
no-unsafe-argument,no-unsafe-assignment,no-unsafe-call,no-unsafe-member-access,no-unsafe-returnallerror. Type-erasure that escapes throughanyorunknownwithout narrowing fails the build. - No explicit any: Permitted only with explicit justification.
- Strict TypeScript:
strict: true,noUncheckedIndexedAccess: true, the lot.
The recipe shape:
just web fix # auto-fix lint and Prettier
just web check # codegen + typecheck + lint + test + build + audit-ci
just mcp fix # auto-fix lint and Prettier
just mcp check # codegen + typecheck + lint + test + audit-ci
Comments do not count toward line limits. ESLint’s skipComments: true and skipBlankLines: true are explicit in the config. This rule is in the cardinal rules below for a reason: removing or shortening comments to satisfy a length cap is one of the easiest unhealthy reactions to a lint failure, and it is exactly the wrong move.
The pre-commit hook
The hook at .githooks/pre-commit is the load-bearing piece of automation that makes the gates happen on every commit. Strike shares hooks via core.hooksPath = .githooks/, set by just hooks (which is part of just setup). Read the file directly — it is short and worth understanding. The order:
- Beads JSONL flush. Auto-export and stage
.beads/issues.jsonlso the committed state is current. - lint-staged on
packages/web. Runs Prettier on staged TS/JSON/YAML files, automatically fixing formatting. - Timbers pre-commit gate. Checks ledger state; non-zero exit blocks the commit.
The hook is shared via core.hooksPath, not installed into .git/hooks/. This matters for two specific commands the team has agreed not to run:
- Do not run
bd hooks install. It writes to.git/hooks/, which git ignores whencore.hooksPathis set. The bd integration in.githooks/pre-commitis already there; touching.git/hooks/will silently put a duplicate in a place that does nothing. - Do not run
timbers hooks install. Same reason. The timbers blocks in.githooks/pre-commitand.githooks/post-commitare already there.
If you need to change the hook, edit .githooks/<name> directly and commit. The next contributor’s just hooks (or their existing core.hooksPath) will pick up the change. No global setup, no per-machine drift.Some agents will reach for bd hooks install or timbers hooks install on a freshly cloned repo because that is what the upstream README suggests. Correct this when you see it. The team’s pattern is project-shared hooks via core.hooksPath, and the upstream READMEs do not know that.
Audit gates
audit-ci runs as part of just web check and just mcp check (the slow check). It scans the npm dependency tree for advisories at high severity or higher (production deps only), and any hit fails the build. This catches CVEs in transitive dependencies — packages your code does not import directly but that your direct dependencies pull in. The fast check (just check-fast) skips the web audit but still runs just mcp check, so MCP audit hits do show up there.
When audit-ci fails, the cause is almost always a transitive dependency that has shipped a fix. The fix on our side is one of:
- Upgrade the direct dependency that pulls in the vulnerable transitive version.
- Use the
npm audit fixflow when the upgrade is patch-level and safe. - Override the transitive version in
package.jsonif the direct dependency has not caught up yet.
Treat audit failures as their own work item — open a bead, fix it on a dedicated branch, do not bury it inside an unrelated feature commit. Two reasons: the fix touches dependency files that are noisy in review, and a security upgrade landing inside a feature PR makes both harder to revert if either turns out to be wrong.
The Go side has govulncheck running inside just api check (covered above). Same idea: vulnerable Go modules fail the build; the fix is a separate, focused upgrade.
Cardinal rules
A handful of rules are non-negotiable. They exist because each one names a failure mode that has cost the team real time, and codifying them is cheaper than re-discovering them every quarter.
No nolint without a justification comment. A //nolint: directive without an inline reason is a build error in spirit, even when it compiles. Every disable carries an explanation of why the rule does not apply here, in the form of a one-line comment immediately above the directive. The Strike codebase has 100+ nolint directives; every single one was reviewed and has a real reason. The nilnil interface contract for errcheck, the wrapcheck exception for test mocks, the bodyclose exception for tests — these are the canonical cases. New ones get the same scrutiny.
Fix the code, not the metric. When you hit a length cap, extract — do not compress. Splitting a 75-line function into a 40-line caller and a 35-line helper is the correct response. Removing whitespace, collapsing readable formatting, shortening descriptive variable names, or inlining helpers to dodge the rule are all wrong responses. Prettier will expand collapsed lines back anyway; the only stable fix is structural.
Comments do not count toward limits. Both the Go (revive file-length-limit) and TypeScript (max-lines, max-lines-per-function with skipComments: true) configs exclude comments from line counts. Never shorten or remove comments to satisfy a limit. The comments are doing useful work; removing them is removing the explanation, not the complexity.
Hit the limit, extract. A file at 350 lines is a signal that something inside it is overgrown — extract a logical section into a companion file (order-service.ts becomes order-service.ts plus order-validation.ts, order-transforms.ts). A function at 60 lines is a signal that it is doing too many things — extract named helpers. The limits drive the architecture; the architecture is the point.
The gates do not exist to be satisfied. They exist to drive a particular shape of architecture: small, focused, decomposable, testable. Code that satisfies the gates by gaming them gets all the friction and none of the benefit.
Why the agent should never bypass
Two specific bypasses are forbidden, and naming them in advance saves arguments later. They apply to the agent and to any human who finds themselves typing git directly.
git commit --no-verify is forbidden. Skipping the pre-commit hook means the beads JSONL flush does not happen, lint-staged does not run, and the timbers gate does not fire. A commit that lands without those three things sets up real damage downstream — out-of-sync beads state, unformatted code in the repo, undocumented commits the next session has to reconstruct. If you see the agent reach for the flag, redirect.
git commit --amend after a hook failure is dangerous. When the pre-commit hook blocks a commit, the commit did not happen — the previous commit is still the most recent one. Running --amend then modifies the previous commit instead of landing the fix as a new commit. This is one of the most common ways agents (and humans) accidentally rewrite the wrong commit. The right pattern: fix the hook failure, re-stage, create a new commit. Linear history, clear blame, no surprises.
If a pre-commit hook fails, do not –amend. Address the failure, re-stage, and create a new commit. The result is a clean second commit that does what you wanted; the alternative quietly modifies a commit you may not have wanted to touch. The agent will sometimes reach for –amend here on its own; redirect when you see it.
Both patterns will appear without project-specific guidance, and both are recoverable when caught and expensive when not. The catch is partly your habit (or your agent’s), partly Claude Code’s auto-mode classifier when it is in play, and partly review at the PR layer — none of those is foolproof on its own, which is why all three matter.
Summary
- Go thresholds:
funlen60, file-length 350,cyclop10, cognitive-complexity 15.errcheck,gosec,govulncheckall on. - Go coverage: target 85%, currently enforced 83% (bead
os-hjz). Update either when you change which number is real. - TypeScript thresholds:
max-lines400,max-lines-per-function60,complexity10. Strict types; no floating promises; no unsafe-anything. - Auto-fix first.
just api fixandjust web fixhandle the bulk of lint complaints; reach for them before manual fixes. - Hooks shared via
core.hooksPath = .githooks/. Do not runbd hooks installortimbers hooks install; they write to.git/hooks/and do nothing. - Audit gates run on every check. Treat audit failures as dedicated beads, not as line items in unrelated feature work.
- Cardinal rules: no
nolintwithout justification; fix the code, not the metric; comments do not count; hit the limit, extract. - No
--no-verify; no--amendafter hook failure. Fix, restage, new commit.
- The 83-vs-85 coverage situation is a textbook example of "documented standard" drifting from "enforced floor." If you had to write the rule that prevents that drift in the future, what would it look like — automated reconciliation, a CI check, a quarterly audit?
- The "comments don't count" rule is universally agreed-on but rarely written down explicitly. What other behavioral rules around the gates do you find the team executing reliably without a formal rule, and which of those should we promote to written-down status?
- The forbidden bypasses (
--no-verify,--amendafter hook failure) are forbidden because they land bad state. There is also a third pattern — quietly disabling a lint rule for "this one file" — that has the same effect over time. How would you catch and correct that one before it becomes a habit?
- How We Build Here — The trail's opening cairn. The "deterministic constraints over taste" argument is restated and operationalized in this cairn; read together for the philosophical and the mechanical halves.
- The Workshop — The trail's tool map. The gates row is the deep read here.
- Just: One Place to Discover — The justfile cairn.
just check,just fix, and per-package variants are the surface this cairn invokes throughout. - Your Box and Your Trust Model — The trust-model cairn. Auto-allow lists for
just *recipes and the sandbox semantics around git operations both connect to this cairn's bypass discussion. - golangci-lint — Official documentation. The complete list of linters and configuration options that
.golangci.ymldraws from. - typescript-eslint — Official documentation. The complete list of TypeScript-aware ESLint rules used in
packages/webandpackages/mcp. - govulncheck — The Go vulnerability scanner that runs as part of
just api check. Background reading when a CVE flag fires. - audit-ci — The npm advisory gate that runs in
just check-fast. Background reading when an audit failure fires.
Generated by Cairns · Agent-powered with Claude