---
title: "Rules for agents"
description: "The hard rules a model must follow when generating opsinjs UI, written as prohibitions you can test rather than principles you can nod at."
url: "https://opsinjs.pensievelabs.org/agents/rules-for-agents"
source: "https://opsinjs.pensievelabs.org/agents/rules-for-agents.md"
section: "Agents & automation"
kind: "handbook"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["agent rules", "system prompt", "do not invent", "generation rules"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="handbook" />

## The short version [#the-short-version]

Six rules. They are written as prohibitions because a prohibition is testable
and a principle is not.

1. **Tokens, not values.** Never emit a hex colour, an rgb value, a pixel radius
   or a millisecond duration. Emit a token or a utility that resolves to one.
2. **Status, not colour.** Never express clinical urgency as a colour choice.
   Set a status level and let the system choose the colour.
3. **One axis per element.** Category identity and clinical status never colour
   the same element.
4. **Never invent a component.** If it is not in the catalogue, it does not
   exist, and saying so is the correct answer.
5. **Never assume a registry.** Do not emit an install command for a registry
   namespace the project has not configured.
6. **Never invent a number.** No contrast ratio, no threshold, no reference
   range, no bundle size, unless it came from a generated source you actually
   read.

Everything below is the reasoning, the failure each rule prevents, and how to
check whether an agent is following it.

Copy-paste form: these six rules are distributed as the four files in
`skills/opsinjs/rules/` and are what the [agent skill](./agent-skill.mdx)
installs. Appearance rules 1 to 3 are split across `tokens-not-values.md` and
`status-and-colour.md`, invention rules 4 and 6 are in `never-invent.md`, and
rule 5 is in `registry.md`. Prefer installing the skill over pasting prose,
because the skill also reads the project's own configuration.

## How it works [#how-it-works]

An agent generating UI makes three kinds of decision, and only one of them is
safe to make from general knowledge.

**Composition** is the decision about which elements go where. General
knowledge is fine.

**Appearance** is the decision about what colour, what spacing, what radius.
General knowledge is actively harmful here, because the model's prior is "a
warning is amber" and opsinjs' answer is "a warning is a status level whose
amber is measured, tuned per theme, escalated on wide-gamut displays, and
always accompanied by a word". The model's answer looks identical and is wrong
in every way that matters.

**Clinical meaning** is what this screen asserts about a person. General
knowledge is dangerous. A model that infers a threshold is practising medicine
without a licence and without a chart.

The rules exist to push decisions out of the second and third categories and
into a lookup. Every one of them has the same shape: *do not decide, resolve*.

The mechanism that makes resolution possible is the machine surface described in
[Agents & automation](./index.mdx): the catalogue, the registry, the markdown
twins and the generated tables all answer questions that a model would otherwise
answer from its prior.

## Do this [#do-this]

### Resolve a component before using it [#resolve-a-component-before-using-it]

Before emitting `import { RangeBar } from …`, check the catalogue. Also check
`/r/index.json`, which answers the same question in a field rather than in
prose. Every id resolves. All sixty carry `"implemented": true` and
`"status": "shipped"`: the registry item
carries its source, and the right output installs it from the registry this
project has configured and says what `shipped` costs. Every opsinjs component
has been audited against WCAG 2.2 AA by its own authors, in a static source pass
and a rendered pass, but no component has had an independent accessibility
review or a clinical review (see
[ADR 0025](../project/decisions/0025-the-audit-is-author-run.mdx)). `shipped`
means the source installs, and it does not mean an independent review or a
clinical review has happened. Nothing here is for a production health surface. Were an id ever `"implemented": false` at
`"status": "planned"`, it would be a specification with no code, and the right
output would be a refusal that names the status and stops.

What is wrong for either is an import from a package. There is no published
opsinjs package, so `@opsinjs/react` never resolves and an install that copies
source into the project is the only shape that works.

```txt
RangeBar is implemented in opsinjs, at status: shipped. Its source is served as
a registry item and is copied into the project rather than imported from a
package. Shipped means the API may change in any release. The component has been
audited against WCAG 2.2 AA by its authors, but it has had no independent review
and no clinical review, so read the installed source instead of guessing the
props, and do not put it on a production health surface.
```

### Use the status vocabulary [#use-the-status-vocabulary]

Four ordinal levels, token ids `steady`, `watch`, `attention`, `urgent`. Set the
level; never pick the colour. And never render a status without a word. Colour
is a second channel, not the only one. `StatusPill` takes no children: it draws
the word for the level itself, `describes` names what the level is about so a
screen-reader user hears a subject, and `label` only re-words the level for
translation.

```tsx
// Right: the level is data, the word and the colour are the system's business.
<StatusPill status="watch" describes="HbA1c result" />

// Wrong: a colour decision, an invented prop, and no word.
<StatusPill color="#f59e0b" />
```

### Say "I do not know" about thresholds [#say-i-do-not-know-about-thresholds]

If a prompt asks you to decide whether a reading is high, the answer is that
opsinjs does not own thresholds and neither do you. Reference ranges belong to
the product's clinical logic. See
[Reference ranges](../health/reference-ranges.mdx).

### Cite the page you used [#cite-the-page-you-used]

When you assert something about opsinjs, name the page. It is verifiable, and it
makes a hallucination obvious rather than plausible.

### Prefer a full URL for an install [#prefer-a-full-url-for-an-install]

A full registry URL resolves in any project. A namespace only works in a project
that has configured it, and the failure is a confusing error rather than a
missing dependency.

```bash
npx shadcn@latest add https://opsinjs.pensievelabs.org/r/status-pill.json
```

One exception, and it is worth stating in the same breath: a component that
composes others names them namespaced. Twenty-one items carry `@opsinjs/…`
entries in `registryDependencies`, and `result-card`, `range-bar`,
`metric-tile` and `consent-sheet` are among them. Installing one of those needs
the `@opsinjs` block in `components.json` even when you gave the full URL, so
say so up front rather than letting the install fail halfway through.

## Not this [#not-this]

### Do not emit raw colour values [#do-not-emit-raw-colour-values]

```tsx
// Wrong.
<div style={{ background: "#fef3c7", borderColor: "#f59e0b" }} />

// Right.
<div className="bg-status-watch-surface border-status-watch" />
```

A hex value bypasses dark mode, the Display-P3 escalation, the increased-contrast
path and every contrast measurement CI runs. It will look correct in exactly one
theme on exactly one display.

### Do not mix the two axes [#do-not-mix-the-two-axes]

```tsx
// Wrong: the tile is both a category and a verdict, in colour, twice. The
// background line would reach past the bridge for a category surface value and
// paint it under a status tile. The value is left as a note, not a token, so
// this block cannot be pasted into the tint the rule forbids.
<MetricTile
  category="heart"
  status="urgent"
  style={{ background: "/* a category surface value goes here, and must not */" }}
/>

// Right: identity from the glyph, verdict from the surface.
<MetricTile category="heart" status="urgent" />
```

The rule and its rationale are in
[The two colour axes](../health/two-colour-axes.mdx). opsinjs ships no Tailwind
utility that tints a card by category, and the wrong tile above has to reach
past the bridge for a raw category surface value to break the rule at all. That
withdrawal is deliberate. A category surface and a status surface at the same
ladder step measure Lc 0.00 and WCAG 1.00 against each other, so a heart card
and an urgent card would be the same colour to a reader. Category identity
travels by glyph, ink and accent, and the surface is left to status. The
interactive demonstration at [`/playground/status`](/playground/status) refuses
to render the mixed pair.

### Do not invent a component, a prop or a variant [#do-not-invent-a-component-a-prop-or-a-variant]

If a name is only in the catalogue at `planned`, it has no implementation and no
API. No component sits there today, but the moment one does, inventing an API for
it produces code that compiles against nothing and a reviewer who believes the
feature exists.

### Do not assume a registry is configured [#do-not-assume-a-registry-is-configured]

```bash
# Wrong, unless you have read components.json and seen @opsinjs in it.
npx shadcn@latest add @opsinjs/result-card
```

### Do not soften a clinical message to make the copy nicer [#do-not-soften-a-clinical-message-to-make-the-copy-nicer]

Tone is governed by [Voice and tone](../content/voice-and-tone.mdx) and
[Writing status and alerts](../content/writing-status-and-alerts.mdx). "Nothing
to worry about" is not a friendlier version of a `watch` status; it is a
different clinical claim.

### Do not generate an accessibility claim [#do-not-generate-an-accessibility-claim]

Never write "this meets WCAG AA" in a comment or a commit message. Conformance
is measured, and the measurements live in
[Contrast conformance](../accessibility/contrast-conformance.mdx) and the
generated contrast tables.

## Gotchas [#gotchas]

**A `shipped` API reference is real and still moving.** An implemented
component page carries its prop interface under **API reference**, generated
from the same source the registry serves, so it is accurate on the day you read
it and carries no promise past that: a `shipped` API may change in any release.
Read the source you installed rather than a cached interface, and record
the commit you took it from, because that commit is the only record a later
upgrade can be measured against.

**A resolved id is not a built id.** Getting an answer is not evidence that
something exists. Read the `status` field. `shipped` means code exists and the
API may change. It means the code has been audited against WCAG 2.2 AA by its
authors, and it does not mean an independent review or a clinical review has
happened. `planned` means a specification and
nothing to install. Every id resolves today, and the field is still the thing to
read.

**The docs chrome is not the product theme.** Class names and colours visible in
this site's HTML belong to a shadcn style used for documentation. Copying them
into a patient-facing app imports the wrong aesthetic and the wrong touch-target
floor. See
[Lyra and the docs chrome](../theming/lyra-and-the-docs-chrome.mdx).

**Anchors are not stable.** Link to a page. Headings get edited.

**Aliases are search synonyms, not names.** `lab result` finds `result-card`. It
is not an importable identifier.

**The token id and the word on screen are not the same string.** `attention`
renders as "Needs attention", and the fifth stem `unknown` renders as "Not known";
the other three happen to coincide, which is what makes the two easy to conflate.
The words are fixed in `lib/status.ts` as `CLINICAL_STATUS_META[level].word`
and are read from there rather than typed, so substituting a synonym is not a
copy tweak. It changes what the interface asserts.

## Related [#related]

* [Prompt recipes](./prompt-recipes.mdx) covers prompts that produce correct
  output, with the reasoning for each constraint.
* [Evals](./evals.mdx) shows how adherence to these rules is measured.
* [Agent skill](./agent-skill.mdx) is the installable form of this page.
* [Machine-readable schemas](./machine-readable-schemas.mdx) has the JSON an
  agent should resolve against instead of guessing.
* [Clinical interaction guidelines](../health/clinical-interaction-guidelines.mdx)
  is the human-facing version of the same prohibitions.
