Skip to content
Rules

Build rules an AI coding agent asks for before it acts.

A rules file grows until nothing in it is read. A rule here carries a trigger surface — file globs, operations, code patterns, languages, project attributes — and the agent asks, with the change it is about to make, which rules apply. Only the rules whose every populated dimension matches come back, most-retrieved first, and each one records that it was asked for. Rules are authored by hand or promoted from an enforced lesson through a database-checked gate; they retire forward-only, and every change lands on the audit chain.

query_applicable_rules

file_paths · operations · languages · project_attributes · task_context
A rule badge A pill reading rule/lock-order — the first rule returned, most retrieved. rule/lock-order A second rule badge A pill reading rule/index-hint — the second rule returned. rule/index-hint A third rule badge A pill reading rule/cross-key — the third rule returned. rule/cross-key

returned 3 · candidate_count 12 · suppressed_count 0 · illustrative

How it works

Ask. Author. Derive. Retire.

Four verbs. The first is the one this page is for; the other three are how a rule comes to exist and how it leaves. Each tool links to its row in the catalogue.

  1. Ask

    Before it touches a file the agent passes the file paths, operations, languages and project attributes of the change, and a line of task context. Only the project’s active rules whose every populated trigger dimension matches come back — most-retrieved first, up to a limit — with candidate_count and suppressed_count, so the caller knows what it did not see.

    query_applicable_rules
  2. Author

    A slug, a title, a body in markdown, and any of the five trigger arrays. The project comes from the call or from the caller’s open build session. The rule lands active, with its enrichment pending.

    create_rule
  3. Derive

    A lesson that has reached enforced — a state the database refuses without a written prevention mechanism — is promoted: the prevention mechanism becomes the rule’s body, the lesson’s applicability tags seed project_attributes, and source_lesson_id points back. A lesson that is not enforced is refused.

    derive_rule
  4. Retire

    active → archived, forward-only, enforced by a database trigger; an archived rule is never a candidate again. A mis-filed rule can be hard-deleted by an authorised credential; archiving is the retire-without-delete path.

    archive_rule

The problem is arithmetic. A conventions file is read whole, at the top of every session, whether or not a line applies to the change at hand — so every rule added costs attention on every change, and past a certain length the file is loaded and not read. Splitting it into more files moves the problem to the index. A rule that knows when it applies is the other shape: it is stored once, and returned only to the change it is about.

In the session protocol this is step two: the agent starts or resumes a build session, calls query_applicable_rules with the files, operations and languages it is about to change, does the work, and then writes each material decision to the decision log before it ends the session. Rules are advisory to the agent — PostMQ records that they were retrieved and shows which ones came back; enforcement stays in your CI. What the record gives you is the two numbers a rules file cannot: how many rules there were, and how many were ever asked for.

The trigger surface

Five dimensions, and one rule for reading them

A rule’s triggers are five string arrays, always present, stored and returned in one canonical key order. A change is described in the same five terms. Matching is globs, overlaps and substrings — deliberately nothing fuzzier.

The five trigger dimensions: what the rule declares, what the change passes, and how they match
on the rulefor exampleon the changematches when
file_patterns path globs — src/**/*.cs file_paths a glob matches one of the paths; **/ spans directories, a bare ** matches anything including separators, * stays inside a segment, ? is one character; \ and / both read as /; case-insensitive
operations create, modify, … operations any value in common (case-insensitive, trimmed)
languages csharp, sql, … languages any value in common (case-insensitive, trimmed)
project_attributes ef-migration, schema, … project_attributes any value in common (case-insensitive, trimmed); a derived rule starts with the lesson’s tags here
code_patterns UPDLOCK, ef migrations remove, … task_context the pattern appears as a substring of the task context (case-insensitive)

A rule applies to a change iff it has at least one populated dimension and every populated dimension is satisfied — AND across the populated ones, never OR. A rule that names only file_patterns matches any change touching such a path, whatever its language; a rule that names file_patterns and languages needs both. A rule with no populated dimension — pending enrichment, no hand-written triggers — is counted as a candidate and matches nothing until its triggers are set. Bounds are fixed at write time: 32 entries per dimension, 200 characters per entry, 8,000 characters serialized; the model’s output, when enrichment runs — it is off by default — passes the same bounds a person’s does.

