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).

Key Takeaway

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: funlen ceiling 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: cyclop ceiling at 10. Test files are exempt.
  • Cognitive complexity: revive’s cognitive-complexity and gocognit, both at 15. Test files are exempt.
  • All errors handled: errcheck is enabled; an unchecked error is a build error.
  • Security: gosec is 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% in scripts/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: govulncheck runs 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.

Tip

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-lines ceiling at 400 lines. Comments and blank lines excluded.
  • Function length: max-lines-per-function ceiling at 60 lines. Comments and blank lines excluded.
  • Complexity: complexity ceiling at 10.
  • No floating promises: @typescript-eslint/no-floating-promises is error. Every promise must be awaited or explicitly void-ed.
  • No unsafe anything: no-unsafe-argument, no-unsafe-assignment, no-unsafe-call, no-unsafe-member-access, no-unsafe-return all error. Type-erasure that escapes through any or unknown without 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
Warning

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:

  1. Beads JSONL flush. Auto-export and stage .beads/issues.jsonl so the committed state is current.
  2. lint-staged on packages/web. Runs Prettier on staged TS/JSON/YAML files, automatically fixing formatting.
  3. 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 when core.hooksPath is set. The bd integration in .githooks/pre-commit is 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-commit and .githooks/post-commit are 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 fix flow when the upgrade is patch-level and safe.
  • Override the transitive version in package.json if the direct dependency has not caught up yet.
Warning

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.

Key Takeaway

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.

Warning

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

  1. Go thresholds: funlen 60, file-length 350, cyclop 10, cognitive-complexity 15. errcheck, gosec, govulncheck all on.
  2. Go coverage: target 85%, currently enforced 83% (bead os-hjz). Update either when you change which number is real.
  3. TypeScript thresholds: max-lines 400, max-lines-per-function 60, complexity 10. Strict types; no floating promises; no unsafe-anything.
  4. Auto-fix first. just api fix and just web fix handle the bulk of lint complaints; reach for them before manual fixes.
  5. Hooks shared via core.hooksPath = .githooks/. Do not run bd hooks install or timbers hooks install; they write to .git/hooks/ and do nothing.
  6. Audit gates run on every check. Treat audit failures as dedicated beads, not as line items in unrelated feature work.
  7. Cardinal rules: no nolint without justification; fix the code, not the metric; comments do not count; hit the limit, extract.
  8. No --no-verify; no --amend after 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, --amend after 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?
  1. 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.
  2. The Workshop — The trail's tool map. The gates row is the deep read here.
  3. Just: One Place to Discover — The justfile cairn. just check, just fix, and per-package variants are the surface this cairn invokes throughout.
  4. 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.
  5. golangci-lint — Official documentation. The complete list of linters and configuration options that .golangci.yml draws from.
  6. typescript-eslint — Official documentation. The complete list of TypeScript-aware ESLint rules used in packages/web and packages/mcp.
  7. govulncheck — The Go vulnerability scanner that runs as part of just api check. Background reading when a CVE flag fires.
  8. audit-ci — The npm advisory gate that runs in just check-fast. Background reading when an audit failure fires.