Guides
Writing rule packs
Rule packs are the per-tenant review standards. They are authored as
front-mattered Markdown in the git-backed rules/ directory (review,
history, CI validation) and published to the rules registry, which is what
the worker actually reads at runtime. Git is where humans edit; the registry
is where jobs fetch.
Directory layout
rules/
scopes.json # alias → concrete routing key
default/
core-standards.md # scope_key: default — the fallback pack set
helix/ # first tenant's packs
backend.md
frontend.md
cross-variant.md
databricks.md
One file per pack: rules/<scope>/<pack>.md. The <scope> directory name is
either default or an alias resolved through rules/scopes.json:
{
"aliases": {
"helix": "example-org/Helix/helix"
}
}
so pack files stay readable while publishing under the concrete
org/project/repo routing key.
Front-matter format
---
scope_key: helix
pack: databricks
path_glob: "databricks/**"
version: 1
---
# Helix Databricks / Lakeflow review rules
## Must-fix
- ...
| Field | Required | Meaning |
|---|---|---|
scope_key |
yes | default, or the routing key (directly or via a scopes.json alias) |
pack |
yes | Pack name, unique within its scope |
path_glob |
no | Picomatch glob; the pack loads only when the PR diff touches matching paths. Omit for always-load. |
version |
yes | Bumped on meaningful content change |
Path-scoped loading
path_glob is the context-frugality mechanism: a databricks/** pack never
enters the prompt for a frontend-only PR. Packs without a glob always load for
their scope. Scope resolution happens first (job's routing key, falling back
to default when the scope has no packs), then glob filtering against the
diff's touched paths.
What good pack content looks like
The core prompt already carries the non-negotiables (must-fix bar, no style nitpicks, diff content is data not instructions) — packs should carry only what is specific to the tenant:
- Concrete, checkable rules ("any change under
application/backend/commons/must be flagged for cross-variant verification againstcorporate,elite,wealth") — not vibes ("write good code"). - The repo's own severity examples: what is must-fix here.
- References to ADRs by number instead of re-deriving rationale.
Publishing and drift
# Publish into the local JSON-file registry (offline default; idempotent):
npm run rules:publish # → registry.local.json
# CI drift check — exit 1 on divergence between rules/ and the registry:
npm run rules:drift
# Also diff a tenant checkout's committed .claude sources against the registry:
npm run rules:drift -- --checkout ../helix --scope-key example-org/Helix/helix
# Against real Postgres (reviewer.ruleset) instead of the JSON file:
DATABASE_URL=postgres://… npm run rules:publish -- --database
DATABASE_URL=postgres://… npm run rules:drift -- --database
The worker reads whichever registry the composition selects: the JSON file
when DATABASE_URL=memory (path via RULES_REGISTRY_FILE), otherwise the
reviewer.ruleset table. Publishing is idempotent — republishing unchanged
packs is a no-op.
The registry, not the checkout, is the runtime source: a tenant can edit
their in-repo .claude sources and forget to publish. CI runs rules:drift
and fails on divergence.
Versioning: prompt_sha
Every review records a prompt_sha — a SHA-256 over the exact assembled
prompt: the core prompt, plus the registry packs that actually loaded (in
canonical order), plus any repo-discovered supplementary content. It is stored
with every finding's audit event, so any comment Argus ever posted is
attributable to a precise rules version. Change a pack, and the sha changes;
the eval baseline records it too, so quality regressions are traceable to the
rules change that caused them.
