Rules for agents
The hard rules a model must follow when generating opsinjs UI, written as prohibitions you can test rather than principles you can nod at.
The short version
Six rules. They are written as prohibitions because a prohibition is testable and a principle is not.
- 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.
- Status, not colour. Never express clinical urgency as a colour choice. Set a status level and let the system choose the colour.
- One axis per element. Category identity and clinical status never colour the same element.
- Never invent a component. If it is not in the catalogue, it does not exist, and saying so is the correct answer.
- Never assume a registry. Do not emit an install command for a registry namespace the project has not configured.
- 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
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
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: 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
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). 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.
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
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.
// 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
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.
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
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.
npx shadcn@latest add https://opsinjs.pensievelabs.org/r/status-pill.jsonOne 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
Do not emit raw colour values
// 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
// 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. 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 refuses
to render the mixed pair.
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
# Wrong, unless you have read components.json and seen @opsinjs in it.
npx shadcn@latest add @opsinjs/result-cardDo not soften a clinical message to make the copy nicer
Tone is governed by Voice and tone and
Writing status and alerts. "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
Never write "this meets WCAG AA" in a comment or a commit message. Conformance is measured, and the measurements live in Contrast conformance and the generated contrast tables.
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.
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
- Prompt recipes covers prompts that produce correct output, with the reasoning for each constraint.
- Evals shows how adherence to these rules is measured.
- Agent skill is the installable form of this page.
- Machine-readable schemas has the JSON an agent should resolve against instead of guessing.
- Clinical interaction guidelines is the human-facing version of the same prohibitions.