Agent-correctness primitives

Four architectural primitives that shrink the surface where an LLM can produce wrong-but-green output. Each makes a class of failure mode harder to express; together they aim to make the path of least resistance and the path of correctness the same path.

primitive 1

Derived obligations

The rule. If an output can be derived from the spec, codegen it: the agent writes the source, tools derive everything downstream. Each derivation layer is a place where incorrect output cannot be expressed, rather than a place the agent has to remember not to express it.

Where it runs here. Contract authors declare what must hold with check_file_exists, check_file_contains and the rest of the closed primitive set; _lib.sh derives the executable check, so a new contract is a fifteen-line check.sh and not a hand-rolled shell pipeline. A project's .gitlab-ci.yml names a catalog component (include: gitlab.com/dunn.dev/pipeline/go-release-binary@2.2.3) and the inputs that vary; three hundred lines of cross-compile matrix, signing and artifact handoff derive from the catalog, and a version bump derives a new job body without the project's source changing. This site's agent surface works the same way: the Markdown at <path>/index.md and the page list in /llms.txt are generated from the sources the browser pages render from, so no hand-curated copy exists to fall out of date. The routing bullets are the hand-kept exception, which is why the build resolves every target they name and fails on a dangling one.

Where we leave room for it. Adding a standard means dropping a directory with a CONTRACT.md and a check.sh: its page, its assertions table and its Markdown derive from that. The one hand-kept step is its row in the page list, and the build fails on a row whose target does not exist.

primitive 2

Prescriptive failure

The rule. When a check rejects an output it names the next action, not the problem. "Lift the avatar.svg template from repository/templates/" is actionable; "file missing" is a postmortem. The agent re-reads the error to produce its next attempt, so a message that names an action replaces a prompt somebody would otherwise write.

Where it runs here. Every assertion emits one line, PASS or FAIL, then <contract>/<assertion-id>, then a message:

FAIL repository/file-contains:README.md: README.md does not match /^!\[hero\]\(hero\.svg\)/

The failure prints the index into the remediation and the contract page carries the remediation: which template to lift, what string to insert, where it lives. The catalog rejects a bad input at pipeline-creation time, quoting the regex the value failed, so nobody waits for a runtime error to dig the cause out of. A .deviations.yaml that will not parse reports ERROR with the filename and yq's own stderr, then states the consequence: deviations are not applied this run, so every assertion below reports its raw verdict with WAIVED unavailable. Not "deviation register broken".

primitive 3

Bundled enforcement

The rule. Verification couples to value delivery: the only path to the artifact runs through the check. Opt-in enforcement, whether a separate lint job, a recommended pre-commit hook or a "remember to validate" note, gets bypassed under context pressure. Ask whether anyone can get the value without passing through verification.

Where it runs here. A project consuming reference-check runs it in the same pipeline as cloudflare-worker, so a conformance failure fails the pipeline and the site does not deploy; they are siblings because the catalog makes them siblings, not because the consumer remembered to wire them together. The reference's own just check runs just guard, the primitive-whitelist parser, so a check.sh calling an out-of-set command cannot ship past CI. go-release-binary and slsa-attest sign on every release tag with no opt-in flag: the job that uploads the artifact signs it.

Where advisory mode is right. Adopting a new standard against a legacy consumer, where the goal is surfacing findings rather than blocking merges that predate the check. reference-check takes fail_on_violation: false for it, and the trade is that every accepted gap carries a deviation-register entry with its reason. When the reason stops holding, the entry goes or the project re-conforms, and enforcement bundles again.

primitive 4

Vacuity detection

The rule. Catch checks that pass while doing no meaningful work. The failure mode is the test passes and the feature is broken; the cause is a pass condition satisfied vacuously by an empty array, an always-skip branch, a regex that matches anything. Ask whether the check can be satisfied without doing the work it exists to require.

