Audience: anyone reviewing a change in this repo — human reviewer, Claude Code session, code-reviewer subagent, or an author self-checking before opening a PR.
Status: Living document. Amend via PR. Ground reviews here, not in taste.
This rubric exists because Commonly is building toward a stable kernel-shaped platform with many replaceable drivers. A change that's "fine" in isolation can erode that property. The rubric gives every reviewer the same bar so drift doesn't accumulate.
In order, for any non-trivial review:
- Read
CLAUDE.md— product manifesto, architecture model, design rules, agent-runtime invariants. - List
docs/adr/ADR-*.md— at least know which ADRs exist. Read the ones that govern the change surface. - Skim relevant domain docs —
docs/COMMONLY_SCOPE.md,docs/SUMMARIZER_AND_AGENTS.md,docs/DISCORD_INTEGRATION_ARCHITECTURE.md,docs/POSTGRESQL_MIGRATION.md,docs/deployment/KUBERNETES.md,backend/TESTING.md,frontend/TESTING.md— whichever apply. - Read the surrounding code, not just the diff. A file's local conventions matter more than any global rule.
- Find the tests. If there aren't any, that's the first piece of review feedback.
- Open
docs/development/review-checklist.md— the incident-derived checklist, applied mid-review. This rubric sets the bar; that file holds the specific rules each earned by a defect that shipped. Its §7 (phantom cross-layer contract) is the one to run on any diff where one layer reads a value another layer promises in prose to provide: if a comment says another layer sets this, grep that the layer actually sets it.
If no ADR governs the change surface and the change is structural, flag it: "this probably needs an ADR before it merges".
From CLAUDE.md and the ADRs. If a change puts pressure on one, the author must justify — not the reviewer.
- Commonly is a rendezvous, not a runtime. Agents connect to Commonly; they don't run inside it. Any change that implies "Commonly runs the agent" breaks the thesis.
- Kernel first, shell second. Build runtime-agnostic kernel pieces; features visible to humans belong in the shell.
- Additive, not destructive. Add the new driver/feature next to the existing one. Never deprecate what works until the replacement is live.
- One runtime change = one adapter file. If changing runtimes requires edits in 3+ files that aren't the adapter, the abstraction is leaking.
sourceandcomponents[]are orthogonal, not 5 categories.@mentionand/commandare orthogonal addressing modes, not a partition. A component can declare both.- Install scope is first-class:
instance | pod | user | dm. - Identity continuity: uninstalling an Installable must NOT delete the User rows of its Agent components. Reinstall must find the old identity and memory intact.
- One envelope per
(agentName, instanceId). Identity from ADR-001 is the join key. - Private by default. Sections with
visibility: 'private'are never returned to non-owners. - Cross-agent primitive is messaging, not reads.
commonly_ask_agentbeforecommonly_read_shared_memory. Don't build silent peer-scraping. - Runtime-opaque schema. Kernel fields don't mention OpenClaw, LangGraph, or any driver.
- Kernel canonical under disaster. If local and kernel disagree after PVC loss, kernel wins.
- Bytes live in the
ObjectStoredriver, metadata lives on the parent entity. - GET must be authorized. A leaked URL from a private pod must not be publicly fetchable.
- Never
sparse: trueon a unique index that can see a storednull. Sparse skips missing fields only — an explicitnull(from a schemadefault: nullor an assignment) IS indexed, so the second such doc throws E11000. Compound sparse is worse: the doc is indexed if any key field is present, so{ podId, optionalField }collides on the second doc per pod even with the field absent. UsepartialFilterExpression: { field: { $type: 'string' } }instead. Two production bugs from this class in one day (2026-07-03):OAuthLoginState.exchangeCode(#581) andTask.sourceRef(#583). If you must keep sparse, the path must have no default and no code path may assignnull.
The test: can I delete this module and replace it with another implementation without touching its callers?
- One module, one concern. A provisioner provisions. A router routes. A service does one service-shaped thing.
- Interfaces before implementations. When a concept has 2+ variants now or planned (runtimes, object stores, drivers), the interface is the primary artifact.
ObjectStore(ADR-002) is the model. - Import direction flows one way. Routes → services → models. Kernel never imports shell. Shell imports kernel. Drivers import interfaces; interfaces don't import drivers.
- Tests cross module boundaries, not internal details. A refactor that preserves behavior should not break tests. If it does, the tests were coupled to internals.
- No circular imports, ever. If you need one, the modules are actually one module.
- An
if (type === 'openclaw') { ... } else if (type === 'webhook') { ... }scattered across 5 files. Collapse to a driver registry. - A service reaching into a model's private fields.
- A "utils" file growing over 500 lines with unrelated helpers. Split by concern or inline.
- Layer violations: a route pulling from the DB without a service, a service constructing Express
Requestobjects. - A test that passes only because it mocks a function the module calls. The mock is proof the module's interface is too narrow.
The test: can a new driver / component / scope / event type be added without editing anyone else's code?
- Open for extension, closed for modification. New variants plug in; existing code doesn't change to accommodate them.
- Extension points are registry-shaped, not switch-shaped. A map/registry lookup beats a chain of
if/else if. Drivers register themselves; callers ask the registry. - Manifest-declared, not hardcoded. Installables declare their capabilities/scopes/addressing in their manifest. The kernel reads the manifest; it doesn't embed per-package knowledge.
- Stable schemas are driver-opaque.
AgentMemoryEnvelope(ADR-003) doesn't nameopenclaw.Installable.components[]doesn't nameopenai. When a driver needs its own shape, it goes in a sub-object that the kernel treats as opaque. - Capability declarations at manifest-time, not install-time. Known at publish; permissions granted at install. OAuth model.
- Adding a new driver required editing a core kernel file (
registry.js, routes, models). Core shouldn't know about drivers by name. - A new addressing mode required a schema migration. Addressing was modeled as an enum instead of a declaration.
- A new scope required editing the permission check for every existing scope. Scopes weren't modeled.
- Parallel tables for "similar things from different sources" instead of one table with a
sourcediscriminator. ADR-001 rejected this.
The test: when the author leaves the team, can another engineer understand and change this within a reasonable read?
- Read-first orientation. Names carry meaning.
provisionAgent(agent)beatsp(a).ObjectStorebeatsStorage. - Short, linear functions over long clever ones. 20 lines is good; 200 is a red flag; nested callbacks are a bug.
- Errors propagate with context. Either handle it meaningfully or let it bubble. Never swallow silently.
catch (e) { console.warn(...) }is a silent swallow. - Migration paths are documented. Every schema change either ships a one-shot migration or declares a compatibility window. No "just change the model and hope."
- Observability lives where it's needed. A kernel endpoint without basic logging of auth decisions is unobservable in production.
- Delete more than you add when you can. A PR that replaces 40 lines with 20 and keeps behavior is a win.
- Dead code that's "maybe useful later." Delete it; git remembers.
- Variable names that disagree with what they hold (
const userList = user.friends). - A function that does 4 unrelated things because that's what the caller needed. Split and compose.
- Copy-paste across 3+ call sites of anything non-trivial. Extract, but only after 3rd occurrence (see below).
- Comments that describe WHAT (redundant with code) rather than WHY (non-obvious constraint).
Temporary workarounds become permanent. The codebase treats them as debt with interest.
- No
// TODO: remove this laterwithout a linked issue and a concrete removal condition. "When the upstream bug is fixed" without a link is not a condition. - No
// HACKas an accepted state. A// HACKcomment is a request for a root-cause fix before merge, not an artifact of it. - No parallel
_v2/_new/_legacycode paths both shipping. If both are live, the older one must have a removal date in this PR or be behind a feature flag gated to off. - No
if (env === 'dev') { workaround }in production code paths. If behavior must differ, it's configuration at the edge, not a conditional in the center. - No
--force,--skip-verify,--no-verify,--allow-unrelatedin committed scripts without an explanation comment naming the exact reason and why the standard approach fails. - No "I'll fix the test later". The test is part of the change or the change doesn't ship.
- No manual patches that mask a root cause. Editing a running k8s resource to unblock something is an emergency tool; the permanent fix goes in the same PR or the next.
- Any phrase in a comment like "temporary", "for now", "until X", "will be removed", "workaround" — demand issue link + removal condition or rewrite as the real fix.
- Any duplicated/renamed function suffixed
2,New,Old,Legacy,Fixed— demand a deletion plan. - Any production code path branching on
NODE_ENVfor behavior (not for config loading).
Three similar lines is better than a premature abstraction. Don't design for hypothetical futures.
- No abstraction for < 3 current users. Inline until the third caller exists. An interface with one implementation is accounting, not architecture.
- No "just in case" parameters. Every parameter must have a call site that uses it. Parameters without users are bugs pretending to be flexibility.
- No wrappers that add nothing but "consistency". A
db.findUser(...)that just callsUser.findOne(...)is weight. - No feature flags for backwards compatibility within code the project owns. Change the code. Flags are for risky rollouts of new externally-observable behavior.
- No generic systems for specific problems. A config-driven rule engine for one rule. A plugin system with one plugin. A strategy pattern with one strategy. All over-engineered.
- No speculative modeling. Adding a field "we might want later" is debt. Add it when we want it.
- No preserved-but-unused code. Don't leave a function behind "in case it's useful." Delete; git remembers.
- An interface or abstract class with exactly one implementation and no credible second one in flight.
- A new config option with no call site reading it.
- A helper function called exactly once.
- A refactor PR touching 30 files to "clean up" without a behavior change driving it.
- A bundled PR mixing a bug fix with a refactor; split unless the refactor is one-line obvious.
- Null/undefined handling at every boundary crossing. Type-safe doesn't mean runtime-safe when data comes from JSON/DB.
- Async races: concurrent calls, double-firing, out-of-order completion.
- Off-by-one, empty-input, max-size boundaries tested.
- The change does what the PR title/description claims. Scope creep is feedback.
- User-controlled data in a shell command, SQL string, path, HTML render → injection. Parameterize.
- Auth/scope/ownership checks on every endpoint. For agent-runtime routes:
req.agentUser?._idis derived (CLAUDE.md §Agent Runtime). - No secrets in logs, commits, response bodies, or env vars named to look benign.
- No custom crypto. No hardcoded keys. Token generation via CSPRNG.
- New code path ≠ test: flag it. Name the specific test that should exist.
- Test that mocks what it should run in-memory (backend/TESTING.md's pg-mem / MongoMemoryServer) or runs real where mocks would do: flag.
- Test asserts on output only but has important side effects (DB writes, event enqueues, external calls): flag.
- Test that fails when the setup adds "one more row" (fragile hard-coded counts): suggest resetting state or asserting on specific rows.
- Integration tests (
INTEGRATION_TEST=true) vs unit tests vs kind-cluster tests — is it in the right tier?
Output structure:
## Verdict
<Approve | Approve with suggestions | Request changes | Needs discussion>
## Critical
- path/file.ts:42 — one-sentence summary. Quote the offender. One sentence on direction of fix.
## Important
- ... same format ...
## Nits
- ... same format ...
## Questions
- ...
## What's good
- brief, skip if nothing stands out
Anchor every point to file:line. Quote the offending line. Say what's wrong AND where the fix should head. Don't complain without direction.
- Direct. Specific. Kind. A review reads like a thoughtful colleague, not a compliance bot.
- No sycophancy. Skip the opening "great work!" unless you're about to say what specifically was great.
- No bikeshedding. Style preferences without a repo convention go under Nits or get dropped entirely.
- Distinguish "this violates a stated invariant" from "I'd do it differently." Say which one you're invoking.
- Push back honestly. If the design is wrong, say so once, clearly. Don't soften to the point of ambiguity.
- Uncertainty is OK.
Needs discussionis a valid verdict. Better than a confident wrong call.
Sometimes the issue isn't "fix these lines" — it's "this needs a design discussion before any code lands."
Flag this and set verdict to Needs discussion when:
- The change implies a new kernel primitive (new endpoint shape, new data model touching multiple components). Ask for an ADR.
- The change crosses 3+ unrelated concerns in one PR. Ask for a split.
- The change contradicts a stated invariant and the justification isn't in the PR description. Ask for it in writing.
- The code reads as a workaround for an unstated problem upstream. Ask: what's the root cause, and is fixing it here the right place?
Two checklists, two moments: this one is for authors, pre-PR; docs/development/review-checklist.md is for reviewers, mid-PR. They do not overlap — don't substitute one for the other.
Before requesting review, answer each:
- Does CLAUDE.md's manifesto, architecture model, and design rules hold after my change?
- Does any invariant from an ADR get violated? If yes, is that intentional and documented in the PR description?
- Is there a test for every new code path? Is the test at the right layer?
- Any temporary workarounds, TODOs, HACKs, or parallel
_v2code? If so, what's the removal condition? - Any abstraction with fewer than 3 users? Any parameter without a call site? Any wrapper adding nothing?
- Any secrets in the diff? Any auth-gated route without auth?
- Does the PR title/description match what the diff does?
- Is this the smallest change that accomplishes the goal?
If you can say yes to all, the review will be faster and you'll ship cleaner.
This document is versioned; change it via PR like anything else. When an ADR lands a new load-bearing invariant, add a reference to its section here. When a pattern of review feedback repeats across PRs, encode it here so future reviews don't re-discover the same rule.