Skip to main content
sourceSource: corpus/docs/reference/checks.mdModified: 2026-06-23

Checks — common mistakes to check for

Works today — plain markdown plus your agent; no corpus tooling required.

This page is corpus's check catalogue, in four parts: the core checks every spec should pass (either form), the evidence checks for task and review packets, the writing-rules watchlist, and the SOL check catalogue for specs that opt into structured requirements (format: sol). Reviewers use it as a checklist; tools build against it.

Reference implementation: corpus check in corpus-cli. Wherever this page says a check is toolable, that is the tool meant. This page is the contract — what a correct checker reports; the reference CLI's design + boundary (and the still-partial format: sol routing) is the CLI reference; its live command set is the CLI's own catalogue.

The honesty legend

Nothing in this repository runs, so nothing on this page is enforced. Every rule here carries one of four levels — read them literally:

LevelWhat it means
conventionExpected practice. Nothing enforces it.
checklistA reviewer is expected to inspect it during review.
toolableA tool can check it mechanically — here, corpus check in corpus-cli.
enforcedA shipped tool actually rejects violations. Today, nothing qualifies.

Approved phrasings, used throughout corpus's docs:

"This is a convention — nothing in this repository enforces it."

"A future check should flag this; until then, treat it as a review checklist item."

A team may adopt any check as blocking by policy — that is the team's gate, not corpus's.

Teams write their own checks beyond this generic core. Where those belong, and how to name them so they don't claim more than they prove, is in local checks and the extension boundary.

Core checks (any spec, either form)

These apply to every spec, whether it uses plain ### AC-NNN requirements or SOL blocks. Both forms encode the same requirement record, so each check means the same thing in both — the C ↔ SOL mapping below shows the correspondence.

Level: checklist today; toolable — corpus check implements exactly this table. The Checker severity column is the contract for that tool (see the severity split), not a claim that anything blocks today.

