Facts and their status markers
01A spec is a list of promises, and each promise has a state: proposed, being built, done, abandoned. vibe reads that state from a small marker written next to the promise, in Markdown or in the XML form, and one command tells you when a marker is malformed or in the wrong place. This page is the grammar of those markers.
The marker
03A marker is one XML-shaped element, <status stage="…" state="…"/>, embedded in Markdown or native in the dialect. A point marker is self-closing; an unclosed one is not well-formed and is an error. The shorthand @status:spec/done means the same as the element, and @status:impl alone means impl/work, with one exception: @status:unknown means unknown/hold. The older spellings without status: are still read. Every vocabulary is closed: a value outside the tables is an error, with the nearest legal value as a hint.
04 One XML-shaped element, embedded in Markdown (and, later, native in XML documents — the frontend duality of PROP-035 §5):
05 A point marker MUST be self-closing (/>). An unclosed<status …>point form is not well-formed XML and is acheckerror.
06@status:<stage>/<state>and@status:<stage>are macro-equivalents of a point marker. The legacy spellings@<stage>/<state>and@<stage>mean exactly the same and are still read, so a document written before the qualified form keeps parsing.
07@status:impl⇒<status stage="impl" state="work"/>— bare shorthand defaults tostate="work", with exactly one exception:@status:unknown⇒state="hold". (@status:freeze⇒freeze/work: "freezing now".)
08 Vocabularies are closed. Any value outside the tables is acheckerror with a nearest-legal-value hint (typos likerewrokdie in CI, not in review).
Anchors
09A marked unit must have an address. In Markdown the anchor is @fact:ID as the first token of a paragraph or a list item. The id starts with a letter and continues with letters, digits, underscore and hyphen, and it shares one address space with the heading anchors, so a duplicate across the two forms is an error. A marker on a unit without an anchor is an error. The scanner ignores fenced code, inline code and addresses, so a marker quoted inside them is text, not a marker.
10 Fact anchors — the anchored-when-marked law (owner, 2026-07-24; spelling amended 2026-08-06). A stable fact address is written@fact:<ID>as the first token of a paragraph or list item. The legacy spelling##<ID>means the same and is still read.
11<ID>is[A-Za-z][A-Za-z0-9_-]*; the unit is then addressable asspec://…/<doc>#<ID>, sharing one address space with the heading{#anchor}s — a duplicate across both forms is acheckerror. The address is unchanged by the spelling: it names the id, never the opener.
12 Every unit that carries a status marker — paragraph or list item — MUST also carry a@fact:<ID>anchor; a marked, anchor-less unit is acheckerror.
13 Inside fenced code blocks, inline code spans, and URLs the element and the shorthand (§3.7) are not recognized — the scanner is fence-aware.
Where a marker goes
14There are four places. A document marker stands in the preamble, or right after the first heading when the document opens with one. A section marker stands alone on the line after its heading; that is the only legal standalone position in a body. A paragraph marker sits inside the paragraph's own text, as its first or its last token. Every list item is a unit of its own, at every nesting level, and its marker sits inside the item's text. A marker standing alone between two paragraphs is an error; there is no nearest-paragraph guess. A unit carries at most one status marker and any number of action markers.
15 Document — marker in the preamble, before the first heading. Pilot amendment (2026-07-24): a document that opens with its heading (the standard shape of this repo's specs) has no preamble; there, the standalone marker immediately after that first heading is the document marker, not a section marker.
16 Section — marker on its own line immediately after the heading line (for any heading other than a preamble-less file's first one, per the amendment above). This is the only legal standalone position inside a body.
17
Paragraph — marker inside the paragraph's own text: the first
token (right after the newlines, or right after the paragraph's @fact:<ID>
anchor) or the last token (right before them).
18 List item (fact amendment, 2026-07-24 — owner-directed) — every item of a bulleted or numbered list is a unit of its own, at every nesting level. Its marker — shorthand or XML form alike — sits inside the item's own text: the first or last token of the item (before any nested sub-items, which carry their own markers).
19
A standalone marker between two paragraphs is a check error — there is
no "nearest paragraph" heuristic.
20 Multiple markers on one node: at most one status marker (stage/state), any number of action markers — a unit may legitimately needremove+actionstage="doc"andcontinue+actionstage="test"at once.
How a status rolls up and down
21A node's marker covers the descendants that carry none. An unmarked node's computed status is the worst of its children, and unknown wins the bottom.
22 Downward (defaulting): a node's marker covers unmarked descendants.
23
Upward (aggregation): an unmarked node's computed status is the
worst-of its children per the §3.3 order (unknown wins the bottom).
Actions, stages and audiences
24Beside the status a marker may carry an action, what should happen next, and actionstage narrows it: action="remove" actionstage="doc" means the documentation of this is to be removed while stage still describes the unit itself. A doc action with an audience is an obligation of the documentation: a page written for that audience must cite the fact, and vibe doc check --coverage checks that one does.
25actionstagenarrows the target:action="remove" actionstage="doc"= "the documentation of this is to be removed", whilestagekeeps describing the unit itself.
26 Primary use:actionstage="doc"markers with an audience are the obligations of the documentation — the promises a page for that audience must cite.vibe progress report --view doc --audience user|author|dev|agentlists them; it is the source of the coverage gatevibe doc check --coverage(PROP-057 §14, the ratchet PROP-047##DOC-COVERAGE-RATCHETnames), never a table of contents — navigation is derived from the page manifest (amended 2026-09-11; the earlier wording fed the two guides' tables of contents).
What closes a fact
27A fact may say what closes it: @requires:implementation,verification right before its final status in Markdown, or a requires attribute in the XML form. The kinds are a closed list, specification, implementation, verification, documentation, decision, research, plan, disposition and external. They name closure artifacts, not genres: a research note may require an implementation.
28 The Markdown form is exactly one qualified trailing annotation@requires:<kind>[,<kind>…], immediately before the fact's final status shorthand admitted by §3.7 (qualified full/bare or legacy full/bare), or final<status …/>point marker.@requiresitself has no legacy bare spelling. The XML-dialect equivalent is arequires="<kind>[,<kind>…]"attribute on the named or generic fact element, besidestatus; it is never an attribute of the standalone document/section<status>element. Both forms lower to one fact-owned semantic set.
29 The closed vocabulary, in canonical order, isspecification,implementation,verification,documentation,decision,research,plan,disposition,external. A new kind requires an amendment here before parser, IR or wire support. These are closure artifacts, not document genres: a research document may require an implementation, and a contract fact may itself be a decision artifact.
The tool
30vibe facts check is the lint, and --exhaustive demands a marker on every paragraph; vibe progress check is a transitional alias that prints the new spelling. The observed files are named by include globs in facts.toml at the package root: globs that say what is observed, never a list of exclusions.
31 The CLI follows the boundary. The markup lint is a facts operation:vibe facts check [--exhaustive]becomes its durable home, withvibe progress checkkept as a transitional alias (printing the new spelling); the gate panel switches to the facts spelling. Campaign verbs (scan/mirror/seal/gate/baseline/rescan/resume/weave/report) stay undervibe progress. The same wave repairs B-100: a bare--campaign <id>resolves againstcampaigns/<id>instead of silently minting a cwd-relative state zone. Landed:vibe facts checkcarries the lint byte-identically (one implementation, two entries), the alias prints its stderr note (suppressed under --json), the shared campaign resolver fails loud on an unknown bare id with the existing zones listed and writes nothing, and the gate panel's markup line runs the facts spelling — proven live on the original B-100 scenario.
32 Optional dev-mode mechanics, configured by afacts.tomlat the package root (theclippy.tomlpattern — tool config, not manifest pollution);progress.tomlis read as a silent legacy fallback for the transition (owner correction 2026-08-22: the observed tree is a facts-layer concern).
33 Include-style globs name what is observed (not gitignore-style excludes):
Edge cases and rules
34A document written before the qualified spelling keeps parsing: the legacy @spec/done and ##ID forms mean what the new ones mean. Converting a file between Markdown and XML keeps every marker, because the marker is part of the model, not of the syntax.