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 scanA rule matches the AST, not text — so it catches the real construct regardless of formatting. For example, the always-shipped repository rule:
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: errorWhich rules ship depends on the project shape:
| Rule | When | Catches |
|---|---|---|
| repo-catch-database-error | always | A repository catch that doesn’t rethrow ErrorFactory.databaseError |
| no-auth-tables-outside-auth | Drizzle + multiple services | user/session/account/verification tables declared outside auth |
| mobile-animated-native-driver | mobile present | Animated.timing/spring without useNativeDriver |
| mobile-no-direct-fetch | mobile present | A bare fetch() in presentation-layer components |
| mobile-no-hardcoded-color | mobile present | Hardcoded 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-tablesIt 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.