IDNameCheckChecker severity
C001unique-idsEvery requirement ID (AC-NNN; C-NNN/I-NNN in SOL form) appears exactly once in the file.hard error
C002duplicate-idNo other file in the workspace claims the same frontmatter id:. Requirement ids (AC-NNN) are spec-scoped — unique within a file (C001) and reused freely across specs; a reference that crosses a spec boundary qualifies as SPEC-x#AC-NNN (ADR-0080).hard error
C003verify-withEvery requirement carries a Verify with: line (SOL form: a VERIFY BY line). This is the highest-value line in a spec — a runnable check outperforms prose plans as task input (preliminary evidence) [ORACLESWE].hard error
C004one-strength-wordEach requirement states exactly one strength word — must, must not, should, should not, or may. Two strength words usually means two requirements.warning
C005non-goals-presentA Non-goals section exists and is non-empty — what this spec deliberately does not change.warning
C006open-questions-presentAn Open questions section exists (even if it says "none").warning
C007no-tbd-at-readyNo TBD, TODO, ???, or unresolved open question (one not marked non-blocking) remains in a spec at status: ready. At status: draft these are fine.hard error
C008sources-namedFrontmatter sources: names at least one origin — a ticket, an intake file, an ADR.warning
C009broken-source-linkEvery workspace file path or cross-reference ID named in sources: or in a requirement resolves to something that exists. External tracker IDs (a bare JIRA-123) are exempt — naming them at all is C008 territory.hard error
C010preserves-refs-resolveEvery entry in a change plan's preserves: frontmatter and its Behavioral-preservation-guarantees table resolves to a real requirement ID (or is an explicit PG-NNN plan-local guarantee).hard error
C011waves-presentA change plan whose kind is migration, rewrite, or schema-change has a non-empty Transformation waves section, each wave naming its verify step.warning
C012coverageA review packet's coverage table reconciles against its source spec, keyed on the task packet's declared scope: every in-scope requirement id has a coverage row (an in-scope id with no row is uncovered), and every coverage row names an id present in the source spec (a row naming an absent id is orphan). Scope-guarded to non-draft source specs (a draft's ids are work-in-progress, the same draft-is-not-final principle as C007's ready gate); when scope disagrees with the spec's id set, that divergence is itself surfaced as a fact, not silently resolved. Recorded under ADR-0079.warning
C013verify-evidence-bindingWhere a review packet coverage row carries a structured-evidence verify block (fenced, id=AC-NNN cmd="…" result=pass|fail, keyed to the row's id; the body verbatim and unparsed), the recorded cmd matches the requirement's named Verify with: / VERIFY BY command and a Pass row's block reads result=pass; a structured mismatch (a cmd disagreeing with the named command, a result=fail under a Pass row, a malformed or duplicate block) is surfaced as an internal-consistency fact, and a Pass row with only the free-form Evidence cell stays a warning. Scope-guarded to non-draft source specs; the structured-form mismatch is hard-capable but ships conservative at warning (a future ADR may promote it). Surfaces a fact, never a verdict (ADR-0077 Decision 8). Recorded under ADR-0083.warning
C014do-not-change-touchedThe change's files (the worktree diff under corpus review, or the review packet's Changed files section) reconcile against the task packet's ## Do not change entries: a changed file matching a Do-not-change entry is surfaced as a protected-path fact routed to Human attention. Matching uses the same path/prefix semantics as the Affected-areas matcher (a context prefix is stripped to its path part; a {{placeholder}} template line is ignored). Distinct from out-of-scope drift: a file may lie inside the declared Affected areas yet still match a Do-not-change entry. Surfaces a fact, never a verdict (ADR-0077 Decision 8). Recorded under ADR-0086.warning
C015citation-resolvesA spec's inline [[KEY]] citation (the citations-are-contextual form) that resolves to no <a id="KEY"> anchor in the sources.md its frontmatter sources: names is surfaced as a dangling-citation fact. Skip-guarded: when the spec names no resolvable sources.md, the check does not fire (a spec that cites nothing, or whose sources.md cannot be located, is never false-flagged); it fires only when a sources.md is resolvable and a [[KEY]] has no matching anchor. v0 is the dangling-anchor case only — the tier checks (a MUST-level claim citing a Caveated/Rejected entry) are deferred to a separate v1. Surfaces a fact, never a verdict (ADR-0077 Decision 8). Recorded under ADR-0087.warning
C016pass-needs-evidenceA review packet coverage row recorded as Pass with an empty Evidence cell is a structural contradiction — a Pass needs pasted output, a CI link, or (for a manual Verify) a named human's recorded observation; an empty cell reads Unverified, never Pass. Unlike the judgment-laden C012/C013, this is unambiguous, so it is the one review-reconcile rule shipped hard error, and the gate path (corpus check <review>) blocks on it. The advisory reconcile path (corpus review) surfaces the same row ids without blocking (ADR-0077 Decision 8 — the reconcile never issues a blocking verdict). Implements the pass-needs-evidence review-packet content rule; minted + measured 0-FP on the real reviews in ADR-0097.hard error
C017orphaned-referenceA bundled .agents/skills/<name>/references/<file> whose filename is named nowhere in its sibling SKILL.md — dead weight no reader is pointed at, the failure the reference-load field test measured (a bundled template helps only when the guide actually loads it). Orphan direction only: a reference no one points at, never the inverse (a named-but-absent target, which is higher-FP). Matching is lenient — the bare filename at a path/word boundary in the body counts as named — so a guide that does point at its references is never flagged, while a substring coincidence (a.md inside data.md) is not mistaken for a mention; measured 0-orphan across the real .agents/skills/ corpus (6 references) in ADR-0097. A warning nudge.warning

One semantic note on C003: a Verify with: line whose target does not exist yet is not a spec defect — it is an unresolved note, and the requirement reviews as Unverified until the target exists and its output is pasted. The check asks that the line be there, not that it already pass.