Where it runs here. The repository contract does not assert check_file_exists README.md, which an empty file satisfies; it asserts check_file_contains README.md '^!\[hero\]\(hero\.svg\)', making the observation the file's structure rather than its presence. The pipeline contract does not look for an @ in .gitlab-ci.yml, which @latest would satisfy; it matches 'gitlab\.com/dunn\.dev/pipeline/[a-z][a-z0-9-]*@[0-9]+(\.[0-9]+)2([^0-9A-Za-z.-]|$)', a major-or-fuller version on a dunn.dev/pipeline component, and adds explicit check_file_lacks assertions for @latest and @main. The deviation register applies the same test to prose: an accepted gap is not "this is fine" but "this is fine because of a named, checkable fact about this project", and an entry without a reason is rejected at the boundary. That is what stops the register becoming a universal escape hatch.

Operational discipline

These did not come from agent-correctness research. They emerged from operating the dunn.dev catalog and shipping software in this estate, and they compose with the primitives because both reduce the surface where things drift.

catalog

Version pinning

The rule. Same source tree, same CI behaviour. Every reference to a catalog, image, tool or template sits at a strict version: no @latest, no @main, no mixed tags across includes. The version advances in commits, not silently between pipeline runs.

What it prevents. A floating ref makes the bug silent: the pipeline goes green at T and red at T+1 against the same commit because an upstream rolled forward, and finding that means diffing the catalog's commit log against the pipeline log and cross-referencing image hashes by hand. Pinned, the bug is loud. A deliberate bump to @2.2.3 either changes behaviour or does not, and the change sits in the commit.

Where it runs here. The pipeline standard enforces it on every consumer's .gitlab-ci.yml, and the catalog holds the same shape internally: each component declares image: registry.gitlab.com/dunn.dev/pipeline/ci:$[[ component.version ]], so a consumer pinned at @2.2.1 provably runs against ci:2.2.1 and tooling advances with contract or not at all. Tags from 2.0.0 on are unprefixed strict semver, which makes a mixed-style pin (@v1.x.y beside @2.x.y) advertise itself as the migration window it is. A worked instance of derived obligations: the pin is the spec, the job body is derived from it.

release

Byte-parity shipping

The rule. The binary you sign is the binary your container runs. Build once, ship twice: once as a signed, attested release artifact, once as a thin runtime layer (FROM ci-runtime-go + COPY dist/<binary>). The same file passes through both pipes.

What it prevents. Attestation means something only when the artifact assessed is the artifact shipped. SLSA, cosign, SBOM tooling and scanners each inspect an artifact; when the production one is a sibling of the inspected one, their verdicts are advisory at best. Signing an image attests the manifest digest and the binary inside stays an opaque blob to cosign, so "sign the release binary" and "sign the container image" are not substitutes. Byte parity makes both signatures refer to one SHA-256. The tell of the broken shape is a go build inside the Containerfile: two builds, two binaries, two attestations that do not reference each other, and a later investigation that has to reverse-engineer which commit produced what.

Where it runs here. The catalog's container-image component takes from_artifacts: true. The package job builds the binary, cosign-signs it and uploads it with its .sha256 sidecar; the downstream job copies that same artifact into ci-runtime-go, a 30 MB UBI-micro base. A worked instance of bundled enforcement: the job that uploads the binary is the job that signs it.

catalog

Minimal artifacts

The rule. A shared artifact is the smallest shape that does its job, then stops. The estate's runtime bases (ci-runtime-go, ci-runtime-node) are a FROM plus OCI labels: no speculative users, no Tini, no WORKDIR, no default ENTRYPOINT. They publish at the catalog's own semver so version coupling propagates cleanly to consumers.

What it prevents. Every speculative addition to a base lands in every consumer image automatically. A USER 1001 nobody asked for becomes a thing every consumer reasons about and overrides for the life of the artifact, and removing it later is harder than never adding it. The same holds for CI template inputs nobody requested and renovate.json customManagers watching what nobody pins. Defaults are not features.

review

Bounded complexity

