Enforcement

Enforcement is where the harness stops suggesting and starts checking. It covers the mechanically-verifiable subset of the conventions — the rules a parser can confirm — and fails on a violation instead of hoping the agent read the docs.

ast-grep rules

stackr generates structural lint rules as ast-grep patterns under .stackr/sg-rules/*.yml, registered by a root sgconfig.yml. Run them with:

npm run lint:sg   # ast-grep scan

A rule matches the AST, not text — so it catches the real construct regardless of formatting. For example, the always-shipped repository rule:

.stackr/sg-rules/repo-catch-database-error.yml
id: repo-catch-database-error
language: TypeScript
files:
- "**/backend/domain/**/repository.ts"
rule:
kind: catch_clause
not:
  has:
    stopBy: end
    pattern: throw ErrorFactory.databaseError($$$)
message: "Repository catch blocks must throw ErrorFactory.databaseError(...)."
severity: error

Which rules ship depends on the project shape:

RuleWhenCatches
repo-catch-database-erroralwaysA repository catch that doesn’t rethrow ErrorFactory.databaseError
no-auth-tables-outside-authDrizzle + multiple servicesuser/session/account/verification tables declared outside auth
mobile-animated-native-drivermobile presentAnimated.timing/spring without useNativeDriver
mobile-no-direct-fetchmobile presentA bare fetch() in presentation-layer components
mobile-no-hardcoded-colormobile presentHardcoded hex colors instead of theme tokens

The files: globs are generated from the actual service paths, so no-auth-tables-outside-auth lists your real non-auth services precisely rather than using a fuzzy ignore.

The auth-table check

ast-grep covers the Drizzle path for the auth-table boundary; for Prisma, the same invariant is enforced by a script:

npm run check:auth-tables

It parses each non-auth service's Prisma schema and fails if it declares an identity table. Either way, the "only auth owns identity" rule is enforceable in CI.

The PostToolUse hook

When Claude Code is a selected tool, stackr installs a PostToolUse hook at .claude/hooks/check-edited.mjs (wired in .claude/settings.json). After Claude edits a file, the hook lints it and, on a violation, exits non-zero so the agent sees the error and self-repairs in the same turn — a tight check-and-reinject loop.

Honest scope of the hook

The generated hook runs backend ESLint against the nearest eslint.config.mjs. Two honest limits: it no-ops on mobile edits (the mobile subsystem uses expo lint with no flat ESLint config — the three mobile ast-grep rules gate at lint:sg/CI instead), and it no-ops before dependencies are installed. It is a real-time backstop for the backend, not a universal gate.

Put it in CI

Because the rules are files, not a service, they run anywhere:

npm run lint:sg            # structural conventions (ast-grep)
npm run check:auth-tables  # auth-table boundary (Prisma)

Adding these to CI turns "we have conventions" into "violations fail the build." Pair them with stackr doctor to also fail on artifact drift.