How query_applicable_rules decides

The candidates are the project’s active rules — archived rules never apply — fetched most-retrieved first, at most 1,000, so the highest-signal rules are always among those considered. Each candidate is matched in memory against the described change; the matches are returned in that order, most-retrieved then newest, up to limit. The answer carries three counts: returned, candidate_count (how many active rules were considered) and suppressed_count (matched, but cut by the limit) — so a caller can tell an empty project from a limit set too low. limit defaults to 50 and must be 1–100 over REST and MCP alike; an out-of-range value is refused, not silently clamped, so the same call returns the same page on either transport. Then every returned rule has its retrieval_count incremented and last_retrieved_at stamped in one bulk UPDATE, reflected in the response — a telemetry touch that is deliberately not audited, and the ordering the next call’s candidates are fetched in.

Rule badge, rank one A pill reading rule/lock-order, the most-retrieved match. rule/lock-order Rule badge, rank two A pill reading rule/index-hint. rule/index-hint Rule badge, rank three A pill reading rule/cross-key. rule/cross-key

returned 3 · candidate_count 12 · suppressed_count 0 · illustrative

Three rules returned for one change, in retrieval order. The first has been asked for most; the counts under them say how many were considered and how many the limit cut.
API surface

Ten tools, eleven routes, three pages

The MCP tool, the versioned REST route under /v1 and the dashboard page — all three run the same service layer, and REST and MCP serialize the rule aggregate — rule, summary, list page, applicable wrapper with its counts — through one shared session-state serializer, tested byte-identical.

query_applicable_rules
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "query_applicable_rules",
    "arguments": {
      "project_id": "01K…",
      "file_paths": ["src/Accounts/CredentialIssueService.cs"],
      "operations": ["modify"],
      "languages": ["csharp"],
      "project_attributes": ["sql-server"],
      "task_context": "insert a credential under the account row lock",
      "limit": 20
    }
  }
}
REST
POST /v1/rules/applicable
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "project_id": "01K…",
  "file_paths": ["src/Accounts/CredentialIssueService.cs"],
  "operations": ["modify"],
  "languages": ["csharp"],
  "project_attributes": ["sql-server"],
  "task_context": "insert a credential under the account row lock",
  "limit": 20
}
Dashboard
app.postmq.com/applicable-rules        the same retrieval, replayed by a person: returned / candidates / suppressed
app.postmq.com/rules/{id}              each match: triggers, retrieval count, last retrieved
create_rule
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "create_rule",
    "arguments": {
      "rule_slug": "account-lock-before-credentials",
      "title": "Take the ai_accounts row lock before touching credentials",
      "body_markdown": "Any path that inserts a credentials row first takes an UPDLOCK on the parent ai_accounts row, in the same transaction, held to commit.",
      "file_patterns": ["src/**/Accounts/*.cs"],
      "operations": ["create", "modify"],
      "languages": ["csharp"],
      "code_patterns": ["credentials", "UPDLOCK"]
    }
  }
}
REST
POST /v1/rules
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "rule_slug": "account-lock-before-credentials",
  "title": "Take the ai_accounts row lock before touching credentials",
  "body_markdown": "Any path that inserts a credentials row first takes an UPDLOCK on the parent ai_accounts row, in the same transaction, held to commit.",
  "file_patterns": ["src/**/Accounts/*.cs"],
  "operations": ["create", "modify"],
  "languages": ["csharp"],
  "code_patterns": ["credentials", "UPDLOCK"]
}
Dashboard
app.postmq.com/rules                   Author a rule: project, slug, title, body, the five trigger fields
derive_rule
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "derive_rule",
    "arguments": {
      "source_lesson_id": "01K…",
      "rule_slug": "typed-params-under-lock-hints"
    }
  }
}
REST
POST /v1/rules/derive
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "source_lesson_id": "01K…",
  "rule_slug": "typed-params-under-lock-hints"
}
Dashboard
app.postmq.com/rules                   Derive from an enforced lesson: the picker lists only enforced lessons
app.postmq.com/rules/{id}              Source → Derived from lesson
update_rule
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "update_rule",
    "arguments": {
      "rule_id": "01K…",
      "body_markdown": "…and name the clustered index in the hint: WITH (UPDLOCK, ROWLOCK, INDEX(PK_ai_accounts)).",
      "replace_triggers": true,
      "file_patterns": ["src/**/Accounts/*.cs", "src/**/Migrations/*.cs"],
      "operations": ["create", "modify"],
      "languages": ["csharp"],
      "code_patterns": ["credentials", "UPDLOCK"]
    }
  }
}
REST
POST /v1/rules/01K…
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "body_markdown": "…and name the clustered index in the hint: WITH (UPDLOCK, ROWLOCK, INDEX(PK_ai_accounts)).",
  "triggers": {
    "file_patterns": ["src/**/Accounts/*.cs", "src/**/Migrations/*.cs"],
    "operations": ["create", "modify"],
    "languages": ["csharp"],
    "code_patterns": ["credentials", "UPDLOCK"]
  }
}
Dashboard
app.postmq.com/rules/{id}              Edit title and body; tick Replace triggers to overwrite the surface

