Standing rules
A standing rule is a current instruction that governs behavior. It is short enough to check without interpretation, hung on a trigger, and able to leave force with a reason.
It is not a memory and not a fact.
- A memory is something that happened or was said — recalled, associated, importance-weighted, Hebbian-linked. Measured on prod before this shipped: 494 of ~2,600 memories were type Instruction — a subsystem wearing a memory's clothes. Narratives are the right form for a lesson and the wrong form for a yes/no check at a decision point.
- A fact is a key-value claim with optional validity windows. Facts answer "what is true"; rules answer "what binds now."
- A standing rule is distilled from memories and links back to them. The narrative stays; the checkable assertion is separate. Without the story a rule is CLAUDE.md again. Without the assertion you re-extract the lesson under momentum and miss it.
All endpoints require authentication. Responses are wrapped in ApiEnvelope<T>. Canonical API host: https://api.atamaia.ai.
Why triggers exist
Without a trigger, every rule is evaluated on every action. That is wallpaper: a payload full of always-on instructions that nobody believes when they fire. Triggers narrow the moment so a reader seeing a rule fire has reason to trust it.
| Trigger | When |
|---|---|
SessionStart |
Hydration at session start (lightweight win: short assertions instead of re-deriving from narrative) |
BeforeDeploy |
Before shipping to production |
BeforeSchemaChange |
Before migrations, renames, shape changes to stored data |
BeforeSecurityChange |
Before auth, encryption, permissions, or tenancy code |
BeforeProdWrite |
Before production data writes, bulk updates, backfills |
BeforeClaimingDone |
Before closing a task or reporting work complete |
BeforeRecording |
Before writing a memory, fact, or handoff |
Always |
In force at every check — use sparingly |
Always is value 99 and is included whenever you call check for any other trigger. Three Always rules live on prod today (ids 3, 5, 10). A store full of Always rules recreates the flooding problem this design avoids.
Triggers are a C# enum added by migration, not SPA-editable. Help and the runtime resolve enums from typeof(TEnum), not from free-form lookup rows — a UI-added trigger would be invisible to code that branches on it. Adding a trigger is a deliberate code change.
Status — the load-bearing field
| Status | Meaning |
|---|---|
Active |
In force. Appears in check and (for SessionStart/Always) in hydration. |
Proposed |
Distilled but not confirmed. Does not surface. Does not govern. |
Superseded |
Replaced by a newer rule named in supersededById. Row and reason kept. |
Withdrawn |
Deliberately revoked and not replaced. Distinct from Superseded. |
Withdrawn vs Superseded are different facts.
Superseded is "we decided something else instead." Withdrawn is "we decided this was wrong." Conflating them loses the reason. Memories could not do this: both rulings stayed findable and both looked current (#245 — ".md files are canonical" 2026-07-21 vs "write docs into the DB by default" 2026-08-11).
There is no DELETE on this surface. A rule leaves force by withdraw or supersede. Deleting would recreate the hole: a ruling that silently stops existing.
Kind
| Kind | Role |
|---|---|
Practice |
Do this — prefer where the rule can be written that way |
Prohibition |
Never do this — sometimes right; often a Practice not yet found |
Preference |
Style/defaults a good reason can override |
The store tends to accumulate hazards faster than recipes. Kind makes the imbalance visible: "40 prohibitions and 3 practices" is diagnosable. Write assertions in positive, actionable form where the rule allows — "Test the constraint before reporting it" beats "do not report an untested constraint."
Grounding and evidence — not Hebbian links
A rule links to memories on two poles (RuleLinkPolarity):
| Pole | Meaning |
|---|---|
Grounding |
The incident that made the rule necessary — why it exists |
Evidence |
An occasion it was followed and worked — why it is worth keeping |
These are deliberately not Hebbian links. Hebbian links carry strength and co-activation count and strengthen with use — associative memory. A rule's grounding is a claim about provenance. It must not decay because nobody recalled it lately, and must not strengthen because two memories were read together. The link is either true or it is not: no strength, no co-activation.
Detail and hydration return titles and notes only, never memory bodies. A rule that drags six narratives into the payload is the flooding problem again. Full story is one memory_get away.
A rule with grounding and zero evidence after many sessions is a candidate for rewrite (wrong form) or withdrawal (wrong rule) — not silent eternity. Live prod: most Active rules have groundingCount=1, evidenceCount=0 (new store; evidence half does not write itself).
Check — the in-force set for the moment of action
GET /api/standing-rules/check?trigger={RuleTrigger}&identityId={optional}
MCP: rule_check.
Returns: Active rules whose trigger matches or is Always. Tenant-wide rules (identityId null on the row) always included; pass identityId to add that identity's scoped rules. Ordered shortest assertion first.
Does not return: Proposed, Withdrawn, Superseded.
Side effect: increments surfacedCount and sets lastSurfacedAtUtc on every returned rule. A rule that never surfaces has a trigger nobody hits — a fact about the rule, not the reader.
Live probes (2026-08-13):
| Call | Result |
|---|---|
rule_check SessionStart |
3 rules — all Always (no dedicated SessionStart Active rules yet) |
rule_check BeforeDeploy |
4 rules — 3 Always + id 8 (Deploy the consumer before removing the producer) |
Hydration
GET /api/hydrate includes standingRules when the StandingRules source is enabled (default presets include it).
Hydration loads Active rules with trigger SessionStart or Always only — not the full check vocabulary. Each item:
| Field | Content |
|---|---|
id, assertion, rationale, kind |
The rule |
grounding |
List of { memoryId, title, note } |
evidenceCount |
Count only — not evidence titles in the lean payload |
Live lean hydrate for ash (2026-08-13): three rules (ids 5, 10, 3), each with one grounding pointer and evidenceCount: 0. Proposed id 12 does not appear.
Propose is not activate
Machine-written rules enter as Proposed. They do not surface in hydration and do not appear in check until a human confirms them.
Why: auto-extraction from narrative inverted a decision (#3115). The agent is often the one who just learned why a rule is needed — so it must be able to draft. Enacting is a person.
POST /api/standing-rules/{id}/confirm → Proposed becomes Active
MCP rule_propose always creates with Status = Proposed. REST POST /api/standing-rules accepts a status on the body (including Active) — the human/API path can create already-active rules; the agent path cannot skip confirmation.
Live: rule 12 is Proposed (Update the affected documentation as part of finishing a task…), surfacedCount: 0, absent from check and hydrate.
Withdraw
POST /api/standing-rules/{id}/withdraw
{ "reason": "required string", "supersededById": null }
| Body | Resulting status |
|---|---|
reason only |
Withdrawn |
reason + supersededById |
Superseded, link to replacement |
Reason is required at the service layer. Self-supersede and missing replacement id are rejected.
MCP: rule_withdraw.
CRUD surface
GET /api/standing-rules
GET /api/standing-rules/{id}
GET /api/standing-rules/check
POST /api/standing-rules
PUT /api/standing-rules/{id}
POST /api/standing-rules/{id}/confirm
POST /api/standing-rules/{id}/withdraw
POST /api/standing-rules/{id}/links
DELETE /api/standing-rules/{id}/links/{memoryId}
List
Optional: identityId (includes tenant-wide when set), status, trigger, paging, sort, filter bag.
When identityId is set, results are that identity's rules plus tenant-wide — asking "what governs Ash" and getting only Ash-specific rows would look complete and be wrong.
Create body
| Field | Notes |
|---|---|
assertion |
Required; max 500; trimmed |
rationale |
Why it exists — the cost of not having it |
trigger, kind, status |
Enums |
identityId, projectId |
Optional scope |
links |
Optional initial grounding/evidence |
Link body
{ "memoryId": 3125, "polarity": "Grounding", "note": "one line" }
Idempotent on (rule, memory): re-post updates polarity/note (a pole change is a real correction).
MCP tools
| Tool | Role |
|---|---|
rule_check |
In-force set for a trigger |
rule_list |
Browse/audit including history |
rule_get |
Detail with grounding + evidence poles |
rule_propose |
Create as Proposed |
rule_add_evidence |
Link memory as Evidence |
rule_withdraw |
Out of force with reason |
REST-only relative to this MCP set: update metadata, confirm, unlink, create-as-Active.
Scope
| Scope | Meaning |
|---|---|
identityId null |
Tenant-wide — governs everyone |
identityId set |
That identity only (plus tenant-wide when listing/checking with that id) |
projectId |
Optional confinement on the entity; check/hydration filter by identity today |
Identity-scoped reads go through the same identity-access guard as other identity surfaces (#401/#458): hierarchy may oversee; only the owner writes.
What this replaces (and what it does not)
Replaces: treating Instruction memories as the standing-order mechanism; hoping two contradictory memories will sort themselves out; stuffing more lines into CLAUDE.md.
Does not replace: the memory store. Grounding is a memory. The narrative is where the WHY lives. Rules without sources become bare commandments.
Does not yet: SPA confirm/withdraw UI (#463). Evidence accumulation is manual (rule_add_evidence) — nothing auto-writes the positive half.
StandingRuleDto fields (list / check)
| Field | Notes |
|---|---|
id, guid |
D3 |
assertion, rationale |
|
trigger, kind, status |
enum names on the wire |
identityId, identityName, projectId |
scope |
supersededById, statusReason, statusChangedAtUtc |
leave-force trail |
groundingCount, evidenceCount |
pole counts |
surfacedCount, lastSurfacedAtUtc |
usage signal |
createdAtUtc, updatedAtUtc |
Detail adds grounding[], evidence[] (titles/notes), supersededByAssertion.