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.