One note on C009: a relative path named in sources: or a requirement resolves against both the spec file's own directory and the workspace root — a hit under either is a resolve, only a path under neither is a broken link. So a spec at specs/checkout/spec.md can name a root-level intake the natural way, intake/CHK-1.md (what corpus pull + corpus new spec scaffold — it resolves from the workspace root), OR co-locate its ticket beside the spec as ticket.md (it resolves from the spec dir). Both work; no ../../-relative path is needed. The fixture checks/fixtures/cross-folder-source/ pins the root-level-intake case so the worked example's co-located ticket.md no longer masks it.

One note on size: ADR-0094 named an oversized-packet heuristic (changed-LOC + files-touched over a band). Measuring real task diffs (ADR-0097) showed a raw band cannot be both useful and low-false-positive for code tasks — legitimate feature-with-tests commits occupy the same 600–1200 LOC range as genuinely-too-big ones — so the band-based check is specified-not-shipped. Instead corpus review surfaces the diff size (changed LOC + files-touched, generated/vendored excluded) as neutral information, and the reviewer judges decomposition. No threshold is asserted.

When is a workspace valid?

The whole bar, nothing more: a workspace is valid when (a) it has a populated AGENTS.md (aim for ~100 lines — corpus's own convention, not a cap), (b) the core templates are present, and (c) at least one spec satisfies the core checks above. "Populated" means filled: an unfilled {{placeholder}} left in a live AGENTS.md or board is a clause-(a) finding, not a valid workspace (the templates keep their placeholders; the live files must not).

corpus check ships clauses (a) and (b): a missing core templates tree (clause b) is blocking, while an unfilled {{placeholder}} in a live AGENTS.md/board (clause a) is a warning (exit 1, not a blocking exit 2) — a "finish setup" nudge, since a freshly corpus init'd workspace ships the kit's boilerplate placeholders and must not greet a day-one user with a failed gate. Clause (c) is verified by the per-spec core checks above. (Level: toolable — corpus check implements exactly this; the ~100-line aim in clause (a) stays a convention, not a checked threshold.)

Task and review packet checks

These guard the evidence chain from agent run to merge decision. Level: checklist — review is expected to inspect each one; corpus check's packet mode can flag the mechanical parts (empty Evidence cells, terminal status with open questions). At convention level, the reviewer also spot-checks at least one green row's evidence by hand — structured packets invite rubber-stamping.

  • non-empty-paste — a completion claim binds to pasted output or a CI link, never a bare "tests passed". A claim without visible output is not evidence [EVIBOUND]. In a review packet: an empty Evidence cell means Unverified, never Pass.

  • no-open-critical — work is not closed with an open blocking question. A task or review packet whose status is terminal must carry no unresolved blocking question anywhere in it.

  • trigger-coverage — the review packet's Human attention section considered every trigger class or marked it n/a: unverified or failed requirements · out-of-scope changes · risky files · missing test output · changed public interfaces · DB migrations · security-sensitive changes · new finding candidates · blocked questions. The checklist itself lives in reviewing output and the review template.

  • verify-evidence-binding(Level: toolable — corpus-cli's corpus review / corpus check reconcile it as core check C013 (shipped); the structured-form mismatch is hard-capable but ships at warning. Still not enforced — the canon repo runs nothing; the team's gate / the agent CLI's hook runtime enforces. This bullet's level overrides the section's checklist default.) Beyond presence, a coverage row may carry a structured-evidence verify block (keyed to the requirement id, naming the command, carrying a closed-value result signal — the format frozen in ADR-0083 and shown in reviewing output). Where the block is present against a non-draft source spec, a checker can match the recorded command against the requirement's Verify with: / VERIFY BY reference and confirm the result token is pass — a closed-value reconcile of whether the row records a matching command and a pass signal. A structured mismatch (a cmd disagreeing with the named command, a result=fail recorded under a Pass row, a malformed or duplicate block) is an internal-consistency fact — the recorded signal disagrees with the recorded Result, the same fact-class as a status: pass contradicted by a non-Pass row — and is the kind of objective corruption the format makes hard-checkable; surfacing or rejecting it is not the tool concluding the row should read Unverified or Fail. An absent block on a free-form-only row stays a warning routed to human attention, because a command-in-prose match is fuzzy [SMELLS]. The check surfaces a fact, never a verdict — the human owns Pass/Fail/Unverified/Blocked (ADR-0077 Decision 8). Its contract entry (a new core check, scope-guarded to non-draft specs, shipped conservative at warning per the ADR-0079 precedent, with a recorded path to promote the structured-form mismatch to hard error once field-tested) is recorded in ADR-0083; it is now core check C013 (warning) in checks.yaml, shipped with the corpus review / corpus check reconcile.

Writing rules — the watchlist

Level: convention. These word families predict requirements an agent will interpret differently than you meant. Automated detection of them is known to be imprecise (precision bounded well below 1.0 in field studies [SMELLS]) — which is exactly why this list is an advisory watchlist, not a gate.

The same-line rule, in plain words: a risky word is fine when the same line makes it checkable — it names who does what to what, gives a number with units, or points at a named test. If the line cannot say how you would check it, rewrite the line.

FamilyExamplesWhat to do instead
Subjective / promotionalrobust, clean, simple, intuitive, seamless, user-friendly, modern, elegant, proper, reasonableReplace with the observable behavior you actually want.
Non-verifiable qualityfast, performant, scalable, secure, reliable, graceful, sufficientGive a number with units, or name the test that decides it.
Vague action verbshandle, support, manage, improve, optimize, streamlineSay who does what to what.
Loopholesas far as possible, if practical, where feasibleDecide: either it is required or it is not.
Ambiguous qualifierssignificant, minimal, as needed, where appropriateQuantify, or delete the qualifier.
Comparatives without a baselinebetter, faster, more efficient, optimalName the baseline and the margin ("p95 under 200 ms, from 450 ms").
Ambiguous quantifiersall, any, every, someState the exact set ("the three admin endpoints").
Bundling connectivesand, or, and/or joining separable behaviorsSplit — one requirement per behavior.
Ambiguous exceptionsunless, except whereRestate as a positive condition ("When X, …").
Vague referencesit, this, that, the aboveName the thing.

Two related habits worth checking alongside the watchlist: a bare "must not" with no paired affirmative behavior leaves what should happen undecided — state what the component does instead; and uncertainty buried in requirement prose ("probably", "we think") belongs in Open questions, not in the requirement.

The SOL check catalogue (format: sol only)

When a spec opts into structured requirements with format: sol, its blocks have enough shape for finer-grained checks. Each SOL-XNNN code below names one common mistake, what to look for, and the fix.

Level: every code below is checklist today and toolable — corpus check reports these codes for format: sol files. None is enforced. Codes are stable identifiers: cite them in review comments ("SOL-P005 on AC-003") so a fix is unambiguous.

Structure (SOL-S…) — is the block well-formed?

CodeCommon mistakeFix
SOL-S001A trigger line (WHEN/IF/WHILE/WHERE) with no actor line after it.Add the THE <actor> MUST <response> line.
SOL-S002An unknown block type or clause keyword.Use a documented block type / clause keyword.
SOL-S003An actor line with no strength word (MUST/MUST NOT/SHOULD/SHOULD NOT/MAY).Insert the strength word you mean.
SOL-S004Two blocks share an ID within one file.Renumber.
SOL-S005The ID prefix does not match the block type (e.g. REQ C-001:).Use the matching prefix: REQ→AC-, CONSTRAINT→C-, INVARIANT→I-, INTERFACE→IF-, QUESTION→Q-.
SOL-S006SHOULD/SHOULD NOT with no BECAUSE or EXCEPT in the block.Add the reason, or strengthen to MUST/MUST NOT.
SOL-S007A malformed header — missing colon, spaces or illegal characters in the ID.Write TYPE PREFIX-NNN:.
SOL-S008Metadata or free prose before the block's first control line.Lead with the condition or actor line; metadata goes at the end.
SOL-S010An unknown trailing metadata field.Use a documented field, or move the text to commentary.
SOL-S011A header with a recognized block type but no ID.Add a PREFIX-NNN ID.
SOL-S012A required spec section missing or out of order.Add or reorder the section — see the spec format.
SOL-S013Hidden characters (zero-width, bidirectional-control, homoglyph) in requirement text — an instruction-injection vector for agents.Strip them; re-author in printable characters.
SOL-S014A block missing a clause its shape requires.Add the required clause.

Prose (SOL-P…) — does the sentence pin down behavior?

CodeCommon mistakeFix
SOL-P001A condition with no consequence — the trigger is there, the behavior is not.Supply the consequence.
SOL-P002No responsible actor.Name who does it.
SOL-P003A missing or lowercase strength word where binding force is meant.Write the uppercase modal.
SOL-P004Several separable behaviors bundled into one clause.Split — one requirement per block.
SOL-P005A watchlist word in a requirement with no same-line observable criterion.Apply the same-line rule: who/what, a threshold, or a named test on the same line.
SOL-P006An undefined term in a requirement.Define it — an in-file TERM or the project glossary.
SOL-P007A bare MUST NOT with no paired affirmative behavior.State what the actor does instead.
SOL-P008Uncertainty left in requirement prose instead of a QUESTION block / Open questions.Lift it into a question.
SOL-P050A pronoun with no unique antecedent.Name the referent.
SOL-P051Passive voice hiding the actor.Name the actor doing the action.
SOL-P052A requirement sentence beyond ~20 words.Split or tighten.
SOL-P053Non-present or non-active phrasing.Write present tense, active voice.
SOL-P054A decorative phrase that adds no constraint.Delete it.
SOL-P055Repeated context that adds nothing new.Delete it.
SOL-P056A comparative with no baseline.Name the baseline and the margin.
SOL-P057A term drifting from its glossary definition (synonym, casing variant).Use the canonical term.
SOL-P058SHALL/SHALL NOT used as a strength word.Write MUST/MUST NOT.

Cross-references (SOL-M…) — do the pieces agree?

CodeCommon mistakeFix
SOL-M001An actor, object, or surface that resolves nowhere in the spec or its imports (also: an ID collision across files).Define the referent, or fix the ID.
SOL-M002Two requirements with the same actor + trigger + surface and opposed strength words — a direct contradiction.Pick one; record why in the spec.
SOL-M003A DEPENDS ON / IMPLEMENTS / PRESERVES reference naming an ID that does not exist.Fix the reference.
SOL-M004A lower-authority file weakening a higher-authority requirement.Resolve at the higher authority — see source authority.

Verification (SOL-V…) — is every claim checkable?

CodeCommon mistakeFix
SOL-V001A requirement block (REQ/CONSTRAINT/INVARIANT) or INTERFACE with no VERIFY BY.Add the binding.
SOL-V002A VERIFY BY whose command or artifact does not resolve.Point it at something runnable.
SOL-V003Evidence that cannot observe what it claims (e.g. an invariant bound only to one unit test).Bind a check that can actually see the property.
SOL-V004A Pass recorded against text that has since changed.Re-run the evidence — never re-bless silently.
SOL-V005A review result outside Pass / Fail / Unverified / Blocked, or a lifecycle marker (Waived, Stale, Contradicted) missing its required fields.Use a valid result; complete the marker's fields.
SOL-V006An INTERFACE verified by anything other than a contract check.Bind a contract test.
SOL-V007A lifecycle marker on the wrong result (e.g. Waived on a Pass).Remove or correct the marker.
SOL-V008A required binding with no recorded result at review time.It counts as Unverified — run it, or waive it on the record.
SOL-V009A VERIFY BY evidence type outside the documented set.Use a documented type — see structured requirements.
SOL-V010A manual or waived result with no named human.Name who accepted it, and why.
SOL-V011Evidence that does not say what it actually exercised.Record what ran relative to the requirement.

Splitting work (SOL-O…) — can tasks run safely in parallel?

These matter when a spec is split into tasks — the advanced lifecycle covers that step in full.

CodeCommon mistakeFix
SOL-O001Two parallel tasks writing the same files.Serialize them, or split the write surfaces.
SOL-O002A dependency cycle between requirements or tasks.Break the cycle.
SOL-O003An unresolved blocking question reaching task-splitting.Answer or downgrade it first — splitting past it commits a guess.
SOL-O004A requirement with no declared write/read scope.Declare the surfaces it touches.
SOL-O005A task writing paths outside its declared scope.Declare the path, or stop writing it.
SOL-O006An imported file duplicating a policy requirement.Deduplicate — one home per rule.
SOL-O007A requirement assigned to no task.Assign it — coverage is the point of splitting.
SOL-O008A requirement assigned to two implementing tasks.Assign exactly one implementer.

How core checks map to SOL codes

One requirement record underlies both spec forms, so a core check and its SOL codes are the same question asked of two surfaces — never two different rules. The fixtures under checks/ in the corpus repo include simple/SOL equivalence pairs that pin this.

Core checkSOL form
C001 unique-idsSOL-S004 / SOL-S005
C002 duplicate-idSOL-M001
C003 verify-withSOL-V001 / SOL-V002
C004 one-strength-wordSOL-S003 / SOL-P003 / SOL-P004
C005 non-goals-presentSOL-S012
C006 open-questions-presentSOL-S012 / SOL-P008
C007 no-tbd-at-readySOL-P008 / SOL-O003
C008 sources-named— (frontmatter check; identical in both forms)
C009 broken-source-linkSOL-M003

The writing-rules watchlist corresponds to SOL-P005 (watchlist word, no same-line criterion), SOL-P056 (comparative, no baseline), and SOL-P004 (bundling connectives).

The severity split: the corpus check contract

Every check on this page maps to one of two severities. This is the contract the reference implementation builds against — until that tool is in front of you, read the split as advice on what to fix first, not as anything blocking.

  • Hard error — the checker must reject (non-zero exit). The defect changes what gets built: a missing or colliding ID, a requirement nobody can verify, a contradiction, a placeholder shipped as ready.
  • Warning — the checker must report but not reject. The defect makes the spec weaker or harder to read, and a human should decide.
SeverityChecks
Hard errorC001, C002, C003, C007, C009, C010 · all SOL-S codes · SOL-P001–P008 · all SOL-M codes · SOL-V001/V002 and V004–V010 · SOL-O001/O002/O003/O005/O007/O008
WarningC004, C005, C006, C008, C011, C012, C013, C014, C015 · SOL-P050–P058 · SOL-V003, SOL-V011 · SOL-O004, SOL-O006

One position-sensitive case: SOL-P056 (comparative, no baseline) is a hard error inside a requirement line and a warning in surrounding commentary — commentary may be loose; a requirement line is the requirement.

Teams may promote any warning to blocking by policy. Going the other way — accepting a hard error — deserves a written note in the review packet saying who accepted it and why.

  • Structured requirements — the SOL notation these codes check, and the two-surfaces-one-model rule.
  • Writing specs — the happy-path guide the core checks back.
  • Reviewing output — where the packet checks and the trigger checklist live.
  • Artifact formats — every template this page's checks apply to.
  • checks/ (corpus repo) — the checks fixtures: expected results per check, the test data corpus-cli runs against.

Ready to run the loop on your own repo? Get started — copy the kit and write your first spec.