The rule. Minimal is the default and complexity buys its way in, on one of three grounds. Use them as the gate when reviewing a diff or a proposal:

  1. Security. A trust boundary you can state in a sentence ("this token never leaves this listener"), an auditing path, or defense in depth against a named threat.
  2. Multi-tenant reuse. The flexibility unblocks at least two real consumers. One real plus one imagined is speculation, not reuse.
  3. Independent deploy lanes. One component ships without coordinating with another. Real in monorepos and shared catalogs, almost never inside a single binary.

Outside those three, remove it, ship the simpler thing, and add the hook back when the second consumer turns up. Real consumers teach you what shape to add; imagined ones teach nothing.

shape

Useful core first

The rule. Build the local-only thing that works by itself, then layer integrations on as flags, subcommands or separate components. bairn fetches Famly archives to local disk before any vendor-push subcommand exists; tutti writes evidence files to disk before the Astro site reads them. The CLI that runs and writes a file stays the gravitational center, and no integration is the only path through the core.

What it prevents. Integration shapes churn: vendors deprecate APIs, schedules shift, deploy targets get re-platformed. A core that depends on the integration inherits every one of those churns. It also kills the recurring failure mode of building the integration first to make the tool useful, then finding the local-only path needed for testing or offline work was never built.

fork posture

Shape for upstream

The rule. A change layered on an open-source project is shaped from day one as a plausible upstream contribution: configuration knobs and hook points rather than hardcoded branches, even when this estate is the only consumer. The test is whether the change could land upstream with cosmetic edits, or whether it carves out estate-specific paths. Hardcoded dunn.dev references become configuration defaults, hardcoded paths become arguments, hardcoded auth models become pluggable. Roughly 20 percent more design effort.

What it buys. The shaped change survives upstream rebases where a hardcoded fork accumulates conflicts. If upstream takes it, you stop being a fork maintainer and become a consumer of a feature. And the question "could this go upstream as-is?" applies a quality bar at write time, when restructuring is still cheap. Operational config (which DNS provider, which bucket) is exempt; behavioural extensions are not.

planning

Adversarial review

The rule. Any change planned to span more than about 500 lines, touch more than one repo, or run autonomously without operator intervention gets at least two adversarial passes before execution. First pass surfaces concerns, second probes the ones that look load-bearing; stop when a round returns little new signal.

What to scan for. Stale claims, meaning anything in the plan asserting the state of another file, repo or registry (re-verify, memory decays). Reversibility: what a half-failed step leaves behind, and whether rollback loses data. Estate consistency: whether this invents a pattern or follows one. Scope discipline: anything not load-bearing for the stated goal gets cut. Silent regressions: which unknown consumer could break, and where that would surface.

What it prevents. Autonomous runs do not stop to question, so a bad assumption compounds through every step that depends on it. A pass at plan time costs one conversation; catching the same thing mid-execution costs the run plus the recovery plus the rework.

debugging

Verify in isolation

The rule. Before claiming upstream is broken, reproduce the failure in a clean environment running only upstream's quickstart: temp directory, fresh clone, the documented init command, the smallest input that triggers it. If it reproduces, file the issue. If it does not, the bug is in your tree.

What it catches. Stale generated state, an old package-lock.json, a node_modules/ left from a previous version, a dist/ nobody regenerated; the fresh clone eliminates all of them at once. And sloppy migration, a version bump that missed a consumer site, where the symptom disappearing under a clean quickstart is itself the diagnosis. The framing matters too: "upstream is broken" carries an authority that "I have not reproduced this yet" does not. Get the reproduction before borrowing the authority.

Why these and not others

A principle earns a slot by meeting two tests: it shapes at least one executable standard on this site or explains why a standard has the shape it does, and losing it would visibly degrade outputs across multiple projects. Personal preference and one-project tactics stay out. The set grows when a new standard forces a new explanation; it shrinks when a principle is fully absorbed into a standard's check.sh and the prose is no longer load-bearing.