The 10 MCP tools · from the server's own manifest

Build rules with a five-dimension trigger surface; query_applicable_rules returns only the ones that match the change at hand. PostMQ speaks standard MCP, so any MCP-capable client can call them; each name links to its row in the catalogue.

The rules MCP tools, from docs/mcp/tool-surface.json
toolwhat it doesneeds
query_applicable_rules Retrieve the build rules that apply to a change you're about to make. Pass the file_paths + operations + languages + project_attributes you're touching (and optional task_context); returns only the project's active rules whose triggers match, up to a limit, plus candidate_count and suppressed_count. Open to any authenticated caller. required: project_id authenticated
create_rule Author a new build rule by hand (no source lesson). Lands 'active'. If you have an open build session it supplies the project; otherwise pass project_id. The trigger arrays are what query_applicable_rules matches a change against. A slug already used in the project is a conflict. Requires the write_session_state scope. required: rule_slug, title, body_markdown write_session_state
derive_rule Derive a rule from an ENFORCED build lesson: the rule body becomes the lesson's prevention mechanism and its trigger surface seeds project_attributes from the lesson's applicable_to tags; the rule links back to the lesson. The lesson must be enforced (else conflict). Slug/title default to the lesson's. Requires the write_session_state scope. required: source_lesson_id write_session_state
get_rule Get a full build rule. Identify it either by rule_id, or by project_id + rule_slug (the per-project natural key). Open to any authenticated caller. authenticated
list_rules List build rules in lifecycle order (active first, newest first), offset-paged with a total. Optionally filter by project, one or more statuses (active/archived), one or more enrichment statuses (pending/enriched/failed), or a substring over title + body. Returns a lightweight summary shape. Open to any authenticated caller. authenticated
update_rule Edit a rule's title, body, and/or trigger surface (content only — use archive_rule for status). Omit a field to keep it. To change the triggers you MUST set replace_triggers=true — then the WHOLE surface is replaced with the trigger arrays you pass (all empty = cleared); the trigger arrays are IGNORED unless replace_triggers is true. An update that changes nothing is an idempotent no-op. Requires the write_session_state scope. required: rule_id write_session_state
archive_rule Archive a rule (active -> archived; forward-only, archived is absorbing). An already-archived rule is an idempotent no-op. Requires the write_session_state scope. required: rule_id write_session_state
delete_rule Hard-delete a mis-filed rule (archive is the retire-without-delete path). Requires the write_session_state scope. required: rule_id write_session_state
enrich_rule Run the LLM trigger-extraction for one rule whose enrichment_status is 'pending', writing the five-dimension trigger surface query_applicable_rules matches against. Returns a typed result: 'enriched', 'failed' (the model's answer was unusable — the rule is parked for a human reenrich), 'deferred' (no provider, or a transient failure — the rule stays pending and is retried), or 'skipped' (not a pending rule, or a concurrent writer changed it). Requires the write_session_state scope. required: rule_id write_session_state
reenrich_rule Requeue a rule whose trigger-extraction FAILED back to pending so the background extractor retries. Only valid on a failed-enrichment rule (else conflict). Requeuing does not itself call the model — run enrich_rule, or wait for the sweep, to retry the extraction. Requires the write_session_state scope. required: rule_id write_session_state

The 11 REST routes

Under the versioned /v1 API. Writes carry write_session_state; reads carry the default authenticated policy. get_rule answers both GET /v1/rules/{id} and GET /v1/rules/by-slug, which is why ten tools are eleven routes.

The rules REST routes
routewhat it doesneeds
POST /v1/rules author a rule (project from the body, or from the open build session) write_session_state
POST /v1/rules/derive derive a rule from an enforced lesson write_session_state
POST /v1/rules/{id} edit title, body, triggers (a present triggers object replaces the whole surface) write_session_state
POST /v1/rules/{id}/archive archive — active → archived, forward-only write_session_state
POST /v1/rules/{id}/reenrich requeue a failed enrichment (failed → pending) write_session_state
POST /v1/rules/{id}/enrich run one enrichment attempt now (deferred when no provider is configured) write_session_state
DELETE /v1/rules/{id} hard-delete a mis-filed rule write_session_state
GET /v1/rules list — active first, newest first; filter by project, status, enrichment status, substring authenticated
POST /v1/rules/applicable the retrieval: a JSON body describing the change, so a POST; same defaults and bounds as the tool authenticated
GET /v1/rules/by-slug one rule by project_id + rule_slug authenticated
GET /v1/rules/{id} one rule by id — body, triggers, enrichment, retrieval telemetry authenticated
Lifecycle and enrichment

Active, then archived; enriched only if you turn it on

Each card names the object it is made of. The status machine and the fixed columns are enforced by the database; the enrichment posture is a default you have to change.

Forward-only status

A rule is active from insert and can only move to archived. The trigger rejects any UPDATE that lowers status_rank — and there is no un-archive surface to reach it: archive is the only status write, and an archive of an archived rule is a no-op that writes no audit row. Archived rules are never candidates.

trigger:tg_build_rules_status_monotonic

Fixed at insert

The same trigger rejects any change to the identity, the per-project slug or the source lesson after insert. Title, body and triggers change; what the rule is and where it came from do not.

columns:rule_id · rule_slug · source_lesson_id

Update replaces triggers whole, on request

update_rule edits title and body under an update lock and keeps the trigger surface unless the call says otherwise — replace_triggers over MCP, a present triggers object over REST — and then replaces it as a whole. An update that changes nothing writes no audit row.

flag:replace_triggers

Enrichment, off by default

Optional, and off by default. Every rule lands pending; the enrichment sweep ships disabled and the LLM client is inert until your own Anthropic key is configured, so without a provider enrich_rule answers deferred and the row is untouched. A usable answer becomes enriched; an unusable one parks the rule failed with the reason, and only reenrich_rule requeues it.

options:LlmRuleEnrichmentSweepOptions

Delete is a hard delete

A mis-filed rule can be removed in any state by a credential holding write_session_state; archiving is the retire-without-delete path. The deletion itself is a row on the audit chain.

tool:delete_rule

8 events on the chain

created, derived, updated, archived, reenrich_requested, enriched, enrichment_failed, deleted — each written into the workspace's tamper-evident SHA-256 chain in the same transaction as the row it records; the retrieval bump is deliberately not audited.

family:build_rule.*
What people see

Three dashboard pages: the catalogue, a rule, and the replay

app.postmq.com/rules is the workspace catalogue — title, slug, status, enrichment status, whether the rule is manual or derived from a lesson, and its retrieval count — with a project filter, a status filter, an enrichment filter and a search over title and body, plus the two forms: author a rule (project, slug, title, body, the five trigger fields) and derive one from an enforced lesson (the picker lists only that project's enforced lessons). app.postmq.com/rules/{id} shows the fields, the body, the triggers by dimension, the last enrichment error if there is one, and the actions: edit with a Replace triggers checkbox, requeue enrichment (offered only while the rule is failed), archive behind a confirmation, delete behind another; an archived rule reads read-only but keeps delete. app.postmq.com/applicable-rules is the replay: describe a change in the same five terms and see the same retrieval an agent runs — returned, candidates, suppressed — with each match linking to its rule. The overview page carries a Rules tile with the active count and the last time any rule was retrieved.

Three honest limits. The catalogue shows the first 100 rules that match the filters and asks you to narrow them rather than paging further; a replay is a retrieval and bumps retrieval_count exactly as an agent's call would; and the replay's limit field allows up to 200 where the tool and the route stop at 100.

Every create, derive, update, archive, requeue and delete, and every enrichment that lands — enriched or failed — is also a row on the workspace's audit chain (a no-op update or archive, and a deferred enrichment, write none) — 8 of the session-state lifecycle event types written into the same tamper-evident SHA-256 chain as messaging, 38 of 123 event types in all (high-frequency touches such as pings, usage reports and retrieval-count bumps are deliberately not audited). A workspace can browse that chain in the dashboard and read it over REST and MCP, with the operator-significant families withheld; the mechanism is on the security page.

Guarantees and limits

What holds, how, and what is default-off

A mechanism per row; the badge says whether it ships on or ships off. One row here is default-off — enrichment — and it says so; everything else ships on.

Rules guarantees and limits: what holds, the mechanism, and its scope
what holdshowscope
A rule applies only when every populated dimension matchesC09 five dimensions — file_patterns, operations, code_patterns, languages, project_attributes; a rule with at least one populated dimension applies iff each populated one is satisfied by the change; a rule with none matches nothing shipped
Globs, overlaps and substrings — nothing fuzzierC68 file_patterns are path globs (**/, a bare **, *, ?; \ and / both read as /); operations, languages and project_attributes need one value in common; code_patterns are substrings of task_context — all case-insensitive; 32 entries per dimension, 200 characters each, 8,000 serialized shipped
Only active rules are candidates; the answer says how much it did not showC69the dashboard replay allows a limit up to 200 the project’s active rules, most-retrieved first (at most 1,000), matched in memory; returned = matches up to limit, plus candidate_count and suppressed_count; limit defaults to 50 and must be 1–100 over REST and MCP — out of range is refused, not clamped shipped
Every returned rule counts the retrievalC10a telemetry touch — deliberately not audited, like pings and usage reports retrieval_count + 1 and last_retrieved_at stamped in one bulk UPDATE, reflected in the response; the counter ranks the next call’s candidates shipped
A person can replay the exact retrievalC11a replay is a retrieval: it bumps retrieval_count like an agent’s call would /applicable-rules runs the same QueryApplicableRulesAsync an agent’s call runs, with the same three counts shipped
A rule derives only from an enforced lessonC12the gate is structural, the human is convention — any write_session_state credential can enforce a lesson; mis-filed lessons and rules can be hard-deleted derive_rule reads the lesson under an update lock inside the insert transaction and refuses one that is not enforced; enforced itself needs a written prevention mechanism (a database constraint); lesson and rule status move forward only shipped
What a derive copiesC72 the lesson’s prevention mechanism becomes the body, its applicability tags seed project_attributes, slug and title default to the lesson’s, source_lesson_id links back, the project is inherited; a slug already used in the project is a conflict shipped
active → archived, never back; id, slug and source lesson fixed at insertC71 tg_build_rules_status_monotonic rejects any UPDATE that lowers status_rank or touches workspace_id, rule_id, rule_slug, source_lesson_id; there is no un-archive surface — archive is the only status write, and an archive of an archived rule is a no-op that writes no audit row shipped
Update replaces the trigger surface only when told toC71 update_rule edits title, body and triggers under an update lock; the surface is replaced as a whole and only with replace_triggers (MCP) or a present triggers object (REST); an update that changes nothing writes no audit row shipped
Enrichment is optional and off by defaultC70until it runs, a rule matches on the triggers a person wrote or a derive seeded — with none, it matches nothing every rule lands pending; the enrichment sweep ships disabled and the LLM client is inert until your own Anthropic key is configured — without a provider enrich_rule answers deferred and the row is untouched; a usable answer becomes enriched, an unusable one parks the rule failed with the reason, and only reenrich_rule (failed → pending) requeues it default-off
8 events on the audit chainC73 build_rule.created, build_rule.derived, build_rule.updated, build_rule.archived, build_rule.reenrich_requested, build_rule.enriched, build_rule.enrichment_failed, build_rule.deleted — each written into the workspace’s tamper-evident SHA-256 chain in the same transaction as the row; no-op updates and archives write none shipped
Writes need one scope; reads need only authenticationC74 the seven writes require write_session_state (operational tier) over REST and MCP alike; the three reads — list, get, the retrieval — require an authenticated caller in the workspace shipped
REST and MCP return the same bytesC05 one serializer for the rule aggregate — rule, summary, list page, applicable wrapper with its counts — pinned by byte-parity tests covering all ten operations shipped

Where rules come from is the other half of the story: recurring pain is filed as a lesson, a lesson moves forward only — observed, documented, enforced — and enforced is refused by the database until someone writes down the prevention mechanism. derive_rule is the step from that lesson to the rule the next session is handed. The ladder, the gate and the candidate queue are on the lessons page; the tools are in the catalogue; the protocol they sit in is on the session-state page.

Questions

Six things people ask

No. It stores rules and returns the applicable subset. A rules file is read whole, every session, whether or not a line applies to the change at hand; a rule here carries a trigger surface, and query_applicable_rules returns only the rules whose every populated dimension the change satisfies. Keep the short conventions file your client reads; put the rules that only matter for some changes here, and let the agent ask.

By promotion through a gate. A lesson is filed when a mistake recurs and moves forward only — observed, documented, enforced — and the database refuses enforced without a written prevention mechanism. derive_rule accepts only an enforced lesson: it reads the lesson under an update lock inside the same transaction that inserts the rule, copies the prevention mechanism into the rule’s body, seeds project_attributes from the lesson’s applicability tags, and links the rule back with source_lesson_id. A rule can also be authored by hand with create_rule; either way it lands active.

The call succeeds with an empty rules array and honest counts: candidate_count says how many active rules were considered (up to the 1,000-rule candidate cap), suppressed_count is zero, returned is zero. Nothing is bumped. A rule whose trigger surface is still empty — pending enrichment, no hand-written triggers, a derive from a lesson with no tags — is counted as a candidate but never matches; the dashboard’s replay page shows the same three numbers so a person can see why.

Edited, yes: update_rule changes the title, the body and — only when the call says so — the whole trigger surface, under an update lock, and an edit that changes nothing writes no audit row. Retired, yes, once: archive_rule moves it active → archived and a database trigger refuses the way back; an archived rule is never a candidate again. Deleted, yes: delete_rule hard-removes a mis-filed rule in any state, and every one of these writes is a row on the audit chain. What can never change after insert are the id, the slug and the source lesson.

Not by default. Every rule lands with enrichment_status = pending. The enrichment sweep ships disabled, the deployment template’s switch defaults to off, and the LLM client is inert until your own Anthropic key is configured — so a workspace that never turns it on matches rules on the triggers people wrote and derives seeded, and enrich_rule answers deferred. When it is on, the model’s answer is validated by the same bounds a hand-authored surface passes; the model is called with no SQL transaction open, and the result is applied in a short transaction that re-reads the row under an update lock and checks that no concurrent reenrich or edit has superseded the attempt. A bad answer parks the rule failed with the reason for a person to requeue with reenrich_rule.

Yes. The ten operations are eleven REST routes under /v1/rules (get_rule answers both /{id} and /by-slug), with the same scope rule and one shared serializer — the responses are pinned byte-identical to the tools’ — and three dashboard pages: the catalogue with its author and derive forms, a rule’s detail with edit, archive, requeue and delete, and the replay page that runs the same retrieval an agent runs. MCP is the transport an agent uses; REST is for scripts and integrations.

Last verified 2026-08-18 against main at 6d28b69: the matcher, the service, the trigger, the ten tools, the eleven routes, the three dashboard pages and the enrichment defaults this page names were read on that day.

Related: lessons — the ladder a rule is promoted from · session state — the protocol this is step two of · the decision log — what the agent writes after it acts · the rules tools in the catalogue · the audit chain every write lands on · connect an agent · pricing — rules and the retrieval are unlimited on every tier.

Write the rule once. Let the agent ask.

Create a workspace, connect your agent, call query_applicable_rules before the first file.