Skip to content

Process Specifications

How we run this project. Covers CI configuration, GitHub workflow, agent conventions, documentation standards, and spec authoring rules.

Behavioral Specifications

The following specifications define Event-Condition-Action (ECA) behavioral requirements with typed preconditions, ordered steps, and postconditions.

Architecture

Defines the architectural constraints for the Vultron codebase. The system follows hexagonal architecture (Ports and Adapters): the core domain is isolated from all external systems, which interact through defined ports via thin adapters. Covers layer separation rules, the core object hierarchy, domain model richness, and the fail-fast validation boundary.

File ID: ARCH

Architecture Ratchet Discipline

ID Priority Requirement Related
ARCH-18-001 MUST Architecture boundary tests that enforce a KNOWN_VIOLATIONS allowlist MUST use a bidirectional equality assertion: assert actual_violations == KNOWN_VIOLATIONS. One-directional subset assertions (assert actual ⊆ KNOWN) are prohibited because they allow the violation count to grow silently.

A subset assertion says "we have at most these violations" but allows new ones to be added without failing the test. Bidirectional equality says "we have exactly these violations" — any addition fails immediately, and any removal must be reflected in the allowlist.
Implements: ARCH-10-001
ARCH-18-002 MUST When a known architectural violation is resolved (the file or import is removed or refactored), the corresponding entry MUST be removed from the KNOWN_VIOLATIONS allowlist in the same commit that resolves the violation. Stale allowlist entries that no longer correspond to real violations MUST NOT be retained.

Stale entries mask the true state of the architecture boundary. A "fixed" violation that remains in KNOWN_VIOLATIONS makes the ratchet appear tighter than it is and hides regression when a new violation with the same name is later introduced.
Refines: ARCH-18-001
ARCH-18-003 MUST Every non-empty ratchet baseline under test/ (an allowlist, exclusion set, or numeric ceiling that is not at its terminal value) MUST carry an # owner: #N comment naming the GitHub issue that owns driving it to its terminal value. A baseline that is permanent by design MUST instead carry a # permanent: <reference> comment citing the issue, ADR, or spec requirement that made it permanent.

A ratchet records what is exempt but not who retires it. When the issue that created a baseline closes with entries left, the debt becomes invisible and the test stays green. MS-10-006 already imposes this rule on the spec-corpus ceilings; this extends it to every baseline. The permanent marker keeps a pinned exemption set from needing an invented owner, while requiring the decision behind it to be citable.
Refines: ARCH-18-002
ARCH-18-004 SHOULD A CI job outside the merge path SHOULD query GitHub for the state of every issue cited by an # owner: #N comment, and when a cited issue is closed it SHOULD file or update a single tracking issue listing the affected baselines. The job MUST NOT fail the build on a closed owner.

Only GitHub knows whether an owner is still open, so this check cannot run in the unit suite. Failing the build on it would turn main red when an unrelated issue closes, which no pull request caused. This is the same race a two-directional count pin produces when two pull requests land together.
Refines: ARCH-18-003
ARCH-18-005 MUST A baseline under test/ is a ratchet when its terminal value is an empty set or a zero ceiling and an # owner: #N issue drives it there; it is a pinned exemption set when it is an exact set with no terminal value, kept equal to reality in both directions (ARCH-18-001, ARCH-18-002), whose entries are exempt by a recorded decision rather than awaiting a fix. Only a ratchet counts as open debt. A pinned exemption set MUST carry the # permanent: marker of ARCH-18-003, and its comment and module docstring MUST describe that constant as a pinned exemption set, not as a ratchet or a backlog. A pinned set MUST contain only permanent entries: an entry awaiting a fix MUST move to its own ratchet constant with an # owner: #N marker.

Several exact sets began as shrinking backlogs and were later decided to be permanent design registers, but their files still called them ratchets. A reader counting open debt over-counted, and the real debt was harder to see. Both kinds use the same bidirectional equality check; what differs is whether an end state exists, so the name and the marker carry that difference.
Refines: ARCH-18-003

Behavior Tree Integration

Requirements for integrating py_trees behavior trees into Vultron handler functions. Covers the BT execution model, state management via the DataLayer, handler integration patterns, actor isolation, concurrency constraints, and failure diagnosis. Complex protocol workflows such as report validation, case creation, and embargo management must be implemented as BT nodes or subtrees.

File ID: BT

Fuzzer Node Replacement Lifecycle

ID Priority Requirement Related
BT-21-001 MUST Each fuzzer (simulator) BT node targeted for replacement by a production implementation MUST have a corresponding tracking issue that captures: the fuzzer node name, the production replacement design (as a BT-20 entry or equivalent), and the blocking dependencies. The tracking issue MUST be linked from the relevant BT-20 tracking_issue field.

Without a tracking issue per fuzzer node, the replacement backlog is invisible in the project board. The BT-20 entries document the design; this entry mandates the process artefact (issue) that makes the work schedulable.
Refines: BT-20-001
BT-21-002 MUST When a fuzzer node is replaced by its production equivalent, the corresponding BT-20 PROVISIONAL rationale note MUST be updated to remove the PROVISIONAL marker and the BT-21-001 tracking issue MUST be closed in the same PR.

Provisional notes that survive after the production replacement is merged become misleading documentation. The two-step close (update spec + close issue) ensures the spec and the project board remain consistent.
Refines: BT-21-001

Behavior Tree Composability

Requirements for implementing Vultron's behavioral logic as composable, reusable BT structures using the simulator as the canonical blueprint. Covers the workflow of BT composition: how to use the simulator reference, where structures must live, when to use BT composite idioms instead of procedural code, and the fractal composability principle that applies at every depth.

File ID: BTC

Simulator Reference Workflow

ID Priority Requirement Related
BTC-01-003 MUST If a corresponding simulator class does NOT exist for the behavior being implemented, the developer MUST document the divergence with a brief rationale comment before proceeding. Refines: BT-06-002

Build Workflow Learnings Policy

Defines the content policy for the per-entry incoming learnings queue used by the build and bugfix skills to pass implementation observations upstream to the learn skill. Covers what belongs in the queue, what must not go there, file naming and format, how the learn skill processes and archives entries, and the learning history entry type. The queue is narrow by design: findings with a definite owner are routed to a GitHub issue or an in-session fix at the moment of discovery, and only unverified general claims and spec divergences are queued for batch processing (BW-07).

File ID: BW

Incoming Learnings Queue Content Policy

ID Priority Requirement Related
BW-01-001 MUST plan/incoming/learnings/ MUST contain only implementation observations, open questions, constraints, and insights discovered during build or bugfix execution that are relevant to future implementation work. Completion summaries, "what was done" narratives, and status updates MUST NOT appear here.

The queue is a focused upstream communication channel from executing skills to the learn skill. Mixing status updates with actionable insights dilutes the signal and creates noise for learn to filter.
Depends on: HM-03-001 — Completion summaries belong in append-history implementation, not in the incoming learnings queue.
BW-01-002 MUST NOT The build and bugfix skills MUST NOT write completion summaries or task-status narratives to plan/incoming/learnings/. Depends on: HM-05-001
BW-01-005 MUST Completion summaries from the build and bugfix skills MUST be archived exclusively via uv run append-history implementation. Implements: HM-05-001
BW-01-003 MUST Each incoming learning file MUST be named YYYYMMDD-SLUG.md where YYYYMMDD is the creation date and SLUG is a short lowercase, hyphenated slug describing the observation — conventionally the originating issue number followed by a descriptive phrase (e.g., 20260918-3399-bt18-has-no-contract-for-a-refusal-carrying-a-payload.md). SLUG MUST NOT be required to equal or derive from the source frontmatter field; the link to the originating work item is carried by source (BW-02-002). One work item MAY produce several learning files, so each file's SLUG MUST be distinct from every other file in the queue sharing its date prefix.

Using individual files instead of a shared flat file eliminates merge conflicts when multiple PRs each have an observation to record. The date prefix provides filesystem sort order. The slug is descriptive rather than source-derived because a filename keyed on source collapses every learning from one work item onto the same name (#1857 — five learnings from #1777 would all have been 20260730-ISSUE-1777.md).
BW-01-006 MUST Incoming learning files MUST be committed as part of the originating PR so the learning is permanently linked to the work that produced it. Refines: BW-01-003
BW-01-004 MUST NOT Skills other than build and bugfix MUST NOT create files in plan/incoming/learnings/.

plan/incoming/learnings/ is specifically a channel for code-execution insights flowing from build/bugfix to learn. Gap-analysis observations from update-plan are a different signal and belong in notes directly.
BW-01-007 MUST The update-plan skill MUST write observations and open questions to the notes/*.md file whose topic matches the content. It MUST NOT use plan/incoming/learnings/ as an intermediate staging area. Refines: BW-01-004

Incoming Learnings File Format

ID Priority Requirement Related
BW-02-001 MUST Every file in plan/incoming/learnings/ MUST use the same YAML frontmatter format as history entry files: required fields title, type (value learning), timestamp (tz-aware ISO 8601 UTC), and source (the originating work-item identifier, per BW-02-002). The incoming file is named YYYYMMDD-SLUG.md, which is a different naming scheme from the archived entry — the archived filename is derived from source, not carried over from the incoming name.

Using identical format means archiving is a git mv equivalent via uv run append-history --from-file — no content transformation needed.
Depends on: HM-02-001
BW-02-002 MUST The source field in an incoming learning file MUST be the originating work-item identifier — ISSUE-<n>, CONCERN-<n>, IDEA-<n>, BUG-<n> or equivalent — matching HM-02-001's definition of source. It MUST NOT be set to the incoming file's own YYYYMMDD-SLUG stem. append-history sanitises source into the archived entry's filename, so source is what makes the archived entry discoverable from the work item it came from, and several incoming files may legitimately share one source when one work item produced several observations.

source answers "which work item produced this observation?", which is why HM-02-001 defines it as an originating identifier and why append_history_entry() uses it to name the archived file. The earlier "must match the filename stem" wording described the post-archive invariant (archived entries are named <source>.md, so there the two do coincide) and mistakenly applied it to the pre-archive file, whose name is date-and-slug. No file in plan/incoming/learnings/ has ever satisfied the old wording.
Depends on: HM-02-001
BW-02-003 MUST The timestamp field in an incoming learning file MUST reflect the actual creation time of the observation (not the current date if the entry was written for a historical run). Depends on: HM-06-001
BW-02-004 MUST NOT The timestamp field MUST NOT be written as a bare YYYY-MM-DD date. _coerce_to_utc() in vultron/metadata/history/models.py rejects any value that is not timezone-aware, and YAML parses a bare date into a naive datetime, so a bare-date entry fails validation the moment append-history --from-file reads it. Write "YYYY-MM-DDTHH:MM:SSZ" (quoted, so YAML yields a string for Pydantic to parse) and use T00:00:00Z when only the date is known.

This is stated separately because bare dates are the dominant form currently present in plan/incoming/learnings/ and are silently accepted by every check except the archiving step — the one that matters. An entry written this way looks fine in review and then cannot be archived, so the defect surfaces only when learn tries to process it.
Refines: BW-02-001
Depends on: HM-06-005
BW-02-005 MUST Conformance to BW-02-001 through BW-02-004 MUST be checked at commit time by validating every file in plan/incoming/learnings/ through HistoryEntryFrontmatter — the same model append-history --from-file uses — so the check and the archiver cannot drift. A validator that reimplements the rules independently does not satisfy this.

Between the spec being written and ISSUE-2762, 37 of 62 entries accumulated frontmatter that no check rejected and the archiver could not accept. The defect was invisible precisely because the only enforcement lived in the last step of a workflow that runs weeks after the file is written. Validating at commit time against the archiver's own model is what makes the queue stay drainable.
Depends on: BW-02-001

learn Skill Processing

ID Priority Requirement Related
BW-03-001 MUST When the learn skill processes plan/incoming/learnings/, each file MUST be promoted to the durable destination matching the content type — requirements to specs/*.yaml, design insights to notes/*.md, agent guidance to AGENTS.md — before being archived via uv run append-history --from-file <path>. Depends on: HM-02-002
BW-03-002 MUST uv run append-history --from-file <path> MUST be used to archive each processed incoming learning file. This command moves the file from plan/incoming/learnings/ to plan/history/YYMM/learning/ and deletes the source. Files MUST NOT be moved manually or remain in plan/incoming/learnings/ after archiving.

Using the CLI tool preserves the audit trail, regenerates the monthly history README, and enforces write-once semantics.
Depends on: HM-01-002
BW-03-003 SHOULD After a complete learn cycle, plan/incoming/learnings/ SHOULD contain only a .gitkeep file. The queue SHOULD be empty after every learning session that fully drains it.

learning History Entry Type

ID Priority Requirement Related
BW-04-003 MUST The learn skill MUST use uv run append-history --from-file to archive each processed incoming learning file. Implements: BW-04-001

Skill Documentation Updates

ID Priority Requirement Related
BW-06-001 MUST The build skill documentation MUST distinguish between learnings (observations and constraints recorded as individual files in plan/incoming/learnings/) and completion summaries (archived via uv run append-history implementation). Both outputs are required; neither may be omitted or combined.
BW-06-002 MUST The learn skill documentation MUST reference plan/incoming/learnings/ as its primary input source and MUST describe the append-history --from-file archiving lifecycle.
BW-06-003 MUST NOT The update-plan skill MUST NOT instruct agents to write to plan/incoming/learnings/. Observations discovered during gap analysis MUST be written directly to notes/*.md files.

Upward-Reflection Finding Routing

ID Priority Requirement Related
BW-07-001 MUST Before completing a build or bugfix session, the agent MUST run an upward-reflection checklist that explicitly asks whether each of the following was encountered: behaviour with no spec entry; a requirement that was unclear or that conflicted with another; something that exists but is wrong; something that should exist but does not; a broken tool, skill, or tracking artefact; and a narrow but true fact about the code. Every triggered item MUST then be routed to exactly one destination per BW-07-004. A session that records nothing is valid only if the checklist was completed and every answer was "no".

Without a mandatory checklist, agents default to recording only tactical tips-and-tricks. The checklist is what surfaces higher-signal problems, so it remains mandatory and unchanged in force. What changed is that a triggered item no longer implies a learning file — most findings have a nearer owner.
BW-07-003 MUST The learn skill MUST process learning files tagged signal: spec-gap, signal: spec-contradiction, or signal: spec-ambiguity before processing signal: theme-candidate or untagged entries.

These signal types represent cases where the specification has diverged from reality or contains internal conflicts. Deferring them risks future build sessions making the same gap-driven interpretations independently.
BW-07-004 MUST Each triggered checklist item MUST be routed at discovery to exactly one destination. Something that exists but is wrong → a GitHub type:Concern issue. Something that should exist but does not → a GitHub type:Idea issue. A broken tool, skill, or tracking artefact → a fix applied in the same session. A narrow, symptom-retrievable fact about the code → an assertion at the site (regression test, type annotation, or comment) in the same session. A general claim that cannot be verified from a single instance → a signal: theme-candidate learning file. Behaviour with no spec entry, or an unclear or conflicting requirement → a signal: spec-* learning file. Only the last two destinations produce a learning file.

A finding with a definite owner and a definite action does not need to wait for the next learn batch; deferring it means every agent between discovery and that batch rediscovers the same problem. Of the 62 queued files audited on 2026-09-02, only 5 were spec-shaped, while 38 named an action that could have been taken at discovery.
BW-07-005 MUST The learn skill MUST NOT promote a claim to notes/ or AGENTS.md on the strength of a single learning file. Promotion requires at least two independent witnesses — two entries from different source work items making the same claim — or independent confirmation against the code.

A single build session cannot distinguish a general pattern from a local accident, and its author is the party least able to judge which it is. Corroboration is the cheapest available test. The queue exists to accumulate it.
BW-07-006 MUST When a session produces a finding that corroborates an existing queued signal: theme-candidate entry, the agent MUST file or update a GitHub type:Concern issue for the now-corroborated theme in that session rather than adding a second learning file and waiting for learn.

The second witness is the moment the claim becomes trustworthy, so it is the moment to act. Without this rule the corroboration threshold merely reintroduces the latency it was meant to remove: the mkdocs --strict theme accumulated four witnesses between 2026-08-27 and 2026-08-31 while remaining unfiled.
Refines: BW-07-005
BW-07-007 SHOULD A signal: theme-candidate entry that has received no corroborating witness within 30 days of its timestamp SHOULD be archived by learn as unreproduced, via append-history learning, without promotion to notes/ or AGENTS.md.

An uncorroborated claim that has not recurred in a month is more likely to have been a local accident than a pattern. Archiving preserves the record without letting the queue grow without bound.
Refines: BW-07-005
BW-07-008 MUST NOT A decision that was made, applied, and shipped within the session — leaving no open question and asking nothing of any future reader — MUST NOT be recorded as a learning file. The commit message, the diff, and the PR body are its record.

Such entries cannot satisfy the completeness rule that a learning must produce a durable output, because there is no output left to produce. They are guaranteed to be archived unpromoted, and the reader who processes them recovers nothing.
Refines: BW-01-001 — A closed decision is a completion summary in the shape of an insight.
BW-07-009 MUST Code-review findings in files the current change did not touch (pre-existing findings, and findings deferred as out of scope) MUST be filed as GitHub type:Bug or type:Concern issues in the session that surfaced them. They MUST NOT be recorded as learning files, and MUST NOT be left only as PR-comment advisories.

A learning file cannot be assigned, scheduled, or closed, so a bug report placed in one is inert. The 2026-09-02 audit traced six such entries holding roughly twenty findings: all but four were eventually filed as issues — by later sessions performing their own reviews, never by the learning file. The queue was therefore not the tracking mechanism but a parallel ledger that duplicated it, at the cost of the four findings that fell through because recording them had felt like tracking them. Several entries also cited the merged PR that surfaced the finding as evidence of tracking; a PR records what shipped, not what was deferred.
Refines: BW-07-004

Case Ledger Processing

Requirements for participant assertions, CASE_MANAGER canonicalization, local case audit ledgering, recorded-history projection, and replication-facing CaseLedgerEntry objects. Covers the commit authorization model, per-case genesis hash derivation, the canonical entry criteria that separate the case ledger from the process log, the CASE_MANAGER inbox routing path that enables ledger entry authorship, and the causal-ordering model that makes log_index order the authoritative causal sequence.

File ID: CLP

CASE_MANAGER Inbox Routing for Canonical Ledger Entry Authorship

Requirements for the end-to-end routing path that delivers protocol-significant activities to the CASE_MANAGER's inbox so the CASE_MANAGER can author canonical ledger entries. Covers outbound addressing rules, the rule that the CASE_MANAGER commits its own emissions in the emitting tree rather than mailing itself a copy (ADR-0109), and the pre-flight guard that prevents non-CASE_MANAGER invocations from committing ledger entries.

ID Priority Requirement Related
CLP-10-020 MUST test/architecture/test_no_dl_mutations_in_execute.py MUST treat a DataLayer write that a received-side execute() body (a use case under vultron/core/use_cases/received/) reaches through any function or method defined under vultron/core/use_cases/, resolved transitively, as a violation of that execute(), and MUST enumerate the remaining violations as an exact KNOWN_VIOLATIONS set.

The detector matched only a write whose receiver was literally the use case's DataLayer inside execute(); a same-package helper that performed the write was excluded by design, which let eleven execute() bodies in nine files reach a write it never saw (ISSUE-3339). A write inside a BT node is not a violation, so resolution stops at the use-case package boundary. Trigger-side execute() and _prepare() bodies are governed by BT-15-001, not this rule.
Refines: CLP-10-005
Refines: ARCH-18-001

CI Security

Requirements for securing CI/CD workflows and GitHub Actions usage in the Vultron project.

File ID: CISEC

Action Version Pinning

ID Priority Requirement Related
CISEC-01-001 MUST All uses: references to third-party or external actions in .github/workflows/*.yml MUST be pinned to a specific commit SHA rather than a version tag or branch name. Local path references to actions within this repository (e.g. ./.github/actions/<name>) are exempt — they are already pinned to the commit being checked out and have no separate tag/release lifecycle to pin against
CISEC-01-002 MUST Each SHA-pinned uses: reference MUST carry an inline comment identifying the corresponding human-readable version tag (e.g., # v4.1.0) to aid maintainability
CISEC-01-003 SHOULD A CI verification test SHOULD confirm that all uses: lines in .github/workflows/*.yml reference a SHA hash rather than a mutable version tag or branch name, and that each SHA line carries the human-readable version comment required by CISEC-01-002

Secrets Management

ID Priority Requirement Related
CISEC-02-001 MUST NOT Workflow secrets and tokens MUST be stored in GitHub Secrets and MUST NOT appear in repository files
CISEC-02-002 MUST The minimal set of permissions required MUST be declared explicitly via permissions: at the job or workflow level

Artifact Integrity

ID Priority Requirement Related
CISEC-03-001 MUST CI workflows that download third-party artifacts or tools MUST verify signatures or checksums before using them

Prevents supply-chain attacks via compromised artifacts

Maintenance

ID Priority Requirement Related
CISEC-04-001 SHOULD SHA pins SHOULD be reviewed and updated on a documented periodic cadence, at minimum when a security advisory is issued for the pinned action or when a new minor version is released
CISEC-04-002 MUST When adding a new workflow step or action, the SHA pin and version annotation MUST be included before the step is merged to main

CI Failure Notification

ID Priority Requirement Related
CISEC-05-001 MUST Every workflow triggered by push to main or by a schedule event MUST include a step conditioned on failure that files or updates a GitHub Issue. The step MUST use the shared .github/actions/notify-failure composite action. The step MUST fire only on a genuine job failure and MUST NOT fire on cancellation alone (see CISEC-05-006).

Without structured failure notification, a broken main or a failed scheduled job goes undetected until a developer manually audits the Actions tab. Structured alerting ensures failures are visible in the issue tracker where the team already triages work. Derived from ADR-0055 (docs/adr/0055-ci-failure-alerting-via-github-issues.md).
CISEC-05-002 MUST Every qualifying workflow (per CISEC-05-001) MUST also include a separate step conditioned on success that closes any open failure issue for that workflow, providing recovery visibility. The close step MUST NOT run on a cancelled (concurrency-superseded) run: a step keyed on the success() status function already satisfies this, and a step keyed on the needs.*.result aggregate MUST require the absence of cancelled (!contains(needs.*.result, 'cancelled')).

A failure issue that stays open after the workflow recovers is misleading. Two dedicated steps (one on failure, one on success) keep each step single-purpose and avoid embedding workflow-status logic inside the composite action itself. A superseded run must not declare recovery: an aggregate with no failure but a cancelled result proves nothing about the commit, because the cancelled job might have failed had it run to completion. This is the mirror image of CISEC-05-006, where the notify step drops the same exclusion because a genuine failure beside a cancelled sibling is still genuine.
CISEC-05-003 MUST Each failure notification issue MUST be tagged with the ci:main-failure label and a workflow-specific label (e.g., ci:workflow-python-app). The combination of both labels MUST uniquely identify the open failure issue for a specific workflow, enabling the composite action to find and update rather than duplicate. Within a workflow, exactly one job MUST run the notify step and the close step for its workflow-specific label; when more than one job can run in the same workflow run, that job MUST be a final job that needs: every other job that can run in it and keys on the needs.*.result aggregate (CISEC-05-002, CISEC-05-006).

The close step closes every open issue carrying both labels. When several jobs each run a notify/close pair under the same label, a job that passes closes the issue a sibling job just filed, and whichever job finishes last decides whether a red main stays reported (#4229).
CISEC-05-004 SHOULD A CI verification test SHOULD confirm that every qualifying workflow (push to main or schedule trigger) includes the .github/actions/notify-failure composite action step.
CISEC-05-005 SHOULD A workflow triggered on the issues: labeled event SHOULD enforce that the ci:main-failure label is bot-managed: if github.actor is not github-actions[bot], the label MUST be removed immediately. This prevents label spoofing that could suppress failure notification or create false alerts.

The ci:main-failure label is the deduplication key used by the composite action to find and update an existing failure issue rather than creating a new one. If a human or external actor applies the label manually, the signal is corrupted — the action may comment on an unrelated issue or fail to file a new one. Bot-only enforcement keeps the alerting signal reliable.
CISEC-05-006 MUST The failure-conditioned notify step (CISEC-05-001) MUST NOT file a failure issue on cancellation alone, and MUST file when any job in the run genuinely failed, even if a sibling job in the same run was cancelled. A step keyed on the GitHub Actions failure() status function already satisfies both, because failure() is false on cancellation. A step keyed on the needs.*.result aggregate MUST key on contains(needs.*.result, 'failure') alone: it MUST NOT file on cancellation directly (e.g. via &#124;&#124; contains(needs.*.result, 'cancelled')) and MUST NOT require the absence of cancelled (e.g. via && !contains(needs.*.result, 'cancelled')). Instead, every job that consumes another job's artifact MUST be skipped when its producer was cancelled (DEMOCI-04-007), so that a failure in the aggregate is always genuine.

A push-to-main run cancelled by the concurrency group (superseded by a newer merge) is not an actual CI failure — a newer run is authoritative and will file or close as appropriate. Two ways a cancelled run otherwise registers as a failure: (1) a guard that files on cancellation directly (e.g. &#124;&#124; contains(needs.*.result, 'cancelled')), and (2) a cancelled upstream job that leaves a downstream job to hard-fail on a missing artifact (#3249), laundering the cancellation into a failure result in the needs.*.result aggregate. The failure() status function is immune to both. The aggregate is not, but the remedy for (2) is to close the laundering path at its source — skip the consumer when the producer was cancelled — not to exclude every run that contains a cancelled result. A blanket !contains(needs.*.result, 'cancelled') cannot tell a laundered failure from a genuine one, so it suppresses the notification whenever a newer push cancels a sibling job after a real failure has already been observed: lint jobs cancelled alongside failing tests (#3293), or the invariant harness cancelled after a demo leg failed (#3294). The close-step guard (CISEC-05-002) keeps its cancellation exclusion because a superseded run must not declare recovery either.
Refines: CISEC-05-001
CISEC-05-007 MUST A failure notification issue filed by the composite action MUST carry the Bug issue type. The action MUST set the type itself, immediately after creating the issue, by resolving the repository's Bug issue type by name and applying it through the updateIssue GraphQL mutation; when a later notify run finds the open failure issue still untyped, it MUST apply the type then. It MUST NOT rely on applying a bug label for the label-to-issue-type workflow to convert, and it MUST fail visibly if no Bug issue type exists.

gh issue create cannot set an issue type, so a failure issue filed by the action was untyped and invisible to every issue-type query (#4038). The label-to-issue-type workflow cannot repair this: events raised with the workflow's own GITHUB_TOKEN never trigger other workflows, so a bug label applied by the action is never converted. Resolving by name survives a re-created type; re-typing on a later run repairs a transient mutation failure; a missing type is surfaced, not swallowed.
Refines: CISEC-05-001

Diátaxis documentation requirements

Brief requirements for organizing project documentation according to the Diátaxis framework.

File ID: DF

General architecture

ID Priority Requirement Related
DF-01-001 SHOULD Each reader-facing page SHOULD be one of the four Diátaxis content types — tutorial, how-to guide, explanation or reference — and declare it as its page_type (DF-11-018); the content types label pages and need not organize the site's navigation.

A reader arriving from search learns what kind of page they landed on from the page itself, while the navigation is free to group pages by subject and reader (ADR-0136). Keeping Diátaxis as a per-page property preserves its authoring discipline (DF-01-002 through DF-01-004) without making it the site's top level.
DF-01-002 MUST Ensure each documentation page maps to exactly one of the four user needs (Learning, Goals, Information, or Understanding).
DF-01-003 SHOULD NOT Documentation pages SHOULD NOT combine multiple Diataxis content types within a single page or section; where a combined view is temporarily unavoidable, the parts MUST be clearly separated and each part MUST link to the canonical full page of the appropriate type.
DF-01-004 SHOULD Use hyperlinks to connect related but distinct content types instead of embedding cross-type content.
DF-01-005 SHOULD Write documentation in Markdown and prepare it for the MkDocs site defined by mkdocs.yml.

Documentation maintenance

ID Priority Requirement Related
DF-02-001 SHOULD Build the documentation architecture iteratively from the inside-out by improving pages until their Diátaxis category is clear.
DF-02-002 SHOULD Maintain navigation structure to avoid link breakage when moving or renaming files.

Tutorials

ID Priority Requirement Related
DF-03-001 MUST Author tutorials as practical lessons that help learners acquire new skills through guided, hands-on experience.
DF-03-002 MUST Require that each tutorial is an activity where the learner learns by doing.
DF-03-003 MUST Ensure tutorials produce visible results early and often to maintain learner confidence.
DF-03-004 MUST Use first-person plural ("we") in the tutorial narrative to establish tutor/learner rapport.
DF-03-005 MUST NOT Forbid alternative learning paths within a single tutorial's primary flow; present a single prescribed learning path.
DF-03-006 MUST NOT Exclude in-depth explanations and theoretical discussions from the body of tutorials; link to explanatory pages for background.
DF-03-007 MUST Require every tutorial step to produce a comprehensible result that gives immediate feedback.
DF-03-008 MUST State at the start of each tutorial exactly what the learner will have accomplished.
DF-03-009 MUST Point out specific signs or changes the learner should notice during the tutorial.
DF-03-010 SHOULD provide explicit links to the related Reference and Explanation pages (prerequisites, background, deeper rationale). Keep these resources out of the tutorial body.
DF-03-011 MUST Tutorials MUST reserve first-person plural ("we") for the shared journey and use second person ("you") for the learner's own actions, possessions, and state.

DF-03-004 establishes "we" as the tutorial voice but does not say what it covers. The division of labor is what makes the voice work: "we" affirms the tutor/learner relationship, while "you" identifies who is acting and what they now have. Writing "we need Docker installed" misattributes the learner's prerequisite to the tutor, and pervasive "you should understand that" signals a tutorial drifting into Explanation.

How-to guides

ID Priority Requirement Related
DF-04-001 MUST Present how-to guides as goal-oriented directions that resolve a specific real-world task or problem.
DF-04-002 MUST Assume the reader is a competent user with required basic skills; surface prerequisites explicitly rather than embedding lessons.
DF-04-003 MUST Require how-to titles to state exactly what the guide shows the user how to do (e.g., start with "How to ...").
DF-04-004 MUST Use conditional imperatives (e.g., "If you want X, do Y") when the step applies only under a specific condition.
DF-04-005 MUST NOT Exclude pedagogical teaching, background history, and extended lessons from how-to bodies.
DF-04-006 SHOULD Prefer concise branching ("if this, then that") to prepare users for real-world complexity.
DF-04-007 MUST Omit unnecessary steps that do not contribute directly to the specified goal.
DF-04-008 SHOULD provide a short "Prerequisites" or "Starting points" section (or link to one) that handles different starting states and messy real-world setups instead of embedding multiple alternative flows.
DF-04-009 SHOULD A how-to guide MAY cover the same demonstration as a tutorial. When it does, it SHOULD lead with the task — what the reader's actor sends, in order, with its branches, and how to verify the result — and place the demonstration's invocation last, as confirmation. A how-to guide with nothing to place before the demonstration's invocation SHOULD be a tutorial instead.

Overlap across quadrants is not duplication when each page answers its own reader's question: "I need my actor to propose an embargo" is work, answered by the activities to send; "I want to see an embargo negotiation happen" is study, answered by the demo. Leading a how-to with the command the tutorial opens with writes it to the tutorial's question. DF-01-003 forbids mixing quadrants within a page and says nothing about two pages; this item covers the pair.
Refines: DF-01-003
DF-04-010 SHOULD When a how-to guide and a tutorial show the same demonstration, the section both pages show SHOULD be one {% include-markdown %} fragment that both include, and the how-to guide's level SHOULD be higher than the tutorial's.

Two copies of a demo invocation and its expected result drift apart; one fragment cannot (DF-10-002), and placement stays with each host page, so DF-04-009's ordering survives. A fragment with two hosts satisfies both hosts' quadrant rules (DF-09-008). A how-to assumes a competent reader (DF-04-002), and the tutorial is where that competence comes from, so the how-to sits above it and may link down to it (DF-11-002).
Refines: DF-04-009
Depends on: DF-11-002

Reference

ID Priority Requirement Related
DF-05-001 MUST Produce reference material that is neutral, objective, and technical in tone.
DF-05-002 MUST Structure reference content to mirror the physical or logical architecture of the system or code being described.
DF-05-003 MUST Keep reference content dry and free from instruction, discussion, or opinion.
DF-05-004 MUST State facts about behaviors, commands, parameters, versions, and limitations.
DF-05-005 MUST Use consistent, standard patterns and formatting in reference documents to make information easy to find.
DF-05-006 MAY Allow brief code examples in reference material only to illustrate usage, not to provide step-by-step instruction.
DF-05-007 MUST Reference material MUST make the system, not the reader, the grammatical subject of its descriptive sentences.

DF-05-001 and DF-05-003 require neutral, instruction-free description without saying how to achieve it. Naming the grammatical subject is the operational test: "A value of 0 disables the timeout" describes the machinery, while "Set timeout to 0 to disable it" has crossed into instruction and belongs in a How-to guide. The subject test catches the drift that "be neutral" alone does not.

Explanation

ID Priority Requirement Related
DF-06-001 MUST Provide explanatory material that focuses on understanding and context for a topic.
DF-06-002 MUST Address "Why?" questions by discussing history, design decisions, or technical constraints.
DF-06-003 MAY Permit opinion, perspective, and weighing of approaches in explanatory documents.
DF-06-004 MUST NOT Exclude practical instructions and dry technical descriptions that belong in Tutorials, How-to, or Reference.
DF-06-005 SHOULD Prefer titles that can be read as "About X" for explanatory topics.
DF-06-006 MUST Weave connections to related topics to build a web of understanding.
DF-06-007 MAY Explanation MAY use first-person plural ("we") for the author's reasoning and second person ("you") to invite the reader to consider a point.

DF was silent on Explanation voice, which left the most-used voice in the corpus formally unaddressed. Explanation is discursive and DF-06-003 already permits opinion and perspective, so both pronouns are legitimate here — "we chose a state-machine model because…" and "you might expect this to behave like a queue; it does not" each do work that impersonal prose cannot.

File metadata

ID Priority Requirement Related
DF-08-001 SHOULD Name Diátaxis specification files using snake_case and concise topic descriptors.
DF-08-002 SHOULD Place cross-reference sources only in the header metadata and use inline links for cross-spec references.

Prose style

Requirements governing the prose itself, as distinct from the architecture of the documentation set. The detailed rules, their SG-nn identifiers, and their worked examples live in .claude/skills/shared/docs-style-guide.md; these entries make that file normative and give reviewers citable IDs.

ID Priority Requirement Related
DF-09-001 MUST Hand-written prose under docs/ MUST follow the documentation style guide at .claude/skills/shared/docs-style-guide.md.

Style rules are numerous, example-heavy, and change more often than requirements do, so they are maintained as a guide rather than as several dozen spec entries. This entry is the anchor that gives the guide normative force and lets lint-docs findings cite a spec ID. Generated trees (docs/reference/code/, docs/reference/case_states/) and docs/adr/ are outside the guide's scope; fix the generator or the template instead.
DF-09-002 MUST Domain terminology in docs/ MUST match the canonical term in docs/reference/glossary.md, with docs/reference/vultron-taxonomy.md canonical for Vultron concept names.

"Avoid jargon" and "use domain language" are the same rule with the glossary as arbiter: a term in the glossary is domain language and should be used, and a term in neither the glossary nor the taxonomy is jargon that must be expanded, replaced, or registered. Naming one arbiter also prevents the three overlapping term sources from drifting further apart.
DF-09-003 MUST Acronyms MUST be expanded on first use on each rendered docs/ page, where "page" means the page as published rather than the source file it was authored in. Site navigation rendered by the theme (the sidebar and tabs) is not page content and is exempt. In a generated listing of child pages, each entry is one unit: an acronym in an entry's link label is expanded on first use when that entry's own description expands it.

First use resets per page because readers arrive from search results and deep links rather than by reading in nav order. Site-wide abbr tooltips do not survive print output and are invisible in raw Markdown. "Page" names the rendered artifact because an assembled page has one first use, not one per fragment (DF-09-007). A generated listing reuses navigation labels as link text, so each entry's description carries the expansion.
DF-09-004 MUST Every acronym used in docs/ MUST be registered in docs/_acronyms/index.md.

pymdownx.snippets auto-appends that file to every page, so a registered acronym gets a hover definition everywhere it appears. An unregistered one silently gets nothing.
DF-09-005 SHOULD A docs/ page SHOULD introduce, define, or link out for each concept before the sections that depend on it.

Prose is a directed acyclic graph of topics. A forward reference forces the reader to hold an undefined term until the page explains it, or to leave the page to find out. Across pages the dependency edge is a hyperlink from first use to the concept's canonical introduction, so the glossary and the taxonomy serve as the concept registry and no separate dependency file is required.
DF-09-006 SHOULD Every diagram in docs/ SHOULD be accompanied by prose stating what the diagram shows.

The page must stay comprehensible when the Mermaid render fails, in print-site output, and to a screen reader. The accompanying sentence also disciplines the author: a diagram whose point cannot be stated in a sentence usually has not earned its place.
DF-09-007 MUST Style rules whose scope is a page MUST be evaluated against the rendered page, and style rules whose scope is a sentence or a block MUST be evaluated against the source file that contains them, including source files that are {% include-markdown %} fragments rather than pages in their own right.

A fragment is prose, so exempting it leaves it unchecked: roughly 2,800 lines of the Protocol Specification went uncovered because its filenames carried a nav-exclusion prefix. But a fragment is not a page, so page-scoped rules applied to one yield artifacts of the split rather than prose defects — acronym first use (DF-09-003), concept order (DF-09-005), and any rule naming the H1 or nav label. .markdownlint-cli2.yaml set the precedent by disabling MD041 for this reason.
DF-09-008 MUST The Diátaxis quadrant of an {% include-markdown %} fragment MUST be taken from the pages that include it, and a fragment with more than one host MUST satisfy the quadrant rules of each host.

Quadrant selects the voice rules (DF-03-004, DF-04-004, DF-05-001, DF-06-007), and a fragment's own path does not determine it. docs/tutorials/worked_example.md and docs/topics/measuring_cvd/possible_histories.md are both included into the reference Protocol Specification's annexes, and includes/_rm-states-table.md has four host pages. Inferring quadrant from the fragment's directory would apply tutorial voice rules to reference content and vice versa.
DF-09-009 MUST A documentation check that acts as a blocking gate MUST report an empty target set as a distinct failure, and MUST NOT report success when it has resolved no targets.

A gate that checks nothing and reports success is worse than no gate, because it produces a false clean signal that suppresses the review the gate replaced. This is not hypothetical: the lint-docs gate in check-docs-sync resolved to zero targets for every fragment of the Protocol Specification and passed, and an American-spelling regression reached main through it.
DF-09-010 MUST Reader-facing docs/ MUST name a protocol activity by its formal protocol message name and code paired with its AS2 wire form — "Report Valid (RV) is implemented in ActivityStreams as Accept(Offer(VulnerabilityReport))" — and MUST NOT name it by its prototype class name. Where a shorthand has no formal name, or one shorthand expands across several activities, the plain-English name from the corresponding docs/reference/messages/ heading stands in for the formal name. The prototype class name MAY survive only as a non-rendered identifier, such as a mermaid node id.

Three layers name every activity — the formal protocol message, the prototype class, and the AS2 wire form — and only the first and last are reader-facing. The protocol semantics are not recoverable from the wire form: Accept(Offer(VulnerabilityReport)), TentativeReject(Offer(VulnerabilityReport)) and Reject(Offer(VulnerabilityReport)) are valid, invalid and closed, and nothing in the wire form says which. So substituting the wire form for the protocol name deletes a layer rather than translating one — the premise #3456 was originally filed on, and rejected. The prototype class name is dropped instead, because an implementer building a conformant peer in another language has no RmValidateReport. Naming the pairing here gives the ratchet in test/architecture/test_docs_activity_verbs.py a spec ID to cite, the same anchoring DF-09-001 provides for the style guide.

Sweep verification

Requirements that apply whenever a documentation sweep (naming sweep, Diátaxis extraction, page split, or any mechanical pass that moves or republishes existing prose) touches content in docs/. Faithful copying is insufficient — re-reading content in a new context is a different act from reading it in place, and a sweep carries source errors forward with full authority unless each moved claim is actively verified.

ID Priority Requirement Related
DF-10-001 MUST When a claim is moved, extracted, or republished in a new docs/ location, it MUST be verified against its authority (source code, specs/*.yaml entry, or docs/reference/ page) before the change is committed. Faithful copying is not verification.

A sweep's success criterion is usually "all content accounted for," which a faithful copy satisfies perfectly while propagating a false statement to a second location with more authority than it had in the first. Such a defect is invisible in the source context and becomes falsifiable only once it is read as a first-class assertion in the destination. Two sessions confirmed the mechanism (#3002 under docs/, #3342 over specs/).
DF-10-002 SHOULD When the same content is needed in more than one docs/ location, use an {% include-markdown %} fragment instead of copying prose — one authoritative source, multiple render points.

Copied prose drifts silently: one copy is updated, the other is not, and both pass lint because neither is wrong by itself. DRY is already a project standard (CS-22-001) and the tree already assembles pages from fragments, so this names the existing mechanism as the required one. An extracted fragment stays in lint-docs' target set for sentence- and block-scoped rules (DF-09-007), but the claims it carries are still verified at the moment of the move (DF-10-001). Placement and include-path mechanics: notes/documentation-sweeps.md.

Site information architecture — stakeholder types, levels, and routing

ID Priority Requirement Related
DF-11-001 MUST Every reader-facing docs/ page MUST declare an audience stakeholder_type and a prerequisite level in its YAML frontmatter, where stakeholder_type is a list whose members are drawn from cvd-practitioner, platform-developer, process-researcher, and project-contributor — or the bare scalar ALL, never [ALL], which MUST be used in place of a list naming every type — and level is one of 100, 200, 300, 400, or 500. A reader-facing page is a published page under docs/ that is neither a project working-record page (DF-11-003) nor an {% include-markdown %} fragment (DF-11-010).

Both axes must be machine readable so ordering can be checked and landing pages generated rather than hand-maintained. The key is list-valued because a page may serve exactly two audiences; requiring ALL over a full enumeration stops one fact having two forms. No member is a CVDRole value, making the reader-type versus case-role conflation structurally impossible. This item is the normative enumeration; its derivation chain is in notes/site-information-architecture.md.
DF-11-002 MUST A docs/ page MUST NOT depend on a page whose declared level is higher than its own, where depending means using a concept the higher-level page introduces without introducing, defining, or linking out for it.

This is the rule levels exist to support, and the reviewable ordering rationale that was previously absent: before it, no ordering decision could be shown to be wrong, so none could be reviewed and drift was undetectable. A violation means the page is misplaced, its prerequisites are unstated, or it is doing two jobs. It extends DF-09-005 from within a page to between pages, and SG-11's rule that a cross-page concept edge is a hyperlink supplies the escape: linking out satisfies the rule.
DF-11-003 MUST Project working-record pages — decision records, generated code documentation, exhaustively enumerated state pages, retained design-history pages, individual requirements-traceability records (the docs/reference/user_stories/ story pages and the traceability matrix that routes them, but not the stories' reader-facing index), and agent- and contributor-facing material such as docs/agents/, docs/developer/ and docs/reference/codebase/ — MUST be excluded from the reader-facing navigation while remaining published and reachable.

The working record is a different kind of content, not a greater depth of the same content. It is also the bulk of the navigated site, which is what makes the tree read as accretion. Research pages are not working record — they are reader-facing material for process-researcher and do carry a level (ADR-0102) — so they are outside this requirement. User stories are working record because each one records a requirement the implementation was traced against, read when a question about that requirement arises rather than in sequence; the reader-facing index that introduces them stays leveled (#3526). Exclusion is demotion, not retirement (#3281).
DF-11-012 MUST A project working-record page (DF-11-003) MUST declare stakeholder_type: [project-contributor] and MUST NOT be assigned a level.

Split from DF-11-003 because that item is verified against mkdocs.yml while this one is verified against page frontmatter, and bundling them let a tree satisfy the navigation half with every declaration still missing. A level on the working record is a category error, but it is addressed to somebody, so it declares a type. The absent level is then a property of one audience's material, not an exemption.
DF-11-004 MUST NOT A page's own level MUST NOT appear in its rendered content or in the site navigation. Documenting the level ladder itself — in the glossary, in an ADR, or in a shared fragment they include — is not a page declaring its own level and is unaffected.

Levels describe prerequisite depth, which is a property of the content, while readers sort themselves by goal: someone wiring a scanner into report validation thinks "how do I plug this in", not "I need a 300-level page". Exposing levels would ask a newcomer to rank their own knowledge before their first click, which is the same self-classification problem the Study/Work split already has. The reader sees a linear flow; the hierarchy that produced it belongs to the author.
DF-11-005 MUST Section landing pages that enumerate their section's contents MUST be generated from page frontmatter and gated by a check mode, and MUST NOT be hand-maintained. Every index.md that opens a navigation group, or that is the only navigation entry for a directory holding reader pages the navigation omits, MUST declare in its frontmatter which of three shapes it is: generated, the enumeration above; routing, an index whose links to its members are part of its framing or of a themed grouping that is editorial content, which is exempt from generation and MUST instead be checked to link every member of its section; or rendered, an enumeration produced at build time from a registry (DEMOCI-11-009), which is exempt because nothing is committed to drift.

Every section landing page was a hand-written duplicate of the navigation and every one had drifted from it — children missing, order disagreeing, and one describing a retired taxonomy to readers. The defect class is the duplication, so correcting individual omissions does not prevent recurrence. Two existing tools supply the pattern: vultron/metadata/adr/index_gen.py and demo-scenarios-sync (ADR-0098). The two exempt shapes exist because generation fixes an index that under-routes and cannot express an argument or a theme; what a routing page owes instead is completeness, which is derivable and therefore checked — the split DEMOCI-11-007 already makes between what is generated and what is checked. The declaration is required rather than defaulted so a sub-section cannot be left undecided by omission. Each index's decision is its contents: frontmatter, pinned by test/metadata/docs/test_site_sync.py.
DF-11-006 SHOULD A set of sibling leaf pages that is too large to browse SHOULD be reached through a routing page that links each member, with the navigation carrying the group rather than enumerating the members.

The pattern is already proven here and applied inconsistently: docs/reference/user_stories/ keeps its whole story set out of the navigation and reaches it through traceability.md. The same navigation meanwhile enumerates the decision records, the case states and the generated code pages inline. Same shape of content, opposite treatment; the routing- page treatment is the one to generalize.
DF-11-007 MUST A rendered docs/ page MUST remain self-sufficient for a reader arriving directly at it; repeated openings, restated prerequisites, and re-expanded acronyms on adjacent pages MUST NOT be removed from the rendered page in order to make those pages read as a continuous sequence. Sourcing a repeated block from one {% include-markdown %} fragment that each page includes satisfies this requirement.

Readers arrive from search results and deep links rather than in navigation order (DF-09-003, SG-12), so a block repeated between neighbors is usually the rule working, and removing it degrades every deep-link arrival. Nav order carries dependency and prominence, not narrative. The requirement is on what the reader sees, not on the source: a block one authority owns, such as a banner, a pointer, or a shared prerequisite list, is sourced once (DF-10-002) and still renders on every page.
DF-11-008 MUST A stakeholder_type × level coverage matrix MUST be generated from page frontmatter into an unpublished location outside docs/ and gated by a check mode that fails when the committed matrix disagrees with the tree. The check MUST NOT fail because a cell is empty.

The matrix turns "we do not serve audience X" from an impression into a recorded fact planning can act on. Gating currency rather than fullness keeps it a checked object rather than an advisory one; requiring fullness would harden into "every type at every level", which the divergence- above-100 shape contradicts. It sits outside docs/ because it is a planning artifact, and a per-page level table shown to readers would defeat DF-11-004 page by page.
DF-11-009 MUST NOT A page's own stakeholder_type MUST NOT appear in its rendered content or in the site navigation, and a routing page generated for a stakeholder type MUST be titled by the reader's situation instead. Documenting the vocabulary itself — in the glossary, in an ADR, or in a shared fragment they include — is not a page declaring its own audience and is unaffected.

Extends DF-11-004's reasoning one step. A reader should not have to classify themselves before their first click, and a situation is recognizable without asking anyone to identify with a label. It also keeps the enumeration free to change: cvd-practitioner may be split later on evidence, and a reader never taught the term loses nothing when it happens, whereas a published label would make the split a change to something readers had been taught.
DF-11-010 MUST NOT An {% include-markdown %} fragment MUST NOT declare stakeholder_type or level of its own; both are taken from the pages that include it, and a fragment with more than one host is governed by each host's declarations.

A fragment is not a page, so a page-scoped declaration applied to one yields artifacts of the authoring split rather than facts about a reader — the same reason DF-09-008 takes a fragment's quadrant from its hosts. Without this the validator's target set is ambiguous for the published-but-unnavved fragments under docs/includes/, and stakeholder_types.md would have to declare an audience for a table with no reader.
DF-11-011 MUST The shared fragment defining the stakeholder_type members MUST be generated from the frontmatter schema and gated by a check mode, and MUST NOT be hand-maintained.

Split from DF-11-005 because it is a different artifact with a different source: a landing page is generated from the frontmatter of many pages, while this fragment is generated from the schema that defines the vocabulary. Bundling them let an implementation satisfy one and leave the other neither met nor unmet. Generating it is what keeps DF-11-001's normative enumeration and the list published to readers from disagreeing, which hand-maintenance cannot guarantee.
DF-11-013 MUST A reading-path thread MUST be declared once, as data in the frontmatter of the start page that opens it, one thread per start page, as an ordered list of stages, where each stage has a title naming the reader's goal, may be marked as a stopping point, and is either an unordered set of reader-facing pages or an ordered chain of them, each member carrying its page path and a blurb written for this thread.

A reader's route through the site is a partial order: at some points order matters, and at others any of several pages will do. Ordered stages of sets or chains express that directly without forcing a sequence, and cannot contain a cycle. Declaring the thread once is what lets the start-page listing and every member page's directions be renders of one source (DF-10-002) instead of copies that drift.
DF-11-014 MUST A start page's listing of its thread MUST be generated from the thread's data and gated by a check mode, and MUST NOT be hand-maintained; framing prose outside the generated block stays hand-written.

The listing is an enumeration of the thread, the defect class DF-11-005 exists to remove when it is hand-maintained. Generating it into the committed source keeps the diff showing what readers will see, the way generated landing pages do.
DF-11-015 MUST Every page that is a member of a thread MUST show, for each thread it belongs to, the set of thread pages immediately before it, the set immediately after it, and a link to the thread's start page, titled by the reader's situation rather than by stakeholder_type (DF-11-009), and the rendered page MUST be the same for every reader. The set before a page is the preceding member of its chain, or else the previous stage's exit pages; the set after it is the next member of its chain, or else the next stage's entry pages. A set stage's entry and exit pages are all its members; a chain's entry page is its first member and its exit page its last.

A reader who followed a thread into a page needs directions onward without being forced down one next page, so predecessors and successors are sets. A page on several threads lists each, so it never needs to know which thread a reader is on: no query parameter, script or referrer is involved, the page renders the same on a deep link, and it still stands alone (DF-11-007).
DF-11-016 MUST A thread check MUST fail when a thread lists a page that does not exist or is not reader-facing (DF-11-001), when a page appears more than once in one thread, or when it resolves no threads at all (DF-09-009).

A thread that routes a reader to a missing or working-record page is a broken route, and a page listed twice in one thread has two places in a partial order. The build's broken-link check catches only the first, and only on the start page.
DF-11-017 MUST NOT A page that is a member of a thread MUST NOT show the theme's navigation-order previous and next footer.

The footer follows navigation order, not the thread, so on a member page it would offer a second "next" link that can disagree with the thread's own directions (DF-11-015).
DF-11-018 MUST Every reader-facing docs/ page (DF-11-001) MUST declare exactly one page_type in its YAML frontmatter, one of tutorial, how-to, explanation, reference or routing, where routing is for a page that mainly gives directions — the home page, the start pages, and indexes that only route. A page's page_type is independent of its contents: shape (DF-11-005): an index whose prose mostly explains is explanation. A project working-record page (DF-11-003) and an {% include-markdown %} fragment (DF-11-010) MUST NOT declare a page_type.

Once the navigation stops grouping pages by Diátaxis type, the type survives only as a declaration on the page. A required key makes a missing one a fault rather than a silent gap, for the same reason contents: is required on landing pages. The working record's door already says what kind of page it is, and a fragment takes its declarations from its hosts.
DF-11-019 MUST The built site MUST show, on every reader-facing page whose page_type (DF-11-018) is not routing, a badge naming that type, rendered from the page's own frontmatter, and MUST show no badge on a routing page. A page-type badge MUST NOT be written by hand beside a link unless a check fails when it differs from the linked page's declared page_type.

A badge tells a reader arriving from search what kind of page they landed on. A badge read from the page's own declaration cannot drift from it; a hand-written badge beside a link restates the target's declaration and drifts silently when the target changes type. A routing page only gives directions, so a badge there would be noise.
DF-11-020 MUST NOT A navigation section MUST NOT hold both a page about the Vultron protocol and a page about the reference implementation.

A reader who wants the protocol must be able to trust that every page in a protocol section binds them or explains what binds them; a reference- implementation page beside them reads as part of the protocol. The nav is the only place the two are grouped together. The Diátaxis-type sections predate the rule and mix the two until #4684 replaces them; until #4946 adds a check over the nav, it is a review rule for new and moved pages.
DF-11-021 MUST The site MUST have one lookup page, linked from the home page and from every start page, that lists the reader-facing pages whose page_type (DF-11-018) is reference, grouped by top-level navigation section in navigation order, each with its description:. The lookup page MUST NOT be a start page (DF-11-013). A reference page inside a navigation group below the top level MUST be listed once, through the index.md that opens the outermost such group containing it; a reference page with no such group, or whose groups below the top level have no opening index.md, is listed on its own. A reference page the navigation omits MUST be placed by the navigation-reachable index.md of its own directory and then follow the same rule. The listing MUST be generated and gated by a check mode that also fails when a reference page has no such entry or when the home page or a start page does not link the lookup page.

Once Reference is no longer a top-level tab (ADR-0136), a returning reader loses the view by type. One generated page gives it back without a type-based top level. Listing a subgroup through its index keeps the page short enough to scan (DF-11-006), and deriving it from the nav and frontmatter keeps it from drifting (DF-11-005).

Protocol Specification normative text rendered from the requirement tiers

The Protocol Specification presents the protocol and foundation requirement tiers of specs/*.yaml as rendered content, never as restated prose, so the specification cannot state a requirement differently from the YAML and presents every one of them; the normative banner marks exactly the normative set (ADR-0136, ADR-0147).

ID Priority Requirement Related
DF-12-001 MUST The Protocol Specification MUST contain a Requirements Catalog body section, placed after the Conformance section and before the back matter, that renders every requirement of kind protocol or foundation exactly once. A catalog subsection MUST be one requirement file, and foundation requirements MUST be rendered on a catalog page of their own.

The specification is the complete normative text only if every rule a conformant implementation must obey renders in it, and normative content belongs in the body rather than in an annex, which the specification declares informative. Keeping the inherited foundation rules apart keeps visible what Vultron adds and what it takes unchanged from the standards beneath it.
DF-12-002 MUST The anchor of a protocol or foundation requirement MUST be defined only by its Requirements Catalog row and by that row's copy in the single-page edition of the specification (full.md), which includes the catalog in document order. Every other page that shows or links the requirement, including the inline callouts of DF-12-003 and the requirement pages for the other kinds, MUST link to that row; on the single-page edition the link MUST target the in-page copy.

A requirement with two published anchors gives two answers to where it lives, and links written against either can disagree. One home also lets the retired per-kind pages for the two tiers redirect to a single place.
DF-12-003 MUST Outside the Requirements Catalog, the Protocol Specification MUST present a normative requirement only as a callout rendered by ID from its YAML entry, in the catalog's row format. Rendering a callout MUST fail the documentation build when an ID names no requirement of kind protocol or foundation.

A callout shows the YAML's own words, so the narrative cannot paraphrase a rule into something different. Failing on a non-normative ID stops the specification from presenting a Project or Architecture rule as protocol.
DF-12-004 MUST NOT A fragment of the Protocol Specification MUST NOT contain an RFC 2119 keyword outside a rendered requirement block, except in the §1.4 Document Conventions paragraph that states the keyword convention. A check MUST enforce this, and MUST report an empty target set as a failure (DF-09-009).

Every hand-written normative sentence is a restatement that can drift from the YAML, and an unlabeled one may have no YAML counterpart at all. Banning the keywords in prose makes the YAML the only place a normative sentence can be written.
DF-12-005 MUST The placement of requirement files on Requirements Catalog pages MUST be declared once, on the documentation side, without a field in the requirement files. A check MUST fail when a requirement file that holds protocol or foundation requirements is placed on no catalog page, or on more than one catalog page for the same tier.

A new requirement file would otherwise be silently missing from the catalog. Declaring the placement where it is rendered keeps the requirement file format unchanged, and one declaration lets callouts and the other kinds' pages find the catalog page that holds a requirement.
DF-12-006 MUST Every page of the normative set — the Protocol Specification's body section pages and the protocol and foundation requirement-tier pages, but not the specification's landing page, its single-page full view, its annexes or its open-questions page — MUST include the shared normative banner (docs/includes/normative.md) in the page itself, not through a shared fragment, and every other page MUST NOT include it except the page that documents the banners. The normative-set pages MUST be declared once, in the check that enforces this, and the check MUST fail when a declared page does not exist.

A banner a reader trusts must be true. Hand-placed banners drifted: 19 pages outside the normative set still called themselves normative after ADR-0136 and ADR-0147 fixed what the set is. Failing on a missing declared page keeps the list current when the Requirements Catalog replaces the per-tier pages.
DF-12-007 SHOULD An informative page that explains the protocol's design SHOULD carry the shared "specification governs" banner, which states that the page is informative and that where it differs from the Protocol Specification the specification governs, instead of the not-normative banner, which says a page is not part of the protocol.

Pages such as Formal Protocol and the RM, EM and CS process models still explain how the protocol is defined, but they predate the specification and may lag it. Neither the normative banner nor the not-normative banner is true of them; a precedence statement keeps their standing and names the tiebreaker.

History File Management

Defines the structure, location, entry format, and tooling for managing project history entries. For implementation and idea types whose --source resolves to a GitHub issue number, append-history posts the entry body as a comment on the corresponding GitHub issue rather than writing a file. learning and priority types continue to write per-entry write-once files under plan/history/, managed by the append-history CLI tool.

File ID: HM

History File Location and Structure

ID Priority Requirement Related
HM-01-001 MUST History entries for learning and priority types MUST reside under plan/history/ as per-entry write-once files.
HM-01-006 MUST NOT The top-level plan/ directory MUST NOT contain any *HISTORY.md files.
HM-01-002 MUST Each learning or priority history entry MUST be stored as an individual write-once file at the path plan/history/YYMM/<type>/<entry-id>.md, where YYMM is the two-digit year and two-digit month when the entry was created, <type> is the entry type (see HM-02-002), and <entry-id> is the originating task or idea identifier (e.g., TASK-BTND5). When that path is already taken — one work item may produce several learnings — the entry MUST be written to <entry-id>-<slug>.md, where <slug> is the incoming file's YYYYMMDD-SLUG stem (BW-01-003). An existing entry file MUST NOT be overwritten in either case. implementation and idea entries whose --source resolves to a GitHub issue number use the GitHub comment output mode (HM-08) instead.

<entry-id> derives from source, which is not unique: BW-01-003 names incoming files YYYYMMDD-SLUG specifically so that several PRs against one issue can each record an observation without colliding. Keying the archive on source alone therefore made the second learning for any work item unarchivable, which surfaced during the 2026-09-02 queue audit when 15 of 48 entries failed to archive.
Depends on: BW-01-003
HM-01-003 MUST After each append-history invocation, the tool MUST regenerate a local plan/history/YYMM/README.md index from entry frontmatter (see HM-03-006). This file is a gitignored local cache — it MUST NOT be committed to the repository. To view the history index, use uv run show-history [--month YYMM &#124; --all].
HM-01-005 MUST NOT Once an entry file has been merged to main, it MUST NOT be modified. History entries are immutable records. An entry MAY still be corrected while it remains unmerged on the branch that created it, because until then no record has been published. The plan/history/YYMM/README.md index may be regenerated locally but is gitignored.

Immutability exists so a published record cannot be rewritten in hindsight. Worded as "once written" it bound the entry from its first commit, forcing any branch whose plan changed during review to choose between an immutable record that was now wrong and a correct one that broke the rule. PR #3523 is the witness. Anchoring on the merge to main keeps everything published immutable while letting a branch land an accurate first version (#3561).
HM-01-007 MUST History entries for implementation and idea types whose --source resolves to a GitHub issue number MUST be posted as a comment on the corresponding GitHub issue (see HM-08).

GitHub comment mode keeps implementation and idea records co-located with the issue they describe, avoiding file churn in plan/history/.
Refines: HM-01-001
HM-01-008 MUST NOT History entries for implementation and idea types whose --source resolves to a GitHub issue number MUST NOT be written as files under plan/history/.

Writing these entries as files would duplicate content already posted as GitHub comments and pollute the plan/history/ directory with redundant records.
Refines: HM-01-001

Entry File Format

ID Priority Requirement Related
HM-02-001 MUST Every history entry file MUST begin with a YAML frontmatter block containing at least the following fields: title (human-readable summary), type (one of the valid HistoryEntryType values), timestamp (tz-aware ISO 8601 datetime string in UTC, e.g., "2026-05-01T15:30:00+00:00"), and source (the originating identifier such as an IDEA-ID or TASK-ID). The legacy date field (ISO 8601 date string) is accepted for reading pre-migration entries but MUST NOT be written by the tool for new entries.
HM-02-003 MUST The Markdown body following the frontmatter MUST contain the full human-readable text of the history entry (e.g., the archived idea text, the completed task summary, or the priority change record).
HM-02-004 MUST The plan/history/YYMM/README.md index MUST be auto-generated by the append-history tool (see HM-03-006) and MUST NOT be manually edited. Its content is derived exclusively from the frontmatter of entry files in the same monthly directory. The file is gitignored and serves only as a local convenience cache.

append-history CLI Tool

ID Priority Requirement Related
HM-03-012 MUST Normal skill and agent guidance MUST use the default current-date flow.

Agents must not diverge from the standard flow by choosing a target month manually; the tool determines the directory from the system clock.
Refines: HM-03-004
HM-03-013 MUST NOT Normal skill and agent guidance MUST NOT require agents to choose a target month manually.

Manual month selection is an error-prone step that the tool eliminates by deriving the directory from the current system clock.
Refines: HM-03-004

Agent Context Boundaries

ID Priority Requirement Related
HM-04-001 MUST NOT plan/history/ MUST NOT be included in the default "read plan context" step that agents perform at the start of every workflow. Agents MUST read only plan/*.md (excluding plan/history/) during their standard orientation phase.
HM-04-002 SHOULD Agents SHOULD read plan/history/ only when specifically investigating historical changes, auditing completed work, or extracting lessons from prior implementation phases.
HM-04-003 MUST The orient-agent skill documentation MUST explicitly state that its plan-context reading covers plan/*.md but NOT plan/history/. Agents following this skill MUST NOT recursively read plan/history/ during orientation.

Skill and AGENTS.md Integration

ID Priority Requirement Related
HM-05-001 MUST The build skill MUST use uv run append-history implementation to write completion summaries.
HM-05-002 MUST The plan-issue skill MUST use uv run append-history idea to archive processed ideas.
HM-05-003 MUST The learn skill MUST NOT read plan/*HISTORY.md files directly. When historical context is needed, the learn skill MUST invoke deepen-context and let that skill mediate which plan files are relevant to load.

Timestamp Field and Future-Date Rejection

ID Priority Requirement Related
HM-06-001 MUST Every new history entry MUST include a timestamp field containing a tz-aware ISO 8601 datetime string in UTC (e.g., "2026-05-01T15:30:00+00:00").
HM-06-005 MUST All timestamp values MUST be timezone-aware.

Protocol Shorthand to MessageSemantics Mapping

Authoritative normative bridge from protocol message shorthand labels (RS, EP, CV, etc.) used in behavioral conformance specs to their MessageSemantics enum values and corresponding AS2 wire-format specifications. Each entry provides one row in the three-hop chain: shorthand to MessageSemantics enum value to AS2 wire form. The correspondence is many-to-many in both directions: several shorthands may collapse onto one wire activity (CV/CF onto vfState, CP/CX/CA onto pxaState, EV/EJ/EC onto EP/ER/EA), and one shorthand may expand across several wire activities (GI). Shorthands with no dedicated dispatch value are the protocol-layer error and acknowledgment signals; these are not unimplemented — the purposes they name are served by mechanisms partitioned on a different axis, specified normatively in MSM-05.

File ID: MSM

Fault and Acknowledgement Mechanism Evolution

Normative record of how the formal protocol's per-model error and acknowledgement shorthands (RE, EE, CE, GE and RK, EK, CK, GK) are realized in the AS2 wire vocabulary. The formal set partitions both concerns by which state machine the message belonged to; the implementation partitions faults by failure mode and acknowledges replicated state cumulatively. These are orthogonal axes, so the shorthands have no one-to-one wire counterparts even though the behaviours they describe are implemented.

ID Priority Requirement Related
MSM-05-004 MUST NOT Documentation MUST NOT describe the wire fault taxonomy as RmError, EmError, CsError, or GmError discriminated by as:inReplyTo; those types exist in no ontology file and no code.

docs/howto/activitypub/activities/error.md previously depicted this taxonomy, and it was never built. Citing it leads implementers to a wire format that does not exist. Note also the name note: VultronError in vultron/errors.py is a Python exception base class, unrelated to protocol wire-level error messages.

Message Type Reference Rendering

Requirements governing the consolidated per-message-type reference pages under docs/reference/messages/, so that the shorthand-to-wire mapping presented to implementers cannot drift from this specification or from the semantic registry.

ID Priority Requirement Related
MSM-06-001 MUST The mapping tables in docs/reference/messages/ MUST be committed include-markdown fragments generated from SEMANTIC_REGISTRY, together with the occasion table YAML they are rendered from, and MUST NOT be hand-written or rendered by a build-time markdown_exec block. A pre-commit --check hook MUST regenerate the YAML and every fragment and fail on any difference from the committed files, including a committed fragment that nothing generates.

Hand-maintained mapping tables drift silently, as the orphaned examples of issue 2904 showed. The tables are generated and committed rather than rendered at build time because they are mostly links, and MkDocs neither rewrites nor checks a .md link a markdown_exec block prints (ADR-0128). A generated file nothing compares with a fresh generation is a hand-edited one; the hook makes divergence fail at commit time.
MSM-06-002 MUST Every SEMANTIC_REGISTRY entry MUST be designated to exactly one primary page under docs/reference/messages/ and MUST appear on that page, or be explicitly exempted as a dispatcher fallback rather than a protocol message. An entry MAY additionally be cross-referenced from other pages, and MUST be where a collapse or an evolved mechanism gives it a second home.

Without a completeness check a new registry entry can be added without ever being documented, so every entry needs one page that owns it. A bare "exactly one page" rule would be unsatisfiable, because the mapping is many-to-many — see docs/reference/messages/ for the entries that legitimately need a second home. A primary page keeps the completeness guarantee without contradicting MSM-06-003. The dispatch fallbacks are the intended exemptions.
MSM-06-003 MUST Each occasion section on a message page MUST state its entry's mapping status, indicating whether the relationship is direct, a collapse of several shorthands onto one wire activity, an expansion of one shorthand across several wire activities, a mechanism that evolved onto a different axis per MSM-05, or none, and the Message Types page MUST define each status value. The occasion table need not carry the status, because its columns serve a sender choosing what to send.

Reference material must state what is true, including divergence from the normative message set. Documentation that shows only direct mappings and omits the rest reads as a complete correspondence and misleads implementers about both the collapses and the evolved mechanisms. The status sits in each section, beside the formal definition it qualifies, rather than in a table column a sender does not need.
MSM-06-004 MUST The Protocol Quick Reference's "Message Types at a Glance" section MUST include the whole occasion table and a table of the formal message set, both as committed fragments generated from SEMANTIC_REGISTRY (MSM-06-001), and MUST carry no hand-written table. The formal table MUST list each shorthand with the wire activities whose occasions carry it, or state that it has none.

The section's hand-written table gave an emit trigger and a response for every shorthand, including the seven that faults and hash-chain acknowledgment replace on the wire, and no check compared it with the registry. ADR-0128 kept that table beside a link to the occasion table, which then gave a second, ungated answer to what is sent when.
Depends on: MSM-06-001

Occasions

Requirements for the occasion, the unit of the message lookup table: a situation in a case that a sender conveys with a wire activity, sometimes narrowed by a distinguishing field value, state context or sender standing (glossary, Messaging and Protocol). One SEMANTIC_REGISTRY entry has one or more occasions. Occasions are declared on the entry, and the lookup table and the per-occasion reference sections are renderings of them. Decision: ADR-0128.

ID Priority Requirement Related
MSM-08-001 MUST Every SEMANTIC_REGISTRY entry outside the dispatcher-fallback exemptions (MSM-06-002) MUST declare at least one occasion, and each occasion MUST state when it applies, a stable anchor ID, a description and the example that renders it. Construction of a non-exempt entry with no occasion MUST fail.

The lookup table is complete only if every wire activity has a row. A construction-time failure makes a new registry entry without an occasion impossible, not merely detectable, so completeness does not depend on a ratchet someone remembers to extend.
MSM-08-002 MUST An occasion's distinguisher MUST be a field value, a state context or a sender standing. A field value MUST be a member of that field's enum, a state context MUST name a state machine and states that are members of that machine's state enum, and a sender standing MUST name a SenderEntitlementKind member and whether the sender holds it.

"Distinguished by" is what tells two occasions of one wire activity apart when they share an addressee (MSM-08-009), such as vendor awareness and fix ready on Add(ParticipantStatus). A free-text distinguisher can name a value no implementation sends. Typing it against the enums makes a renamed or retired state fail at construction. Some occasions differ only in who sends them: the Case Owner's request to invite an actor and a participant's suggestion are one Offer(Actor), and the CASE_MANAGER answers them differently (CM-17-007, CM-16-003). The sender's standing is typed by the entitlement kinds a receiver already checks (HP-01-009), so it has one vocabulary and its wording comes from the kind (MSM-08-010).
Depends on: MSM-08-010
MSM-08-003 MUST Each occasion's example MUST dispatch to the occasion's own SEMANTIC_REGISTRY entry, and an occasion whose distinguisher is a field value MUST have an example that carries that field value.

Dispatch alone does not tell occasions of one entry apart. When this group was planned, the vendor awareness, fix readiness and fix deployed sections rendered the same example, which dispatches correctly and carries the vendor awareness vfState, so the other two showed a value that is not theirs. Each occasion needs an example that shows what makes it that occasion. No example can carry a state context, such as the receiver's EM state or the CASE_MANAGER's register status of the named embargo.
MSM-08-004 MUST Occasion anchor IDs MUST be unique across SEMANTIC_REGISTRY, each anchor MUST resolve to exactly one section on the entry's message page, and every occasion section MUST be referenced by a row of the lookup table.

The table links each row to its details by anchor. A duplicate anchor links two rows to one section, and an unreferenced section is reference content no reader can reach from the table.
MSM-08-005 MUST The lookup table's "Sent by" value MUST be derived from the side of the sender entitlement that the occasion's received use case declares (HP-01-007, HP-01-008) matching the occasion's addressee (MSM-08-009), worded from that side's entitlement kinds (MSM-08-010), MUST state an exempt use case as not yet checked, and MUST state an entry with no received use case as not handled (SE-05-002). An occasion MAY add the CVD roles that usually send it, which MUST be values of CVDRole.

The entitlement is the rule a receiver applies, so it is what another implementation must satisfy, and deriving it means it cannot drift from the code. It is often broader than the CVD convention: any active participant may announce that a fix is ready, though in practice a vendor does. The usual-role note gives the convention without replacing the rule. Positions such as the invitee are entitlement kinds (HP-01-009), so they appear in the derived value.
Depends on: HP-01-007
Depends on: HP-01-008
MSM-08-006 MUST The mapping status that MSM-06-003 requires MUST be derived from the occasions and their shorthands, and MUST NOT be declared per entry, except that an occasion MAY declare a mechanism that evolved per MSM-05.

A declared status is a second copy of what the shorthands already say, and the two disagreed: EP was tagged as expanding to Create(Event) and Announce(Event), where MSM-02-001 maps it to Invite(Event) alone. An evolved mechanism has no shorthand to count, so it alone is declared.
Refines: MSM-06-003
MSM-08-007 MUST An occasion's "when" text MUST describe the sender's situation, for protocol mechanics and for a Create that precedes an Add as well as for CVD events, and every occasion's prose fields MUST follow the documentation style guide (DF-09-001), use the glossary's canonical terms (DF-09-002) and address no reader.

One grammar lets a reader scan the "When…" column by situation. The prose lives in Python, outside the reach of DF-09-001 as written, yet renders on reference pages and in how-to guides; a fragment with more than one host must satisfy the rules of each (DF-09-008), so it can use neither the how-to "you" nor reference-only phrasing.
Extends: DF-09-001
Depends on: DF-09-008
MSM-08-008 MUST A lookup-table row's details link MUST name the formal protocol message and its shorthand when the occasion has a shorthand that expands to no other entry, and otherwise the plain-English heading of the occasion's section.

DF-09-010 requires reader-facing docs to name a protocol activity by its formal name and code paired with its wire form. The row supplies the wire form in "Send", so the link text supplies the name, for example "Fix Readiness (CF)". Where one shorthand expands across several entries, as GI does, DF-09-010 lets the plain-English heading stand in, since the formal name would label every row of the expansion alike.
Depends on: DF-09-010
MSM-08-009 MUST Each occasion MUST declare its addressee as one of the sides a sender entitlement can have (the CASE_MANAGER, a participant, or an actor outside any case for an activity sent before a case exists), and the lookup table MUST render it as the row's "Send to" value. An occasion of a non-exempt use case whose addressee is not a side of that use case's declared rule MUST fail.

An emitter looks up its own situation. One wire activity can carry two occasions with different senders: a participant proposing an embargo sends Invite(Event) to the CASE_MANAGER, and the CASE_MANAGER asking a participant about a proposal sends Invite(Event) to that participant. Both match one registry entry and one use case, so only the addressee tells the two rows which side of the rule gives their sender. See ADR-0129.
Depends on: HP-01-008
MSM-08-010 MUST Each entitlement kind MUST declare the wording a "Sent by" value uses for it and the glossary term that defines it, and that term MUST exist in the glossary. The addressee kind's wording MUST name the request from the object type of the activity being answered.

A wording table kept apart from the kinds is a second copy that drifts when a kind is added. Declaring the wording on the kind makes a new kind incomplete until it can be rendered and defined. The addressee kind serves every request type but an Invite, so its wording takes the request from the answered activity's object type to stay identified ("the addressee of the case proposal") without a kind per request. See ADR-0129.
Depends on: HP-01-009
MSM-08-011 MUST Each occasion that shares a SEMANTIC_REGISTRY entry and an addressee with another occasion MUST declare a distinguisher, and the distinguishers of such occasions MUST differ. Two such distinguishers of one kind on one field, state machine or entitlement kind MUST NOT overlap. They MUST share no field value or state, and two sender standings MUST differ in whether the sender holds the kind. Construction of a registry that breaks this MUST fail.

The lookup table is how a receiver finds which occasion an activity is. Two same-addressee occasions with no distinguisher render as two rows only prose separates, as on Offer(Actor) and Announce(Event) (#4821). Distinguishers that overlap, such as EM NONE and PROPOSED against PROPOSED, still let one activity match two rows. Different fields or machines are not compared, since one activity may carry both values by design.
Depends on: MSM-08-002
Depends on: MSM-08-009

Meta-Specification Style Guide

Normative rules for authoring specification files in this project. Covers file structure, requirement format, IDs, writing rules, cross-references, lifecycle, quality criteria, and the delineation between spec files and ADRs.

File ID: MS

File Structure

ID Priority Requirement Related
MS-01-001 MUST Each spec file MUST focus on a single topic of concern and be largely independent of other spec files.

Focused files are easier to navigate and maintain.
MS-01-002 MUST Requirements MUST be grouped by category within each spec file; each group MUST address a single, cohesive area of concern.

Grouping by category improves navigation and traceability.

Priority Levels

ID Priority Requirement Related
MS-02-001 SHOULD Requirement statements SHOULD include the applicable RFC 2119 strength keyword inline (e.g., 'The system MUST…') for human readability, in addition to the priority: field.

The priority: field is the authoritative record of requirement strength; inline keywords aid readability but are advisory.
MS-02-002 MUST Each spec entry MUST have a priority: field whose value is one of the RFC 2119 vocabulary keywords: MUST, MUST NOT, SHOULD, SHOULD NOT, or MAY.

The priority: field is the machine-readable record of requirement strength. The Pydantic schema enum is the implementation of this normative requirement, not its source.
MS-02-003 MUST Any spec requirement, lint check, ratchet, or tool that selects requirements by priority MUST treat a negative keyword as the same tier as its positive: MUST_NOT with MUST, and SHOULD_NOT with SHOULD.

RFC 2119 makes MUST NOT the same absolute strength as MUST, and SHOULD NOT the same strength as SHOULD; a prohibition is no less binding than an obligation. A gate that names only the positive keyword silently drops every prohibition. See notes/spec-authoring-rules.md for the gaps this left in the corpus.
Refines: MS-02-002
MS-02-004 MUST Code that selects spec requirements by priority MUST obtain the MS-02-003 tiering from a single shared definition on RFC2119Priority, not by comparing against individual enum members.

Selecting with == RFC2119Priority.MUST is easy to write and fails silently, and each hand-written member list (backstop/_model.py has one) re-decides which members count. One shared definition means the tiering is decided once. Display code that maps each member to a label is not a selection and is out of scope.
Refines: MS-02-003

Requirement Format

ID Priority Requirement Related
MS-03-001 MUST Each spec entry MUST contain exactly one normative statement; compound or multi-condition requirements MUST be split into separate entries.

Atomic requirements are independently testable and traceable.
MS-03-002 MUST Every requirement MUST have a unique ID.

IDs enable stable cross-referencing.

Requirement IDs

ID Priority Requirement Related
MS-04-001 MUST Requirement IDs MUST follow the format PREFIX-NN-NNN where PREFIX is the file-level prefix, NN is a two-digit group number, and NNN is a three-digit requirement number.

Consistent format enables automated validation.
MS-04-002 MUST Requirement IDs MUST be globally unique across all spec files.

Uniqueness prevents ambiguous cross-references.
Implements: SR-01-006
MS-04-003 MUST Requirement IDs MUST be stable; once assigned, an ID MUST NOT be renumbered.

Stable IDs preserve traceability in git history.
MS-04-005 MUST Superseded requirements MUST be removed entirely rather than left in place as deprecated.

Deprecated-but-present requirements create confusion for agents and future implementers. The spec schema's deprecated and superseded_by fields are not a sanctioned lifecycle state; they are being retired, with the entries that use them, by #4166.
Refines: MS-04-003
MS-04-004 MUST The group ID prefix of a spec ID MUST match the group it lives in (e.g., spec HP-01-001 MUST live in group HP-01).

Mismatched prefixes break navigability and cross-reference accuracy.
Implements: SR-01-013

Writing Rules

ID Priority Requirement Related
MS-05-001 MUST Requirements MUST be concise; prose explanations MUST NOT appear inline in requirement statements.

Concise statements reduce ambiguity.
MS-05-002 MUST Requirements MUST be specific; vague terms such as 'appropriate' or 'robust' MUST NOT be used unless explicitly defined.

Specificity makes requirements testable.
MS-05-003 MUST Requirements MUST be testable; measurable acceptance criteria MUST be provided where applicable.

Untestable requirements cannot be verified.
MS-05-004 MUST Requirements MUST be declarative; they MUST state what the system must do, not how, unless the how is itself normative.

Declarative requirements avoid constraining implementation unnecessarily.
MS-05-005 MUST A requirement written to guard against an implementation mistake MUST state the domain truth the mistake violates, in the vocabulary of its kind; it MUST NOT be phrased as a prohibition of the mistake, or as a correction of the mistake in another kind's vocabulary.

A bare prohibition reads as if the mistake were a plausible design, and once SR-11-003 extends to protocol MUST_NOT (#4517) it needs a contrived story. The positive truth is design an implementer can follow, and the regression test verifies it. RSH-08-001 is the worked case: the rule is that a participant's assertion about itself is about itself, not about another actor, and the mistake it guards against recurred (ADR-0089, CONCERN-3473). The bug that prompted such a requirement belongs in its rationale; the requirement carries no marker, tag or field saying it is a guard. A guard with no domain truth behind it is not protocol (MS-12-003), or is only a regression test.

Cross-References and Sources

ID Priority Requirement Related
MS-07-001 MUST Cross-references between spec entries MUST use the relationships: list with a rel_type field (one of the defined vocabulary terms) and a spec_id field referencing the target requirement ID.

Structured cross-references in the relationships: field enable automated graph analysis and linting of dangling references.
MS-07-004 MUST Source attribution MUST appear only in the file header, not repeated on individual requirements.

Per-requirement sources create maintenance overhead.

File Naming

ID Priority Requirement Related
MS-08-001 MUST Spec files MUST use kebab-case.yaml naming; specs/README.md is the sole exception and MUST remain Markdown.

Consistent naming enables glob patterns and automated tooling.
Implements: SR-01-001

Lifecycle of Requirements

ID Priority Requirement Related
MS-09-001 MUST Superseded requirements MUST be removed from spec files rather than marked deprecated or left in place with an annotation.

Deprecated-but-present requirements are agent noise and may cause incorrect behaviour. A spec states current understanding; git and the implementation history (MS-09-002) carry what it used to say. The spec schema's deprecated and superseded_by fields are not a sanctioned lifecycle state; they are being retired, with the entries that use them, by #4166.
MS-09-002 MUST The removal of a requirement MUST be recorded in the implementation history so there is an audit trail.

Audit trail preserves context for future maintainers.
MS-09-004 MUST A requirement ID MUST NOT be reused. Once an ID has been removed (MS-09-001), no later requirement, in any spec file, MAY carry it, and a rule that replaces a removed one MUST receive a new ID.

Citations outlive requirements: code comments, tests, ADRs, PR bodies and history entries cite IDs. A reused ID silently re-points every one of those at a rule they never meant, and the reuse of CM-11-005 with the opposite force (found in the 2026-10-02 audit) did exactly that. A new ID costs nothing; a stale citation that still resolves is undetectable.
Refines: MS-09-001
MS-09-005 MUST The text of every removed requirement MUST be preserved in an archive of retired requirements, one file per requirement ID, in a folder outside specs/ (plan/retired-specs/, laid out like plan/history/), and loaders and agents reading the spec corpus MUST NOT load that folder.

MS-09-001 keeps retired text out of the specs so it cannot be mistaken for current obligation, but git history is a poor place to look up what a cited ID used to say. One file per ID gives a citation something to resolve to without putting retired text into agent context. The archive sits outside specs/, which is the only directory the spec loaders read, so no loader can see it.
Depends on: MS-09-001
Depends on: MS-09-002
MS-09-006 MUST uv run spec-retire <ID> MUST move the item's final text from its spec file to plan/retired-specs/<ID>.md with the retiring issue or PR, the date and the replacement ID if any, MUST record the removal in the implementation history (MS-09-002), and MUST list every remaining citation of the ID that MS-09-003 requires repointing. It MUST change nothing when the ID is already archived, is not declared, or the spec files would no longer load without it.

Doing a retirement by hand means three separate edits, and the one that gets skipped is the one that leaves the audit trail or a stale citation behind. One command makes the whole retirement, or none of it.
Implements: MS-09-005
MS-09-007 MUST spec-lint MUST fail on a deprecated: or superseded_by: key on any requirement in specs/, and on any requirement ID that is both declared in specs/ and present in plan/retired-specs/.

MS-09-001 and MS-09-004 are rules a human forgets; the corpus already held both violations (deprecated-in-place entries, and CM-11-005 reused with the opposite force) before anything checked for them.
Implements: MS-09-004
MS-09-003 MUST Cross-references to a removed requirement MUST be updated to point to the replacement requirement.

Dangling cross-references break traceability.

Quality Criteria

ID Priority Requirement Related
MS-10-001 SHOULD Spec files SHOULD be scannable via priority-grouped layout.

Priority grouping lets readers quickly find the most critical requirements.
MS-10-002 SHOULD Spec files SHOULD be traceable via stable IDs and cross-references that the requirements graph lets a reader follow from either end.

Traceability supports change impact analysis.
MS-10-003 MUST Spec files MUST be testable with verifiable acceptance criteria per requirement; every priority: MUST or priority: MUST_NOT requirement MUST include a verification: field.

Untestable specs cannot be reliably implemented; a MUST requirement with no verification field provides no contract for reviewers or agents. Upgraded from SHOULD to MUST (CONCERN-2382) to align with MS-05-003 and enable progressive linter enforcement. MUST_NOT is in scope per MS-02-003: a prohibition with nothing checking it is the same gap. See MS-10-005 through MS-10-008 for enforcement while the backlog is worked down.
Refines: MS-05-003
MS-10-005 MUST spec-lint MUST report in its default output one summary line per kind that has an MS-10-006 owner entry or a verification_debt: marker, giving the number of priority: MUST or priority: MUST_NOT requirements of that kind that carry a marker, tallied by the issue each marker names. The line MUST be computed from the markers, not read from a committed count. spec-lint MUST list the marked IDs only when asked through an opt-in flag, and MUST NOT print one line per marked requirement by default.

MS-05-003 and MS-10-003 require MUST and MUST_NOT requirements to carry measurable acceptance criteria. The original per-item [WARN] (CONCERN-2382) grew to more than a thousand lines, which made a newly introduced instance invisible. A per-kind count keeps the backlog legible and makes each backfill's progress visible. Computing it from the markers leaves no committed number for concurrent PRs to race on (#3984).
Implements: MS-05-003
Implements: MS-10-003
Depends on: MS-10-006
MS-10-006 MUST Until it gains a verification: field, each priority: MUST or priority: MUST_NOT requirement MUST carry a verification_debt: marker naming, in #N form, the open issue that owns verifying it, and that issue MUST be listed as an owner of the requirement's kind in the one owner table under vultron/metadata/specs/. spec-lint MUST fail, one requirement at a time, on a MUST-tier requirement with neither field, on a requirement with both (a stale marker), on a marker below the MUST tier, and on a marker naming an issue that does not own its kind. A pull-request check MUST fail a pull request that closes an owner issue a marker or the owner table still names, and a scheduled check running at least hourly MUST report any marker or owner entry naming a closed issue through one tracking issue, without failing the build.

A count pinned to the live value in both directions is correct on the PR that sets it and wrong on every other PR in flight: a sibling that moves the count by one is green alone and red once merged (#3984). A per-item rule has no shared number to race, and two PRs conflict only when they edit the same requirement. The marker gives every unverified requirement a tracked path to verification. An owner usually closes through the backfill PR that says Closes #N; failing that PR alone races nothing, and catches a backfill that claims to finish while leaving requirements unverified. An owner closed by hand is caused by no PR, so failing the build on it would turn every PR in flight red; it is reported instead (ARCH-18-004). The owner table holds issue references, never counts, so verifying a requirement never edits it.
Implements: MS-10-003
Refines: MS-02-003
MS-10-007 MUST When no verification_debt: marker names an owner issue, that issue MUST be removed from the MS-10-006 owner table, and a kind's entry MUST be removed with its last owner. spec-lint MUST report an owner that no marker names, and MUST treat a marker on any requirement of a kind with no owner entry as a hard error, so that an unverified MUST or MUST_NOT of that kind can only pass by gaining a verification: field.

Zero is the only state a backlog cannot drift from, and only if nothing can raise it back. Removing a kind's entry makes the end state permanent: no marker of that kind is accepted, and an unmarked unverified requirement already fails under MS-10-006. An owner no marker names is reported rather than failed, because two PRs that each verify some of its last requirements are green alone and would turn main red merged (#3984); the PR that closes the owner issue fails until the entry is removed, which forces the removal. The growth guard (#4200) keeps markers from being added while an owner remains. The protocol tier is expected to finish first (#3612).
Depends on: MS-10-006
Implements: MS-10-003
MS-10-008 MUST A change that moves a priority: MUST or priority: MUST_NOT requirement carrying a verification_debt: marker from one kind to another MUST replace the marker with a verification: field in the same change.

Each kind's backlog has its own owner. Re-pointing a relabelled requirement's marker at the destination kind's owner would add debt to a backlog that never took it on, which is the growth the marker exists to forbid.
Refines: MS-10-006
MS-10-009 MUST A verification: clause MUST name a check that exists and that inspects the property its requirement's statement asserts. Naming a tool or test that merely runs near the property does not satisfy this requirement: a build, lint or type-check that never examines the artifact the statement constrains, or a test that does not exist, is not verification.

A verification clause is a claim about the suite, and an unbacked one reads exactly like a backed one. DEMOCI-11-009 named mkdocs build --strict for links strict mode never sees; VM-01-004 described a test that did not exist (#2982); DR-03-002 had no clause and named the wrong component; MS-13-003 had no check and sat at half adoption (#3480); ADR-0099 graduated with two details unbuilt (#3888). MS-15-001 makes a path resolve, not a check examine. See notes/spec-authoring-rules.md.
Refines: MS-10-003
Depends on: MS-15-001
MS-10-004 SHOULD Spec entries SHOULD require only id, priority, kind, statement, and rationale (MS-10-012) to be complete; optional fields (tags, relationships) SHOULD be added only when they provide clear value, not by default.

Requiring optional fields on every entry creates boilerplate that discourages keeping specs current.
MS-10-010 MUST spec-lint MUST report as a hard error each test a verification: clause names that does not exist: a pytest node (<file>.py::<name>) whose names do not resolve, in order, in that file; and a bare test_<name> token that no test file or test directory the clause names defines, or, when the clause names none, that no file under test/ or integration_tests/ defines.

A named test that does not exist reads exactly like one that does, and MS-15-001 resolves only the file. A name looked up only where the clause points also catches a test that moved to another module (#4170).
Refines: MS-10-009
Depends on: MS-15-001
MS-10-011 MUST When the verification: clause of a kind: protocol or kind: foundation requirement names test files, at least one of them MUST carry @pytest.mark.spec("<ID>") for that requirement. spec-lint --changed-since <ref> MUST report each requirement the branch adds or changes that breaks this, and a pull-request growth guard MUST fail on each requirement a pull request adds to the set that breaks it.

A clause pointing at a test that does not carry the marker claims a check the coverage report cannot see. The corpus has a backlog, so the rule holds for what a branch touches rather than corpus-wide.
Refines: MS-10-009
Refines: SR-05-004
MS-10-012 MUST Every requirement MUST carry a rationale: that explains why the requirement is necessary given current conditions: the problem it prevents or the property it guarantees.

A requirement with no written reason can only be obeyed or guessed at. When it conflicts with another requirement, nobody can weigh the two, and when its reason lapses, nobody can see that it should be retired. User stories give a reason only where they exist (SR-11), so for most project and process requirements the rationale is the only place the reason can live.
Refines: SR-02-009
MS-10-013 MUST NOT A rationale: MUST NOT narrate how its requirement came about, such as its prior wording, a reverted change, or the issues and pull requests behind it. That history belongs in the decision record the adr: field cites, the version-control log, or the project history.

A narrated history answers what happened, not why the rule holds, and it reads as a standing endorsement of the earlier state it describes. A reader then has to reconstruct the current reason from the story, and the story goes stale with every later edit, while a stated reason stays true for as long as the requirement does.
Refines: MS-10-012
MS-10-014 MUST Until the schema requires a rationale: on every requirement, a PR-only growth guard MUST fail when a requirement has no rationale at the PR head but had one at the merge base with the base branch, or did not exist there, and MUST list each such requirement. A requirement already missing one at the merge base MUST NOT fail the guard, and the guard MUST NOT compare against a committed count or an exemption list.

The corpus holds a backlog of requirements with no rationale, so a corpus-wide check would fail every pull request until the backfill finishes. Checking only what a branch adds keeps the backlog from growing while it is worked down, and a per-item set has no shared number for concurrent pull requests to race on.
Implements: MS-10-012
Refines: SR-05-005
MS-10-015 SHOULD The project SHOULD provide two advisory reports, each filterable by spec file and neither failing the build: requirements whose ID a live decision record's body names while their adr: field does not cite that record, and rationales that match patterns of history narration.

A requirement a decision record names is often one that record motivates, so a missing adr: link is a likely gap in the trace from rule to reason. History narration is easy to recognize but has false positives, such as an issue cited as evidence, so both lists are worklists for a person to judge rather than gates. A per-file filter lets each backfill take its own share.
Derives from: MS-10-012
Derives from: MS-10-013
Derives from: MS-11-004

Spec vs. ADR Delineation

ID Priority Requirement Related
MS-11-001 MUST A spec file MUST be used to capture testable requirements — what the system MUST, SHOULD, or MAY do — using RFC 2119 keywords. Specs define the intended behaviour of the system; they do not record the rationale for choosing one approach over another.

Mixing decision rationale into spec files blurs the distinction between requirements and design history, making both harder to maintain and trace.
MS-11-002 MUST An Architecture Decision Record (ADR) MUST be created whenever a meaningful alternative was evaluated and rejected. The primary signal for an ADR is the presence of options considered and discarded — "we weighed X, Y, and Z and chose X because …"

If no real fork existed, there is no decision to record. An ADR without evaluated alternatives is just prose documentation and belongs in notes/ instead.
MS-11-003 SHOULD When a significant architectural decision generates recurring testable requirements, both an ADR and a spec entry SHOULD be created. The ADR captures the why (context, options, consequences); the spec captures the what (requirements agents and reviewers must satisfy going forward).

The two documents serve different audiences. The ADR is for human reviewers asking "why was this chosen?"; the spec is for implementation agents asking "what must I do?". Collapsing them into one document serves neither well.
MS-11-004 SHOULD A spec entry that was motivated by an ADR-backed decision SHOULD reference the ADR in its rationale field. The corresponding ADR SHOULD list the generated spec IDs in its "More Information" section.

Bidirectional cross-references make it possible to trace a requirement back to its decision rationale and forward from a decision to its testable consequences.
MS-11-005 SHOULD When the correct approach is uncontested and no alternative was seriously considered, a spec entry alone is sufficient. An ADR SHOULD NOT be created merely to document an obvious or unchallenged choice.

Writing ADRs for trivial decisions adds noise to the decision log and devalues the records that capture genuine trade-offs.
MS-11-006 SHOULD When a decision is a one-time structural or process choice with no recurring testable requirements (e.g., "use CalVer for project versioning", "pin CI actions to full commit SHAs"), an ADR alone is sufficient. A spec entry SHOULD NOT be created if there is no per-change requirement for agents to check.

Not every decision produces enforceable requirements. Creating spec entries for binary one-time choices inflates the spec corpus without providing testable value.
MS-11-007 MUST NOT A spec file, a notes/ file, or a code comment or docstring under vultron/ or test/ MUST NOT resolve its meaning by naming a numbered part inside a decision record's body: a record ID with a detail, rule, step, option, part, invariant or section (§) number, such as a record's "Rule N" or "invariant N". It MUST state the rule where the reader already is, or cite the requirement that states it, and cite the record only as provenance for why. A decision record citing a numbered part of another record is out of scope: that is history citing history. Because a bare "detail N" a sentence after the record was named is the same citation, any "detail" followed by a number counts; the other part words count only with a record ID nearby, because a numbered step of a procedure is ordinary prose.

A part number is defined nowhere but the record body, so a citation of one makes the body load-bearing and forces every reader to load it, which ADR-0127 removes from routine agent context. A rule stated in a requirement is checked by its verification clause; a rule left in a body ages with nothing to say it has. The shape is not specific to one word: a rule, option or decision-part number points into a body exactly as a detail number does. Agents read code comments as often as spec files, so the citation shape is equally harmful there.
MS-11-008 MUST The routine context-loading skill (deepen-context) MUST route through decision records by the generated title index and by the adr: edges requirements carry, read in the record-to-requirements direction, and MUST carry a written list of the skills that read decision-record bodies in full, each with the reason it needs them. That list MUST name at least decision auditing, record authoring, the learning workflow, issue planning and pull-request triage. A skill kept in this repository and not on the list MUST NOT instruct an agent to read a decision-record body, and each listed skill kept in this repository MUST weigh what it reads by the shared guidance on when a record is challengeable.

A record body is history, and its description of the code ages with every refactor while the decision stands; loading it as routine context spends working memory on claims that no longer check out (ADR-0127). Writing the exception list down keeps the narrowing from being over-read as "never read a record", and keeping the trust weighting beside every remaining reader preserves how a provisional or hedging record is treated.
Refines: MS-11-007

SpecKind Classification Decision Tree

Rules for assigning a kind value to a new or reclassified spec. Who must obey a rule decides its kind. Split a spec that mixes kinds first, then stop at the first matching question.

ID Priority Requirement Related
MS-12-001 MUST When a spec is about how the project is run — CI configuration, GitHub workflow, agent conventions, documentation standards, or spec authoring rules — the spec MUST be classified as process.
MS-12-002 MUST A spec's kind MUST be decided by who must obey it, never by whether its statement names a codebase construct such as a file path, library, class, or Behavior Tree node. When only this codebase must obey the spec — a deployment choice (such as running the CASE_MANAGER as an actor separate from the case owner), a performance or operating limit, how a receiver records what it receives, or a core internal — the spec MUST be classified as project.

Compare TCP and an operating system's socket API. An RFC fixes what two independent implementations exchange on the wire; how one implementation structures its internals (its endpoints, stores, compare-and-set loops) is not RFC content, however firm the MUST. EP-02-004 (compare-and-set on the policy PUT) is the worked example: only this codebase must obey it, so it is project. Naming code is a symptom, not the test: a protocol rule spelled in this codebase's terms is reworded, and one that also says how this codebase meets it is split (MS-12-010). Neither is relabeled.
Depends on: MS-12-005 — applies only when MS-12-001 did not already match
MS-12-003 MUST When every Vultron implementation, in any language or framework, must obey the spec to take part in the protocol, the spec MUST be classified as protocol. protocol is the Vultron layer: Vultron's message forms on the wire (including where ActivityStreams allows several forms and Vultron requires one), which messages are sent and when, what a receiver does given its state (including on duplicate delivery), and the state machines underlying those behaviors. Nothing else is protocol.

The test is the TCP analogy: if the sentence could appear in an RFC for the protocol and an implementer in any language would have to honour it to interoperate, it is protocol. Behaviour that only changes how one implementation is built is not. State-machine states such as RM.CLOSED are protocol constructs. The CASE_MANAGER role and case-ledger sync are protocol for now; they are re-judged if the protocol moves to a federated design with no CASE_MANAGER (ADR-0134).
MS-12-004 MUST When none of the above apply — the spec is a structural or behavioral constraint that is implementation-independent but not required for Vultron compliance — the spec MUST be classified as architecture.
MS-12-005 MUST A spec that mixes kinds MUST first be split as MS-12-010 directs. The decision tree MUST then be applied in order (MS-12-001 → MS-12-002 → MS-12-009 → MS-12-003 → MS-12-004), and a spec MUST be assigned the kind corresponding to the first matching question.

Each question asks who must obey the rule, in this order: how the project is run, this codebase only, every implementation of a standard Vultron inherits, every Vultron implementation, and recommended structure. foundation precedes protocol so a rule that only restates ActivityStreams, ActivityPub or HTTP is not counted as Vultron's own. Splitting comes first because a mixed spec would otherwise match project and hide its protocol half from implementers.
MS-12-006 MUST spec-lint MUST emit a hard error for any spec with kind protocol or foundation that carries no stories: back-reference and whose statement text references an unambiguous codebase construct — a .py suffix, a vultron/, test/, or scripts/ path, or the tokens pytest, pydantic, or py_trees. The check MUST be suppressible per-spec via the protocol_kind_with_code_reference lint_suppress code. Its message MUST advise rewording the statement so any implementation could obey it, or splitting the spec (MS-12-010), and MUST NOT advise relabeling it.

MS-12-001 through MS-12-005 went unenforced until issue #2601, and the corpus accreted protocol-kind specs suppressing the missing-story-reference check instead of carrying a corrected kind. An unenforced MUST anti-advertises: partial adoption reads as "no rule here". Three choices keep it off already-compliant specs: story-less specs only, a token list narrow enough to exclude domain English, and reword-or-split — not relabel — as the remedy, because naming code never decides a kind (MS-12-002). Derived in notes/spec-authoring-rules.md. Only the statement is scanned: a verification field names the test that checks a rule, and naming a test file there says nothing about whether the rule itself is protocol content. foundation is scanned too: a foundation rule whose statement names this codebase's code also says how this codebase meets it, so it is a mixed spec to split (MS-12-010, ADR-0134). The suppression code keeps its protocol_ name, which specs already carry.
Verifies: MS-12-002
Verifies: MS-12-005 — the error message advises reword or split rather than naming a single branch
Depends on: SR-11-003 — shares the story-traceability population; the absence of stories gates both
MS-12-007 MUST No pull request may add a lint_suppress: [missing_story_reference] carrier: a PR-only growth guard MUST fail when a spec carries the code at the PR head but did not at the merge base with the base branch, whether or not SR-11-003 would fire on it, and MUST list each such spec. Removing a suppression MUST NOT fail the guard, and the guard MUST NOT compare against a committed count.

Suppression is the escape hatch SR-11-003 provides, so it cannot be forbidden, but an unguarded escape hatch is how the backlog accumulated. Each carrier is recorded on its own spec, so the guard compares carrier sets and commits no number: a pinned count races concurrent PRs and a one-sided ceiling collects slack (notes/testing-pitfalls.md). Every carrier counts, dead ones too; spec-lint prints the total. MS-12-008 closes the other code, so the two cannot trade places.
Depends on: SR-11-003 — guards the suppression escape hatch SR-11-003 provides against growth
Depends on: SR-11-004 — the same suppression code silences this advisory, so dead suppressions are in scope
MS-12-008 MUST A test MUST assert that no spec in the live corpus carries lint_suppress: [protocol_kind_with_code_reference].

MS-12-006 adds a second escape hatch next to the one MS-12-007 guards. If only one of the two is held, a pass can swap suppressions from the guarded code to the unguarded one: one total falls, the other grows, and every gate stays green. The code has never had a carrier, so it is held at that terminal value outright rather than by a growth guard; admitting a carrier means amending this requirement.
Refines: MS-12-007 — closes MS-12-006's suppression code alongside the code MS-12-007 guards
Depends on: MS-12-006 — holds the suppression escape hatch MS-12-006 provides at no carrier
MS-12-009 MUST When every implementation must obey the spec because Vultron inherits it from a standard beneath it — ActivityStreams 2.0, ActivityPub, or HTTP — the spec MUST be classified as foundation, and it MUST carry the tag naming that standard (as2, activitypub, or http). spec-lint MUST emit a hard error for a foundation spec whose effective tags name none of the three.

Addressing and delivery, HTTP status codes, and rules that only restate ActivityStreams (every id is an IRI) bind every implementation, but Vultron does not own them. Filing them as protocol inflates what Vultron defines; filing them as project hides them from implementers, who read protocol and foundation together. Where Vultron requires one form among several the standard allows, the rule is Vultron's own and is protocol. The tag lets a reader trace each rule to the standard that owns it.
Depends on: MS-12-005 — applies only when MS-12-001 and MS-12-002 did not already match
MS-12-010 MUST A spec that mixes kinds MUST be split along what other participants can observe. The original ID MUST keep the half that states what is emitted or observed, reworded so any implementation could obey it, and the half that states how this codebase does it MUST move to a new project ID whose relationships: name the original ID.

Keeping the original ID on the protocol half keeps its user stories, test markers and citations where implementers look, so the PR-only growth guards (SR-05-005, MS-12-007) see no new uncovered protocol spec and no new suppression. Worked example: default-embargo creation. The protocol half says other participants never observe the intermediate Proposed state; the project half says this codebase applies PROPOSE and ACCEPT atomically in one behavior tree node.
Refines: MS-12-005 — the split MS-12-005 runs before the decision tree
MS-12-011 MUST Changing a spec's kind MUST NOT remove the tests or @pytest.mark.spec markers that already verify it, whatever the destination kind. A spec relabeled from protocol to project, for example, keeps its tests and markers even though project does not require them.

A test that verifies a rule is evidence the rule holds, and a kind change does not change whether it holds. Dropping markers on a relabel would turn a re-judgement sweep into a way to shed coverage, and a later relabel back would find the tests gone.
Refines: MS-12-005 — constrains how the kind the decision tree assigns is applied

Spec Item Format Selection (StatementSpec vs BehavioralSpec)

ID Priority Requirement Related
MS-13-001 MUST A spec item MUST use BehavioralSpec format (with preconditions, steps[], and postconditions fields) when it describes a sequential, stateful process that has a defined start state, ordered actions, and terminal conditions.

StatementSpec statement text is a single normative sentence. Encoding ordered sub-steps as numbered lists inside that sentence violates MS-05-001 (no inline prose explanations) and makes the start state, step ordering, and terminal conditions invisible to conformance tooling.
MS-13-002 MUST A spec item MUST use StatementSpec format when it expresses a capability constraint, behavioral property, or structural rule that does not depend on step ordering — i.e., it specifies a required outcome or invariant, not a prescribed sequence of actions.

BehavioralSpec's steps[] field implies ordering that is not meaningful for property requirements; StatementSpec is simpler and more appropriate.
MS-13-003 MUST A spec group whose items describe a demo scenario workflow MUST include a trigger field with type: scenario_start to indicate to conformance tooling that the group is a process workflow, not a property group.

The trigger field allows tooling to classify groups by activation kind without parsing item content. scenario_start distinguishes demo-script-triggered workflows from message_received and state_entered behavioral groups.
MS-13-004 MUST A spec group with trigger type scenario_start MUST contain at least one BehavioralSpec item with a non-empty steps field describing the ordered protocol exchange.

Mixed groups are permitted — terminal-state assertions and infrastructure constraints MAY remain StatementSpec — but the sequential workflow itself MUST be expressed as BehavioralSpec so that conformance tooling and CI can verify ECA structure without parsing prose. This requirement is enforced by spec-lint (SR-04-003); a group with only StatementSpec items causes a hard error. See CONCERN-1650 for the regression that prompted this rule.

ADR Status as Confidence Signal

ID Priority Requirement Related
MS-14-001 MUST Every ADR under docs/adr/ (excluding the template and index) MUST declare a non-empty status field whose value is a member of the AdrStatus StrEnum (proposed, accepted, accepted-provisional, deprecated, rejected, superseded); the inline MADR form "superseded by " is accepted and normalised to superseded.

The status field is the primary confidence signal that reaches coding agents (deepen-context weights ADR trust by it). A blank or non-standard status leaves agents to treat an unvalidated decision as settled fact. ADR-0024 shipped with a blank status; this rule prevents recurrence. AdrStatus is the fixed vocabulary (SR-02-002) the linter typechecks against. Enforced by spec-lint. See ADR-0043 and notes/specs-vs-adrs.md.
Derives from: MS-11-004 — extends ADR/spec cross-referencing with a status-confidence rule
MS-14-002 SHOULD An ADR's status value SHOULD agree with its prose. An ADR whose body contains provisional markers (e.g. "formed in sand", "not concrete", "provisional", "forward-looking", "will converge", "SHOULD refine this ADR", "status will advance") SHOULD NOT declare status: accepted; it SHOULD use accepted-provisional or proposed.

A status/prose contradiction is a landmine: the agent trusts status: accepted while the body admits the design is unvalidated. ADR-0025 was the motivating case. spec-lint emits an advisory warning (not a hard error) because the substring scan can false-positive on ADRs that merely discuss provisional-ness; flagged ADRs are decision-audit candidates for human adjudication. An ADR that legitimately discusses provisional-ness (e.g. ADR-0043, which defines the vocabulary) MAY opt out with frontmatter 'lint_suppress: [status_prose_contradiction]', mirroring the spec-level lint_suppress mechanism (SR-02-011).
MS-14-003 MUST An ADR's placement in docs/adr/index.md (Accepted / Proposed / Rejected / Superseded-Archived sections) MUST agree with its own status field, and a retired ADR (status deprecated or superseded) MUST be listed under the Superseded / Archived section with a forward link to its replacement, not under Accepted.

Index-vs-frontmatter drift (ADR-0027 was listed provisional in the index while its file said accepted) misleads both humans and the agents that read the index.
MS-14-004 SHOULD A newly-retired ADR (status deprecated or superseded) SHOULD be moved to docs/adr/archived/ so it stays out of the default docs/adr/ context sweep; ADRs retired before this convention MAY remain in place until next touched.

Archiving retired ADRs keeps outdated decisions out of the default docs/adr/ sweep so agents must deliberately seek them. Applied forward (SHOULD, not a retroactive MUST) to avoid churn on already-superseded ADRs that are still referenced by path. See ADR-0043.
MS-14-007 MUST An ADR's status MUST be at least the lifecycle epoch computed for it (ADR-0120). Epoch 1 (proposed) ends at whichever comes first: day 14 after updated, or a spec requirement that cites the ADR in adr: and is verified by a test file that exists (MS-14-010), counted in the tree being checked: the pull request's head for a check on a pull request, main for the scheduled check. Epoch 3 (accepted) starts at day 28 after updated; between the two the epoch is 2 (accepted-provisional). A pull request that materially edits an ADR (its updated date changes, or the ADR is new) MUST leave the status on the epoch for that date or carry a status_override: whose value is a human's reason, and a check comparing the pull request against its base MUST fail it otherwise. No merge-blocking check may depend on today's date for an ADR the pull request did not materially edit. A scheduled check MUST report every ADR whose status is behind today's epoch through one tracking issue that closes itself when none remain, and MUST NOT fail the build.

Elapsed time alone is the wrong signal that an ADR has had its chance to be revised; the first test-verified dependent is, because the material test asks whether something built on the old text would now be wrong. The timers are a backstop so every ADR hardens. A merge-blocking check reading today's date would turn main red with no commit to blame (ADR-0110, 2026-10-08), so the clock's drift is reported instead (ARCH-18-004). Boundaries are counted in whole days (14 and 28) because updated is a date. The dependency is counted on the pull request's head because the pull request that creates it must already show its effect (MS-14-010).
Depends on: MS-14-001
MS-14-008 MUST ADR frontmatter MUST carry created (set once, never changed), updated (changed only by a material edit), and revision (an integer incremented by each material edit). Existing ADRs MUST be migrated with created = updated = date and revision = 1.

The epochs of ADR-0120 are measured from updated, so it must move only when the decision changes; date was rewritten by editorial edits and cannot serve. created is the immutable birth date and revision counts how many times the decision has changed.
Depends on: MS-14-005
MS-14-009 MUST A pre-commit check (adr-lifecycle-check) MUST fail a diff that changes the Decision Outcome or Considered Options section of an epoch-3 ADR without adding a dated Amendment block, and MUST fail a diff that bumps updated on an ADR that was past epoch 1 unless the ADR carries a status_override. The same check MUST also run on every pull request that changes an ADR, against the merge base, so a skipped local hook cannot bypass it.

ADR-0120 forbids rewriting an accepted ADR under its own number, and the audit that produced it found exactly that done by agents. The rule is only as strong as the gate that refuses the edit, and a reviewer reading a large diff does not reliably see a changed section. A heading that names "Amendment" and a date is the mechanical mark of a dated Amendment block.
Depends on: MS-14-007
MS-14-010 MUST A pull request after which an ADR that is proposed on its base first has a test-verified dependent (MS-14-007) MUST, in the same pull request, set that ADR's status to accepted-provisional, and the check comparing the pull request against its base MUST fail it otherwise. This holds however the dependent is created: by adding a requirement that cites the ADR, by adding adr: or a verification to an existing requirement, or by adding the test file a verification names.

The dependency ends epoch 1 (MS-14-007), but it lands in a spec and a test, usually in a pull request that never touches the ADR. The pull request that changed the facts owns the status, as a material edit already does; leaving the flip to the scheduled drift report would leave the ADR reading proposed, and free to rewrite, while code depends on it. The check sees that the named test file exists, not that it passes.
Depends on: MS-14-007
MS-14-012 MUST An ADR's status MUST only move forward through the epochs. A status ahead of the computed epoch that the ADR already carried on the base stands: it is not a fault for the scheduled check, nor for a pull request that does not materially edit the ADR, and a material edit that restarts the timers keeps it under the status_override: that MS-14-007 and MS-14-009 already require of that edit. A pull request that changes status to a value ahead of the epoch computed for it MUST add or change a status_override: whose value is a human's reason; an override already on the base does not satisfy this. The check comparing the pull request against its base MUST fail it otherwise.

ADR-0120 exists so that a citation keeps meaning what it meant. When the timers were lengthened (from 3 and 10 days to 14 and 28), ADRs already accepted under the old timers would have computed as accepted-provisional; demoting them would loosen the edit tiers on decisions agents had already built on. Treating the computed epoch as a floor covers that change and any later one. The guard keeps a promotion ahead of the clock a visible human act rather than a silent frontmatter edit, and an override left from an earlier, unrelated reason would make every later promotion silent again.
Depends on: MS-14-007

Spec Statements and ADR Edits Must Not Reference Phantom Paths or Phantom Symbols

ID Priority Requirement Related
MS-15-001 MUST NOT A spec item's statement, verification, behavioral step, precondition, and postcondition fields MUST NOT reference a file path or directory path that does not exist.

A normative statement naming non-existent infrastructure is a stale-premise landmine: the agent invents something inconsistent with the codebase or silently ignores a MUST. DEMOMA-19-008 named a conftest.py registry that had never existed; an audit found 18 phantom-path clauses. Behavioral step, precondition, and postcondition fields are scanned in addition to statement and verification because they are equally normative. rationale legitimately cites gone paths and is excluded.
Derives from: MS-04-001 — extends spec-authoring rules with a path-existence guarantee
MS-15-002 MUST NOT A spec item's statement and verification fields MUST NOT use an absolute path or a .. segment.

Absolute paths and parent-traversal segments are deployment-specific and will not resolve correctly in the repo-relative context used by spec-lint.
Refines: MS-15-001
MS-15-003 MUST A spec-first requirement that deliberately names a file yet to be created MUST either use a placeholder form (e.g. test_XXX_invariants.py) or opt out with lint_suppress: [phantom_path_ref].

The placeholder or lint_suppress mechanism makes the spec-first intent explicit rather than silently passing a phantom-path check by accident.
Refines: MS-15-001
MS-15-004 MUST NOT A spec item's statement, verification, behavioral step, precondition, and postcondition fields MUST NOT reference a backticked all-caps code symbol containing at least one underscore that appears nowhere in the vultron/ or test/ Python sources.

MS-15-001 only inspects backticked tokens containing a directory separator, so a bare symbol name passed it silently. That gap let the retired SEMANTICS_ACTIVITY_PATTERNS name survive across five spec files, eight of them MUST-level kind: protocol statements, from the registry move until issue #3022. A MUST about a symbol nobody can find is worse than an absent requirement: the agent either invents a replacement or drops the obligation. The single-underscore floor excludes the token shapes that are not symbols — RFC 2119 keywords, protocol shorthands, one-word state names. Resolution is textual and tree-wide rather than a binding lookup, preferring a false negative over blocking a legitimate reference. rationale is excluded, as in MS-15-001: the problem it names (MS-10-012) may be a removed symbol that must not return.
Refines: MS-15-001 — extends the phantom-reference guarantee from paths to code symbols
MS-15-005 MUST A requirement that deliberately names a retired code symbol, or a convention token that was never code, MUST opt out with lint_suppress: [phantom_symbol_ref].

Two legitimate reasons to name a symbol that does not resolve: recording that it was removed (the retired SE-08-005 documented the removal of a semantic), and naming a proposed spec-authoring tag rather than an identifier (PROTO-04-001's PROD_ONLY). Requiring the explicit opt-out keeps both visible as deliberate choices instead of leaving them indistinguishable from the stale MUSTs MS-15-004 exists to catch.
Refines: MS-15-004
MS-15-006 MUST NOT A change that edits an ADR MUST NOT add to it a backticked file path, directory path or code symbol that resolves nowhere: a path that does not exist in the repository, or a symbol that appears nowhere in the vultron/ or test/ Python sources. Recognized symbol shapes are underscored all-caps constants, CamelCase class names, called names such as snake_case(), and members of a CamelCase class or all-caps members after a dot. The rule is scoped to references the change adds to a record it edits: a reference present on the merge base, however stale, and a change that edits no ADR are outside it. The pre-commit check adr-added-reference-check MUST enforce it against HEAD, and the same check MUST run on every pull request that changes an ADR, against the merge base, so a skipped local hook cannot bypass it.

ADR-0127 makes an ADR a historical record: a reference that aged after it was written is an accurate account of the code as it then stood, so a refactor owes the record nothing and no check keyed on code changes may fail on it. A reference newly written into a record is different — it should resolve on the day it is written, and that is a question about the change's own diff. Comparing against the merge base is the shape MS-14-009 already uses. The spec-corpus check of MS-15-004 reads only underscored constants, which misses the class and function names that make up the aged references in the corpus, so this check reads more shapes. It resolves textually, preferring a false negative to refusing a legitimate reference, as MS-15-004 does.
Refines: MS-15-001 — extends the phantom-reference guarantee to references an edit adds to an ADR
Refines: MS-15-004
MS-15-007 MUST An ADR edit that deliberately adds a reference that does not resolve — a symbol or path the record annotates as removed, or a convention token that was never code — MUST opt the record out in its frontmatter with lint_suppress: [phantom_symbol_ref] for a symbol or lint_suppress: [phantom_path_ref] for a path. The opt-out covers the whole record: every later edit to it adds references of that kind unchecked.

The annotated-removal house style ("that mechanism and that module were deleted in #2940") names a symbol precisely because it is gone, so it can never resolve. The opt-out mirrors MS-15-005 for requirements and keeps the choice visible rather than indistinguishable from a mistyped name.
Refines: MS-15-006

Long-Lived Docs Must Not State Unverifiable, Drift-Prone Facts

ID Priority Requirement Related
MS-16-001 MUST A spec statement, notes file, or AGENTS.md entry MUST NOT restate a count that will drift independently of the authoritative source. When cross-referencing another spec item, omit the count and cite the source by ID: write "the universal types (DEMOMA-16-001)" not "the five universal types (DEMOMA-16-001)". Counts in enumerations that are the authoritative source (e.g. DEMOMA-16-001 itself listing its own members) are not subject to this rule.

Counts restated in cross-references drift silently: when the authoritative requirement is updated, its sibling references and cross-file citations are not. The linter validates that a referenced spec ID resolves; it has no opinion about the prose next to it. A reader who encounters the stale count first gets a false picture of the system. The count adds no normative force — "the universal types (DEMOMA-16-001)" is equally specific because the ID is the authoritative pointer. This class of drift required 12 coordinated edits when engage_case was promoted to a fifth universal semantic type in ISSUE-2266 (CONCERN-2277). The same principle extends beyond spec cross-references to any long-lived document: notes files, AGENTS.md pitfall entries, and architecture docs MUST NOT state counts such as "there are 4 unimplemented nodes" or "15 xfails" — these are snapshots guaranteed to be wrong when read in the future.
Derives from: MS-04-001 — extends spec-authoring rules with a count-drift prohibition
MS-16-002 MUST A long-lived document — a spec, a notes file, AGENTS.md, or a docs/ table that mirrors a code, spec, or config inventory — MUST NOT state a fact that no automated check could falsify and that drifts independently of its source. The governing test is "could a test fail on this sentence?": a restated count (MS-16-001) is one instance; a per-item runtime outcome ("✅ passes"), a forecast ("resolved by #789"), or a free-prose claim copied from source are others. Each such fact MUST either be made verifiable — derived from, or ratcheted against, its source — or removed in favour of a pointer to where the live answer lives.

A fact nothing can check is a fact nobody does check, so it rots first, and it rots into a confident wrong answer rather than into silence — which costs more than no doc at all. MS-16-001 caught the most common instance (counts); this entry generalises it from "is this a number?" to "could a test fail on this sentence?". Witnessed twice: the case-ledger invariant tables (ISSUE-3337), where the two columns no test could hold were 100% wrong while every source-derivable column stayed right; and the scenario table (the rcvcv and vc rows of vultron/demo/scenario/README.md, ISSUE-3450 / CONCERN-3466), where a free-prose column named four actors for a five-actor scenario. Before concluding a column is unverifiable, check whether it conflates more than one fact — a column answering two questions at once is unverifiable as a unit while each part may be individually derivable; the procedure for that triage lives in notes/documentation-sweeps.md.
Derives from: MS-16-001 — generalises the count-drift prohibition to any unverifiable, drift-prone fact

One Wire Form per Protocol Act

How a requirement handles an act that ActivityStreams lets a sender express in more than one wire form, so that the reference implementation's habit never reads as a protocol rule and a receiver never pays for forms no sender needs.

ID Priority Requirement Related
MS-18-001 MUST When ActivityStreams allows more than one wire form for one protocol act, the protocol requirement for that act MUST name exactly one form. Each other form that was considered MUST be recorded as a deferred alternative in an open question on the Open Questions page (docs/topics/future_work/open_questions.md), and a form the protocol does not name is not a protocol message.

A receiver that must accept every allowed form pays for each form any sender picks, and when several forms are allowed the reference implementation's choice is all a reader sees, so its habit reads as a protocol rule (the create_note occasion said the Create "precedes the Add", #4550). Deferring the other forms keeps them open for later design without binding any receiver now.
MS-18-002 MUST NOT A requirement, a semantic registry occasion or a reader-facing page MUST NOT allow several wire forms for one protocol act and then record which of them this codebase sends; the choice is made once, in the protocol requirement (MS-18-001).

A per-message "allowed versus sent" marker was weighed and rejected (#4550): it keeps every receiver accepting forms that no implementation needs to send, and it is one more hand-kept fact that drifts from the emitters (MS-16-002).
Refines: MS-18-001 — rules out the allow-several-then-mark-one alternative

Notes Frontmatter

Requirements for YAML frontmatter in notes files, covering the required schema, a Pydantic-based loader, validation enforcement, migration requirements, what a note is for and how it is written, and the report that flags delivered plan notes.

File ID: NF

Frontmatter Schema

ID Priority Requirement Related
NF-01-001 MUST Every notes/*.md file (except notes/README.md) MUST contain a YAML frontmatter block delimited by --- at the top of the file.
NF-01-002 MUST The frontmatter MUST include a title field whose value is a non-empty string matching the file's # H1 heading.
NF-01-003 MUST The frontmatter MUST include a status field whose value is one of: active, draft, superseded, archived.
NF-01-004 MUST When status is superseded, a superseded_by field MUST be present and MUST contain a non-empty string identifying the replacement file path (e.g., notes/new-topic.md or specs/new-spec.md).
NF-01-005 MUST The optional description field, when present, MUST be a non-empty string summarising the file's purpose for tooling and agents.
NF-01-006 MUST The optional related_specs field, when present, MUST be a non-empty list of non-empty strings naming related specification files (e.g., ["specs/behavior-tree-integration.md"]).
NF-01-007 MUST The optional related_notes field, when present, MUST be a non-empty list of non-empty strings naming related notes files.
NF-01-008 MUST The optional relevant_packages field, when present, MUST be a non-empty list of non-empty strings naming packages relevant to the note's topic. Each entry MUST be either a dot-separated Python package name (e.g., vultron.core) or a top-level package directory name (e.g., vultron).
NF-01-009 MUST The frontmatter MUST include a kind field whose value is one of guidance, plan, or generated, as defined by NF-08-002. Depends on: NF-08-002
NF-01-010 MUST A note whose kind is plan MUST include a tracks field: a non-empty list of GitHub issue references in #N form naming the issues the plan informs and their parent epic. A note of any other kind MUST NOT include tracks.

Tracking the parent epic as well as the leaf issues keeps the note alive while related work continues: a Concern closed by being split into Tasks, or a leaf moved to another epic, does not by itself make every tracked issue closed. Epics close by hand once no open children remain.
NF-01-011 MUST A note whose kind is generated MUST begin its body, immediately after the frontmatter, with an HTML comment whose text starts with the word GENERATED and names the command which writes the file. The loader MUST reject a generated note without it, naming the file per NF-03-006.

Migration

ID Priority Requirement Related
NF-05-001 MUST All existing notes/*.md files (except notes/README.md) MUST have compliant YAML frontmatter added as part of the initial implementation of this spec.
NF-05-002 MUST The status field added during migration MUST accurately reflect the current state of each file's content.

Maintenance

ID Priority Requirement Related
NF-06-001 SHOULD When modifying any notes/*.md file, the author SHOULD review and update its frontmatter — in particular status, related_specs, and related_notes — to reflect any changes in scope or relationships.
NF-06-002 MUST AGENTS.md MUST document the frontmatter maintenance rule so that AI coding agents apply it consistently.

Note Kinds and Content

ID Priority Requirement Related
NF-08-001 MUST A note MUST exist only to inform work that has not been done yet. Once that work lands, the note's content MUST either be archived (PD-03-004) or, if it remains useful beyond that stage, be moved to docs/ (reference material) or into a guidance note (lasting rules). notes/ MUST NOT be used as a narrative history of the project; git and plan/history/ hold the history.

Notes grew 25% in the four weeks after the 2026-09-17 sweep, mostly from backstory appended to durable notes by fix and feature PRs, not from new plans. Nothing told the writer that a note is a working document with an end.
Refines: PD-03-004
NF-08-002 MUST The kind values MUST be applied as follows. guidance: a rule for changing the project without repeating a known mistake, or a procedure for a recurring task, that would still be true and still change how someone works if every open issue were closed. plan: a design or decision for work the code does not do yet, too large or too cross-cutting for one issue body. generated: a file a tool writes. Material that is none of these — reference material, or rationale for delivered work — MUST NOT be kept in notes/; it belongs in docs/ or the archive.
NF-08-003 MUST Each note MUST have exactly one kind. A plan item MUST NOT be added to a guidance note; it goes in a plan note (a new one if needed) or in the issue itself. When a plan is delivered, its lasting lessons MUST be promoted into the matching guidance note, or docs/, before the plan note is archived. Depends on: NF-08-002
NF-08-004 MUST Each item in a guidance note MUST be stated as the rule, the reason for it, and at most one citation (issue, ADR, or spec ID). It MUST NOT carry provenance chains, HTML Source: comments, accounts of how a defect hid or was found, change-history lists, migration-complete notices, reconstructions of removed features, copies of live code, file.py:line tables, or inventories a test already enforces. The writing guide at .agents/skills/shared/notes-writing-guide.md holds the full pattern list.

A 2026-10-12 survey found these patterns made up most of the removable ~23% of notes/. The one-sentence rule a defect produced is what a future reader needs; the story is in git.

Parallel Agentic Development

Requirements for coordinating multiple parallel AI agents and human developers via GitHub Issues. Defines the issue hierarchy, label taxonomy, task claiming protocol, size labeling, orphan recovery, bundle selection and execution, and revised workflows for the build, plan-issue, review-priorities, and update-plan skills, and the two paths through GitHub Project #24: Schedule on Epics, and a Status path for every other Issue and a ship stage for PRs, both mirrored from facts, and where the build-to-merge pipeline places its local test gates.

File ID: PAD

Issue Hierarchy

ID Priority Requirement Related
PAD-01-001 MUST Tasks MUST be represented as GitHub Issues in the CERTCC/Vultron repository. All work is tracked exclusively as GitHub Issues.
PAD-01-002 MUST Issues MUST be organized in up to three levels: Epic (thematic group), Task (coherent unit of work), and Subtask (atomic PR-sized chunk). Leaf issues (those with no sub-issues) are the claimable work units.
PAD-01-003 MUST Epic Issues MUST be wired on GitHub Project #24 with a Schedule field value (Focus/Now/Next/Later/Someday) that reflects the current priority tier of the thematic group they represent.
PAD-01-005 MUST Task Issues MUST be sub-issues of their parent Epic. Refines: PAD-01-002
PAD-01-006 MUST Subtask Issues MUST be sub-issues of their parent Task. Refines: PAD-01-002
PAD-01-004 SHOULD The minimum depth that produces right-sized leaf issues SHOULD be used. Not every Epic requires Task or Subtask children.
PAD-01-007 MUST Every open non-Epic Issue (Task, Feature, Bug, Idea, Concern) MUST have an Epic ancestor. A non-Epic Issue with no Epic ancestor is untriaged, and triage consists of routing it to an Epic.

A non-Epic Issue carries no Schedule value of its own (PAD-02-001), so its only priority signal is its Epic's tier. Without an Epic ancestor it is invisible to every tier-ordered selector. Small work that belongs to no thematic group goes under a long-lived catch-all Epic, whose tier is set like any other Epic's.
Refines: PAD-01-005
PAD-01-008 MUST When the last open sub-issue of an Epic is closed, the Epic MUST be closed. An Epic that has never had a sub-issue MUST NOT be closed by this rule.

An Epic reaches "all children closed" only once its tasks are done and no further planning is open. Work that arrives later belongs to a new Epic, so closing needs no human confirmation and the Schedule field needs no end-of-life value.

Label Taxonomy

ID Priority Requirement Related
PAD-02-001 MUST A non-Epic Issue on GitHub Project #24 MUST NOT carry a Schedule value. Its priority tier is the Schedule value of its nearest Epic ancestor.

A child's own Schedule value could only restate its Epic's or silently differ from it, and skills could not tell deliberate from drifted. Holding back one member of an active Epic is done by moving it to a separate Epic, or with the needs-info hold (PAD-16-004), not with a per-issue tier. Priority grouping is by Epic sub-issue relationships, not by group: labels.
Refines: PAD-03-001
PAD-02-004 MUST A stale-claim label MUST be applied by the sweeper job (PAD-10) when a claimed Issue has no open PR and the claim branch has not received a commit within 3 days.
PAD-02-011 MUST Agents MUST skip Issues that carry the stale-claim label. Refines: PAD-04-003
PAD-02-005 MUST A needs-rebase label MUST be applied when an open PR has merge conflicts that cannot be automatically resolved (PAD-11). Human intervention is required before the label is removed.
PAD-02-006 MUST Docs-only PRs (containing only specs/ and notes/ changes) MUST carry the specs-notes label to identify them as docs-only for reviewers.
PAD-02-008 MUST Every thematic group that has two or more open leaf Issues MUST have exactly one open Epic Issue on GitHub Project #24. A leaf Issue is any Issue with no open sub-issues.
PAD-02-009 MUST Epic Issues MUST be created using the GitHub Epic issue type (via GraphQL createIssue with the Epic issue type's issueTypeId, discovered via the GraphQL issueTypes query) and MUST be added to GitHub Project #24 with a Schedule value (Focus/Now/Next/Later/Someday) matching the work's priority.
PAD-02-013 MUST The create-epic skill MUST be used to create Epic Issues to ensure consistent sub-issue wiring, correct issue type assignment, and addition to Project #24. Implements: PAD-02-009

Priority Ordering Authority

ID Priority Requirement Related
PAD-03-001 MUST The Schedule field (Focus/Now/Next/Later/Someday) on Epic Issues on GitHub Project #24 MUST be the authoritative source for work priority ordering. Priority changes MUST be made by updating the Schedule field on the relevant Epic — not by maintaining any external file.

The Project #24 board is the single source of truth for scheduling, and scheduling is decided per body of work, not per issue.
PAD-03-002 MUST NOT Agents MUST NOT create or write to any external priority-ordering file. Only the review-priorities skill (and humans directly) may change Schedule field values on GitHub Project #24.
PAD-03-003 MUST When the build skill selects a task, it MUST query GitHub Project #24 for Epics with Schedule=Focus or Schedule=Now (Focus first), then select an open unblocked leaf Issue that is a sub-issue of one of those Epics and passes the PAD-08-001 filters (no stale-claim, needs-info, or ready-for-human label, and not already assigned).

Task Claiming Protocol

ID Priority Requirement Related
PAD-04-001 MUST An agent MUST claim an Issue before beginning work on it by having GitHub create a branch named <prefix>/<issue-number>-<slug> linked to the Issue, then checking that branch out. The branch's existence on GitHub is the authoritative distributed lock: if it already exists, the Issue is already claimed. A bundle member other than the first is claimed by the first member's branch instead (PAD-15-009), so "the Issue's claim branch" means its own branch or, for such a member, its bundle's branch.

A branch created only in a local clone locks nothing across machines or cloud agents, and it is invisible to the stale-claim sweeper until a PR is pushed. A GitHub-created linked branch also appears in the Issue's Development sidebar. createLinkedBranch answers an existing name with a null linkedBranch rather than an error, and that null is the "already claimed" signal; gh issue develop exits 0 in the same case, so it is not a lock (ISSUE-4617).
PAD-04-002 MUST After creating the claim branch, the agent MUST assign the Issue to the triggering user, MUST set its Project #24 Status to In Progress (PAD-16-008), and MUST post a comment on the Issue identifying the agent session and branch name. An Epic claimed this way (to plan it) MUST NOT be assigned and carries no Status: its assignee is the epic-run claim (PAD-19-001, PAD-16-012).
PAD-04-003 MUST NOT Agents MUST NOT begin implementation work on an Issue whose claim branch already exists, whose stale-claim label is set, or that is already assigned to another user.
PAD-04-004 SHOULD An agent working on a claimed Issue SHOULD push the claim branch at each checkpoint of its workflow, so that the branch's last commit on GitHub reflects current activity.

The stale-claim sweeper (PAD-10-001) judges activity by the claim branch's last commit on GitHub, so work kept only in a local clone reads as abandoned.

Size Labeling

ID Priority Requirement Related
PAD-05-001 MUST At Issue creation, an Issue with 1–2 acceptance-criteria (AC) checkboxes MUST receive the size:S label.

Provides an objective, count-based size estimate at the point the Issue is created, before any implementation diff is available.
PAD-05-002 MUST At PR open time, the actual diff (total lines added plus removed across all changed files) MUST be measured and the corresponding size: label applied to the pull request. This applies to PRs whose head branch is in this repository; a PR from a fork is exempt, because the workflow that applies the label holds a read-only token there.

Replaces the creation-time AC-count estimate with an objective implementation measurement before the PR is reviewed. The label goes on the PR because that is what the measurement describes; a survey of 692 PRs merged 2026-07-22..2026-09-22 found 40% carrying no size: label at all, because the step it replaced asked an agent to write the measurement onto an Issue that was about to close.
PAD-05-003 MUST The measured size: label on the pull request (PAD-05-002) is the authoritative size of the delivered change; the AC-count label on the Issue is an estimate only.
PAD-05-010 MUST NOT The PR-open measurement MUST NOT overwrite the AC-count estimate label on the Issue.

Keeping both makes estimate-versus-actual a queryable calibration signal. Overwriting the estimate destroys the only record of what the work was predicted to cost, and does so at the moment the Issue closes, where no consumer reads the result.
PAD-05-011 MUST Every size: label — estimate or measurement — MUST be derived from the shared band table in vultron/metadata/planning/size_bands.py, via the pr-size console script.

The thresholds were previously restated in prose in eight places and computed in none, and size:S was applied correctly 24% of the time as a result. A threshold a reader can retype is a threshold that drifts; one table with one applier cannot.
PAD-05-004 MUST At Issue creation, an Issue with 3–6 acceptance-criteria (AC) checkboxes MUST receive the size:M label.

Provides an objective, count-based size estimate at the point the Issue is created, before any implementation diff is available.
PAD-05-005 MUST At Issue creation, an Issue with 7+ acceptance-criteria (AC) checkboxes MUST receive the size:L label.

Provides an objective, count-based size estimate at the point the Issue is created, before any implementation diff is available.
PAD-05-006 MUST At PR open time, a diff of ≤100 lines MUST receive the size:S label.

Calibrates the size label to the actual implementation effort measured at PR open time (see PAD-05-002). Across the 692 PRs merged 2026-07-22..2026-09-22, the ≤50 ceiling this replaces captured only 12% of them, and ~55% of the PRs labelled size:S under it were larger than the band allowed.
PAD-05-007 MUST At PR open time, a diff of 101–400 lines MUST receive the size:M label.

Calibrates the size label to the actual implementation effort measured at PR open time (see PAD-05-002). Merged-PR diff size is lognormal with a median of 258 lines and no natural clusters, so this band covers the normal unit of work (38% of the surveyed window).
PAD-05-008 MUST At PR open time, a diff of 401–1200 lines MUST receive the size:L label.

Calibrates the size label to the actual implementation effort measured at PR open time (see PAD-05-002). The previous 301+ definition was unbounded above and covered 46% of the 692 PRs merged 2026-07-22..2026-09-22, across two orders of magnitude, so it could not distinguish a large change from an unreviewable one.
PAD-05-009 MUST At PR open time, a diff of 1201+ lines MUST receive the size:XL label.

size:XL marks a PR that review cannot cover, not merely a big one. Across the surveyed window, review findings held steady at roughly 4.5 per 1000 diff lines from 200 lines through 1000, then fell to 3.5 (1000–1499), 2.7 (1500–2499) and 1.5 (2500+). True defect density does not drop threefold in large PRs, so the falling detection rate is review saturation.
PAD-05-012 MUST NOT An Issue MUST NOT receive the size:XL label from an AC-count estimate; size:XL is reachable only by measuring a diff.

An Issue predicted to need more work than size:L should be decomposed, not labelled. XL is a retrospective finding — the PR grew past the point where review stays effective — and "estimated L, landed XL" is the signal worth keeping.

Pre-PR Review Protocol

Who reviews a branch before its first push, and how the post-push triage reuses that review. The author runs a four-item self-check; a fresh agent then runs the pr-triage phases that need no Pull Request, and the post-push triage runs only the phases that do, plus the delta.

ID Priority Requirement Related
PAD-07-001 MUST Before invoking create-pr, the build skill MUST have the PR-independent phases of pr-triage (its pre-push mode) run on the branch by a subagent that did not write the change. Those phases include the code-review subagent.

An author reviewing its own change misses what it overlooked while writing it; the post-push fresh-agent triage still found a median of six problems per PR, each fixed after the push at the cost of a fix commit and a CI run.
PAD-07-005 MUST The pre-push review MUST run against the branch diff relative to origin/main with every change committed, and MUST be given the draft PR body so it can check the author's Self-check: record (PAD-07-009). Refines: PAD-07-001
PAD-07-002 MUST Every pre-push review finding MUST carry a pr-triage severity (FAIL, IMPROVE, or NEW-ISSUE) and a pr-triage decision outcome (fix-now, fix-now-file, defer-ask, or inversion-halt).

One taxonomy across the pre-push and post-push reviews lets the post-push triage carry a pre-push finding forward unchanged.
PAD-07-003 MUST The build skill MUST resolve every pre-push review finding by its decision outcome before invoking create-pr, and MUST record each finding's resolution in the pre-push artifact. Refines: PAD-07-001
PAD-07-006 MUST NOT The build skill MUST NOT re-run the pre-push review after resolving its findings. The post-push triage reviews those fixes as part of its delta (PAD-07-007).

A second full review before the push would repeat every phase on a diff that changed only where the fixes landed; the delta review covers exactly those lines, once.
Refines: PAD-07-003
PAD-07-004 SHOULD After a PR is opened, the pre-push review's findings and their resolutions SHOULD be posted as a comment on the PR for human review.
PAD-07-007 MUST When a pre-push artifact exists for a PR's branch and PAD-07-008 does not call for a full triage, pr-triage MUST run the phases that need the PR (issue linkage as posted, PR body format, the Docs: line comparison, CI status, and merge state), MUST re-run the PR-independent phases on the delta alone, and MUST confirm each pre-push finding's recorded resolution on the PR head, re-emitting any finding whose fix is absent. The delta is the change to the branch's own added and removed lines between the reviewed head and the PR head.

Every phase still runs, once per change. Defining the delta on the branch's own lines keeps create-pr's cherry-pick onto a newer base out of it.
PAD-07-008 MUST pr-triage MUST run the full triage when the PR carries the needs-rebase label, when the PR's branch has no usable pre-push artifact (none exists, it is not a JSON object, it lacks a reviewed SHA, it names another branch, or a reviewed commit is not in the object store), when the reviewed diff is empty, or when the delta's changed lines exceed half of the reviewed diff's changed lines.

A PR not opened by build, a conflict resolved by hand, or a delta most of the size of the change gives the pre-push review too little claim on the tree that will merge.
Refines: PAD-07-007
PAD-07-009 MUST The build and bugfix skills MUST run the four-item pre-review self-check (affected-text sweep, failure-path pass, branch coverage, and reuse check) on the finished diff and record its four results under Self-check: in the PR body's ## Verification section. The code-review subagent of pr-triage MUST be given the same four items as review prompts.

About 45% of post-push triage findings fall into these four oversights. A written result per item shows the reviewer the check ran, and the reviewer checking the same four items against the diff catches what the author missed.

Build Skill — Issue-Based Task Selection

ID Priority Requirement Related
PAD-08-001 MUST The build skill MUST select work by querying GitHub Project #24 for Epics with Schedule=Focus or Schedule=Now (Focus first), then querying for open unblocked leaf Issues that are sub-issues of those Epics, filtered to exclude Issues that carry stale-claim, needs-info, or ready-for-human, that are already assigned, or that are held by an epic run (PAD-19-002).
PAD-08-002 MUST The build skill MUST open a GitHub Pull Request with "Closes #N" in the body after completing implementation and passing all validations.
PAD-08-004 MUST NOT The build skill MUST NOT commit directly to main.
PAD-08-003 MUST After implementation, the build skill MUST compute the diff size and update the size label on both the Issue and the PR per PAD-05-002 before opening the PR.

Ingest-Idea — Docs-Only PR and Issue Creation

ID Priority Requirement Related
PAD-09-001 MUST After writing specs/<topic>.yaml and notes/<topic>.md, the plan-issue skill MUST open a docs-only PR carrying the specs-notes label before creating the implementation GitHub Issue. This ensures spec and notes files are on main and referenceable from Issues before any implementation work begins.
PAD-09-002 MUST After the docs-only PR is opened, plan-issue MUST create a GitHub Issue with: a title and description derived from the spec, the size: label derived from the AC count per PAD-05-001, and a link to the spec file in the body. The Issue MUST be added to GitHub Project #24 as a sub-issue of an Epic (PAD-01-007), with no Schedule value (PAD-02-001).
PAD-09-003 MUST NOT plan-issue MUST NOT begin implementation work. It MUST stop after creating the Issue. Implementation work begins only when the Issue is Ready (PAD-16-004) and is selected under its Epic's Schedule tier on GitHub Project #24.

Orphan Recovery — Stale Claim Sweeper

ID Priority Requirement Related
PAD-10-001 MUST A scheduled GitHub Actions workflow (weekly cadence, also triggerable on workflow_dispatch) MUST scan for Issues that are assigned, have a claim branch on GitHub under any claim prefix (PAD-04-001), have no open PR, and have had neither a branch commit nor their claim within 3 days.

GitHub creates a claim branch at the main tip, whose last commit can already be older than 3 days when the claim is taken. The claim time (the Issue's latest assignment) is therefore activity too.
PAD-10-002 MUST When the sweeper identifies an orphaned claim, it MUST apply the stale-claim label to the Issue and MUST post a comment identifying the orphaned branch and requesting human review.

Merge Conflict Recovery

ID Priority Requirement Related
PAD-11-001 MUST When an open PR has merge conflicts with its base branch, the agent syncing it MUST merge the base branch into the task branch and resolve the conflicts. It MUST NOT rebase a branch that has an open PR.

Rebasing a pushed branch rewrites the commits reviewers commented on and the commit_ref values the triage and execute artifacts recorded. A merge commit keeps both valid.
PAD-11-002 MUST After resolving the merge, the agent MUST push the merge commit with a normal push and MUST NOT force-push the branch. CI re-runs automatically on the updated head.

A merge only adds commits, so a normal push succeeds unless the remote branch moved. A push that git rejects as non-fast-forward means another push landed or someone rewrote the remote branch; either is a reason to fetch and inspect, never to force.
PAD-11-003 MUST If the agent cannot resolve the merge safely, it MUST abort the merge and post a comment on the PR explaining the conflict.

Ensures human reviewers have visibility into the nature of the conflict before intervening.
PAD-11-004 MUST If the agent cannot resolve the merge safely, it MUST apply the needs-rebase label to the PR.

The label signals to the sweeper and other agents that human intervention is required before the PR can proceed. Its name predates merge-based syncing and is kept so existing queries still find it.
PAD-11-005 MUST NOT If the agent cannot resolve the merge safely, it MUST NOT attempt further changes; human intervention is required to resolve the conflict and remove the needs-rebase label.

Prevents the agent from making the conflict worse or creating additional commits on a branch that requires human resolution.

Review-Priorities — Issue Triage

ID Priority Requirement Related
PAD-12-001 MUST The review-priorities skill MUST surface as a triage list for the current review session every open Epic with Schedule=Someday or no Schedule value, and every open non-Epic Issue with no Epic ancestor (PAD-01-007).
PAD-12-002 MUST For each Epic surfaced in triage, the review-priorities skill MUST use ask_user to determine which Schedule tier to place it in, or whether to close or defer it. For each unparented non-Epic Issue, it MUST ask which Epic the Issue belongs under.
PAD-12-003 MUST After the user confirms a placement, the review-priorities skill MUST apply it via the GitHub API: the Epic's Schedule field on GitHub Project #24, or the Issue's sub-issue relationship to the chosen Epic.

Update-Plan — Issue Creation

ID Priority Requirement Related
PAD-13-001 MUST When the update-plan skill identifies a new implementation gap (a spec requirement with no corresponding code or test), it MUST create a GitHub Issue for the gap.
PAD-13-002 MUST Issues created by update-plan MUST be added to GitHub Project #24 as sub-issues of an Epic (PAD-01-007), with no Schedule value (PAD-02-001) and the initial size: label derived from the AC count per PAD-05-001.

GitHub Actions Requirements

ID Priority Requirement Related
PAD-14-003 MUST A GitHub Actions workflow MUST be created to implement the stale-claim sweeper (PAD-10): run on schedule (weekly) and workflow_dispatch, scan for orphaned claims per PAD-10-001, apply stale-claim label and post comment per PAD-10-002.

Bundle Selection and Execution

ID Priority Requirement Related
PAD-15-001 MUST A work bundle MUST be selected in two stages: eligibility (open, unassigned, no stale-claim, needs-info, or ready-for-human label, all blockedBy CLOSED, leaf, not held by an epic run per PAD-19-002), then fit. Clearing eligibility MUST NOT by itself make an Issue a bundle member.

Selection was eligibility-only and took the first five candidates in sub-issue order, so an Idea, a Someday Concern and two size:L Tasks were all proposed as equally valid members of one bundle (ISSUE-3482).
PAD-15-002 MUST A bundle MUST be homogeneous by executing skill, determined from each candidate's issueType: Task and Feature are executed by build, Bug by bugfix, and Idea, Concern and Epic by plan-issue.

An Idea or Concern leaf clears every eligibility gate but cannot be implemented; it needs planning first. Routing by type is the rule work-issue already applies to a single issue.
PAD-15-003 MUST Bundle candidates MUST be ordered by their governing Project #24 Schedule tier per PAD-03-001, which is the tier of the candidate's nearest Epic ancestor (PAD-02-001). A leaf's own Schedule value MUST NOT be read. Sub-issue list order MUST be used only to break ties.

Sub-issue list order is manual drag-order and carries no priority meaning, so treating it as the priority signal silently overrode the authoritative field. A leaf's own value is drift by definition once only Epics carry Schedule.
PAD-15-004 MUST NOT An Issue whose governing Schedule tier is Someday MUST NOT be proposed as a bundle member.

Someday is explicit human deprioritisation; bundling such an Issue reverses a scheduling decision the board already recorded.
PAD-15-005 MUST A bundle MUST be bounded by a total size budget computed from the PAD-05 size: label weights, weighting an unlabelled Issue as the largest bundlable size. Member count MUST also remain bounded.

A bundle is implemented as one PR, so the budget is a review budget. An unlabelled Issue is unmeasured rather than small, so counting it as the largest bundlable size fails safe. The weights themselves are not restated here: they are derived from the band table (PAD-05-011), so a recalibration cannot leave this requirement behind.
PAD-15-011 MUST NOT An Issue labelled size:XL MUST NOT be proposed as a bundle member.

size:XL means a change that already exhausts the review budget on its own (PAD-05-009), so it is refused outright rather than given a weight. A weight would say "fits alongside something smaller", which is precisely what this band rules out, and a raised --budget could then reopen it.
PAD-15-006 MUST A bundle proposal MUST report every candidate it excluded with a per-candidate reason, distinguishing exclusion at the eligibility stage from exclusion at the fit stage.

The excluded-candidate report listed only eligibility reasons, so a candidate held back for size or tier was indistinguishable from one that was blocked or already claimed.
PAD-15-007 MUST Thematic coherence of a bundle MUST be decided by stating the single design idea its members share in one sentence. Mechanical overlap signals MAY be surfaced as evidence but MUST NOT be treated as the coherence verdict.

Cutting by superficial theme rather than design grain produces plausible-looking but wrong work units — the same failure calve-epics guards against when shaping the Epic forest.
PAD-15-008 MUST A skill given several Issue numbers MUST implement every member and open one Pull Request whose body carries one Closes #N line per member. It MUST NOT silently work only the first member or silently drop a member.

Getting more than one Issue done in a single PR is the entire purpose of bundling; executing one member and discarding the rest defeats it. A member dropped for grain is reported, not silently omitted.
PAD-15-009 MUST Every bundle member MUST be claimed onto the single branch named for the first member, and each member MUST receive the tests and acceptance-criteria coverage it would receive if worked alone.

Branch creation is the distributed lock (PAD-04), so one bundle is one lock. A bundle amortises context loading, never rigor.

Project Board Status Path

ID Priority Requirement Related
PAD-16-001 MUST Every non-Epic Issue on GitHub Project #24 MUST carry exactly one Status value: Backlog, Ready, In Progress, In Review, or Done, determined by the first of PAD-16-010, PAD-16-009, PAD-16-008, PAD-16-004/PAD-16-005, and PAD-16-011 whose condition holds.

Schedule answers "which body of work matters now" and lives on Epics. Status answers "where does this piece of work stand" and lives on everything else, so the two paths never describe the same item.
PAD-16-002 MUST An Issue's Status MUST be reproducible from facts recorded elsewhere alone: its open or closed state, issue type, sub-issues, acceptance-criteria lines, blockedBy relationships, labels, Epic ancestor, assignee and claim branch, and linked Pull Requests. A value that those facts do not produce is drift and MUST be corrected to the reproduced value.

A second, hand-maintained copy of claim or review state drifts from the branch and PR it describes, which is why a claimed label was rejected. A mirror can drift too, but recomputing it repairs it without a human.
PAD-16-003 MUST NOT An agent MUST NOT decide whether an Issue is claimable or selectable from its Status value. Claimability and selection are decided from the facts Status mirrors (PAD-04-003, PAD-08-001, PAD-15-001).

Status is a display of situation for humans. If a stale mirror could cause two agents to claim one Issue, drift would stop being cosmetic.
Constrains: PAD-08-001
PAD-16-004 MUST A Task, Feature, or Bug MUST be Ready ("ready to build") when it is open, unassigned, a leaf, has no claim branch (PAD-04-001), has an Epic ancestor, carries at least one - [ ] AC-N: line, has every blockedBy Issue closed, and does not carry the needs-info label.

These are the conditions build already applies privately when it auto-selects work, made visible so that a wrong or missing condition shows on the board instead of in a surprising pick. needs-info ("waiting on a person or decision") is the only hold.
PAD-16-005 MUST An Idea or Concern MUST be Ready ("ready to plan") when it is open, unassigned, has no claim branch (PAD-04-001), has an Epic ancestor, and does not carry the needs-info label.

An Idea or Concern is input to plan-issue, which consumes it, so it can be planned from the moment it is triaged. The board separates ready-to-plan from ready-to-build by issue type, not by a second column.
PAD-16-006 MUST NOT The Schedule tier of an Issue's Epic ancestor MUST NOT affect the Issue's Status. The tier orders selection among Ready Issues only.

"Could this be started?" and "should it be started now?" are different questions. Keeping the tier out of Status means reprioritising an Epic changes no child's Status, and planning a Next-tier Idea early or fixing a Bug under a Later Epic is an ordinary choice among Ready Issues, not an exception.
PAD-16-007 MUST NOT An agent MUST NOT auto-select or claim an Issue carrying the ready-for-human label. The label does not affect the Issue's Status.
PAD-16-008 MUST An open Issue that is claimed (PAD-04-001, PAD-04-002), including a bundle member claimed by its bundle's branch (PAD-15-009), and has no open, non-draft Pull Request that closes it MUST be In Progress. A draft Pull Request keeps the Issue In Progress.

A draft PR says its author is not finished. Marking it ready for review is the author's "done" signal.
PAD-16-009 MUST An open Issue with an open, non-draft Pull Request that closes it MUST be In Review. In Review means pr-ship may be run on that Pull Request.

Review here is agent-driven, so In Review marks work handed to the ship pipeline. Progress through that pipeline is tracked on the PR (PAD-17), not on the Issue, because one bundle PR closes several Issues.
PAD-16-010 MUST A closed Issue MUST be Done, whatever its close reason. An Issue that will not be actioned MUST be closed with GitHub's "not planned" (or "duplicate") close reason, not marked with a label.
PAD-16-011 MUST An open, unclaimed Issue that does not meet the Ready conditions (PAD-16-004, PAD-16-005) MUST be Backlog.
PAD-16-012 MUST NOT An Epic Issue MUST NOT carry a Status value.

An Epic is a container, not work that is claimed, reviewed, and merged. Its progress is the sub-issue progress GitHub already shows, and a decomposition-needed Epic is identified by its needs-decomposition label.

Pull Request Ship Stage

ID Priority Requirement Related
PAD-17-001 MUST Every open, non-draft Pull Request MUST be an item on GitHub Project #24 carrying a Ship stage value: Awaiting ship, Triage, Execute, Verify, Needs you, or Ready to merge.

pr-ship records its progress in gitignored local files, so no other machine or session can see whether a PR's triage has run, whether it is paused at a gate, or whether it is ready to merge. With several agents working in parallel, "which PRs need me?" is the question the board most needs to answer.
PAD-17-002 MUST pr-ship MUST set a Pull Request's Ship stage to Triage, Execute, or Verify on entering that phase, including when it resumes into it and when its triage reviews only the delta over a pre-push review (PAD-07-007); to Needs you while paused at a gate awaiting a human; and to Ready to merge when verification completes. The pre-push review sets no Ship stage, because no Pull Request exists yet.

The one skill that knows its own phase writes the value, so the field is a mirror of pipeline progress that needs no manual upkeep.
PAD-17-003 MUST Opening a non-draft Pull Request, or marking a draft Pull Request ready for review, MUST add it to GitHub Project #24 with Ship stage set to Awaiting ship.

PAD-17-002 covers only the stages pr-ship itself enters. Without an entry rule, a PR nobody has shipped yet is either absent from the board or has no Ship stage, and PAD-17-001 cannot hold.
Refines: PAD-17-001
PAD-17-004 MUST When a Pull Request is converted to draft, closed, or merged, its Ship stage value MUST be cleared. Marking it ready for review again re-enters it at Awaiting ship (PAD-17-003).

PAD-17-001 covers only open non-draft PRs. Without an exit rule, a PR that leaves that set keeps its last stage, and the board shows a merged or abandoned PR as Ready to merge or Needs you.
Refines: PAD-17-001

Pipeline Test-Gate Placement

Where the build-to-merge pipeline runs tests locally, and when it merges the base branch into a PR. CI on pull_request runs the full suite against the PR merged into its base as of the push, so after the first push a local run is worth its cost only when it catches something before CI would.

ID Priority Requirement Related
PAD-18-001 MUST Before an implementation Pull Request's first push, create-pr MUST run the full local unit and integration suites and push only when both pass. A Pull Request that changes no Python MUST be gated by the linters alone.

One full run keeps an obviously broken branch out of CI. It is the only point where the local suite is not repeating a CI run that is about to happen anyway. A docs-only branch has nothing for the suite to catch, so the linters are the whole gate.
PAD-18-002 MUST After a Pull Request's first push, a fix commit MUST be gated locally by the linters plus the targeted test set for the branch diff: every test file that imports a changed symbol, the mirror test file of every changed module, every changed test file itself, test/architecture/, and test/metadata/. The run MUST select those files with the integration marker enabled, so that marker-bearing tests among them are not deselected. A conflict merge with no fix in it is not a fix commit; PAD-18-004 sets its gate.

A full local run after a one-line fix costs nine to eighteen minutes on a 2-CPU slot; CI then runs the full suite again in six to nine. The targeted set catches a broken fix before that round trip at a fraction of the cost. test/architecture/ and test/metadata/ are named because their ratchets and generated-file checks read the tree rather than import a symbol, and they were where nearly every red local full run failed (ADR-0143). A changed test file is named explicitly because the import index cannot reach it: no vultron symbol changed, and the mirror lookup covers only vultron/ paths. The marker clause is needed because addopts carries -m 'not integration', dropping those tests from a file list.
PAD-18-003 MUST The fix-commit gate of PAD-18-002 MUST escalate from the targeted set to the full unit and integration suites when the branch diff touches shared test infrastructure (conftest.py, test fixtures, factories, helpers or data files outside the directories PAD-18-002 always selects, pyproject.toml, uv.lock, vultron/__init__.py), and MUST NOT escalate for any other path. When the fix is for a CI failure in a test the targeted set does not include, that test's file MUST be added to the gate.

The import index cannot see tests that reach a change through fixtures or configuration, so only those paths need the whole suite. An integration-bearing layer (vultron/core/behaviors/, vultron/adapters/, and the like) is gated by its importers like any other code, integration-marked ones included; escalating on it sent most code Pull Requests to a full local run on every fix commit, which repeated, more slowly, the full run CI makes on the push (ADR-0143). A CI failure outside the set is answered by running that test, not the whole suite: CI runs the rest again on the push. Keying on the branch diff, not the fix commit, means a fix landing on top of a conftest.py change still escalates. The list does not govern the gate after a conflict merge (PAD-18-004): applied to the post-merge branch diff it sent most conflict merges to the full suite for paths the merge never touched (ADR-0138).
Refines: PAD-18-002
PAD-18-004 MUST After a Pull Request's first push, an agent MUST merge the base branch into it locally only when the Pull Request conflicts with the base: GitHub reports it CONFLICTING or DIRTY, or a trial merge (git merge-tree) conflicts. A Pull Request GitHub reports BEHIND without a conflict MUST be updated on GitHub (gh pr update-branch), with no local merge and no local suite run. Files changed on both sides without a conflict MUST NOT cause a merge. After resolving a conflict, the agent MUST gate the merge with the linters plus the targeted set for the conflicted files alone, each treated as wholly changed, plus test/architecture/ and test/metadata/, escalating to the full suite only when a conflicted file is a conftest.py, pyproject.toml, uv.lock, or vultron/__init__.py.

CI already tests the merge of the PR into its base as of each push, and no repository rule requires branches to be up to date. A conflict is the only sync that needs an agent, because someone has to resolve it; every clean merge can be made by GitHub and tested by CI. Merging on file overlap, and deriving the post-merge gate from the whole branch diff, sent most merges to a full local suite run for paths the merge never touched. A conflicted file is gated as wholly changed because both sides edited it; the rest of the merge changed no line the PR wrote, and CI runs the full suite on the push. BEHIND is cleared on GitHub because it is the one state in which GitHub itself refuses the merge until the branch contains the base, and gh pr update-branch produces the same merge commit a local merge would; no ruleset produces it today.
Extends: PAD-11-001 — PAD-11-001 says how a conflicted PR is synced (merge, never rebase); this adds when a pushed PR is synced at all (on conflict only, or on GitHub for BEHIND).
PAD-18-005 MUST NOT pr-verify MUST NOT compute base-tip containment itself, nor withhold READY-TO-MERGE on the strength of its own containment check, when the PR is conflict-free and PAD-18-004 did not call for a merge. Only GitHub's reported merge state governs: a mergeStateStatus of BEHIND still blocks.

Requiring the base tip in the branch makes every merge to main invalidate every other ready PR, which is the loop PAD-18-004 exists to break. GitHub reports BEHIND only when a ruleset requires up-to-date branches, so deferring to it keeps the one case where being behind genuinely blocks a merge, without re-deriving it on every verify.
Depends on: PAD-18-004
PAD-18-006 MUST pr-execute's summary comment MUST record the local suite runs the Pull Request has cost in a fixed line, Suite runs: <N> full, <M> targeted. The count MUST include only runs that happened: the first-push run of PAD-18-001, which create-pr made before any execute artifact existed, counts as one full run when create-pr ran the suites, and a Pull Request that create-pr gated by the linters alone starts the full count at zero.

Local suite runs are the one pipeline cost GitHub does not record. The execute artifact is gitignored and deleted by pr-verify, so the PR comment is the only place the count survives. The first-push run has to be counted explicitly because it happens in a different skill before the comment exists; leaving it out would understate every Pull Request by the single most expensive run in the pipeline, which is the figure PAD-18-007 needs. Counting it for a docs-only Pull Request that ran no suite would overstate the cost the same way.
PAD-18-007 SHOULD Velocity reporting SHOULD report, per merged Pull Request and by week: CI runs and failed CI runs, merges from the base branch, local suite runs (PAD-18-006), and open-to-merge time, plus failed CI runs on main.

Without these, the cost of the pipeline is invisible, and a change to gate placement cannot show that it saved time without moving failures onto CI or main.
Depends on: PAD-18-006
PAD-18-008 MUST The full local gate of PAD-18-001 MUST run exactly once before a Pull Request's first push. A skill that hands off to create-pr MUST NOT run the unit or integration suites itself.

build and create-pr each ran the same four-command block back to back, with no change to the tree between them, so every implementation Pull Request paid the first-push gate twice. One of the two runs was pure duplication, and it is the most expensive single step in the pipeline. Placing the gate in create-pr alone keeps it adjacent to the push it guards, which is the only thing that makes it worth its cost.
Refines: PAD-18-001
PAD-18-009 MUST Immediately before merging a Pull Request, the agent merging it MUST check that Pull Request alone against the current base. On a conflict it MUST send the Pull Request back through the review pipeline instead of merging. When the Pull Request is BEHIND, or the base changed files it also changes, the agent MUST update it on GitHub (gh pr update-branch) and merge only after CI passes on the updated head, updating at most once per merge. The agent MUST NOT re-check or re-sync other open Pull Requests because a merge moved the base.

Overlapping files are where a clean merge is most likely to hide a behavior conflict, and the moment of merging is the only time the base the Pull Request lands on is known. A GitHub-side update tests that exact combination in CI with no agent, no local merge, and no local suite run. Sweeping every open Pull Request after each merge, or updating again whenever the base moves during the CI wait, re-creates the loop ADR-0138 removed; a semantic conflict between files neither side shares is caught by CI on main (ADR-0126).
Depends on: PAD-18-004

Epic Claim for an Epic Run

An epic run is work-epic-tasks driving one Epic's open children to completion across several waves. Its plan lives in a gitignored local file, and claiming each child only when its wave starts left later-wave Tasks looking free to every other agent. The Epic claim makes the run visible on GitHub and holds the lane it owns.

ID Priority Requirement Related
PAD-19-001 MUST An epic run MUST claim its Epic after its plan is approved and before any build starts, by assigning the Epic and posting a claim comment. The claim MUST NOT use a label, a branch, or a Project #24 field, and MUST be refused for an Epic that is already assigned.

An Epic carries no Status (PAD-16-012) and no claim branch, and the assignee is a fact GitHub keeps rather than a marker someone must maintain. Refusing an assigned Epic stops a second run on one Epic. The check and the assignment are two calls, so two runs started at the same moment can both pass; runs are started by a person, so that race is accepted rather than locked.
PAD-19-002 MUST While an Epic is assigned, every open Task and Feature whose nearest Epic ancestor it is MUST be held: selection MUST report it as not workable, with the reason. Bugs, Ideas, and Concerns under that Epic MUST NOT be held.

The run owns the build lane, including Tasks planned for later waves. Bugs, Ideas, and Concerns are left open on purpose so other agents can keep fixing and planning while the run builds.
PAD-19-003 MUST An explicit request to build a held Task or Feature MUST stop and ask the user, unless the caller names that Epic as its epic run (--epic-run <Epic>). The epic run MUST name its Epic this way for every build it starts.

Every agent runs as the same GitHub user, so the Epic's assignee cannot tell the run from a stray session. Only the run knows it is the run.
Refines: PAD-19-002
PAD-19-004 MUST An epic run MUST release its Epic at every stop, finished or not, by removing the assignee and posting a comment with the run's summary.

A claim that outlives its run holds the Epic's Tasks from everyone. Running the skill again claims the Epic again and resumes from its status file, so releasing loses nothing.
PAD-19-005 MUST The stale-claim sweeper (PAD-10) MUST flag an assigned Epic with stale-claim, and comment on it, when the claim is older than 3 days and none of the issues it holds (its descendants whose nearest Epic it is) has an open Pull Request or claim-branch activity within 3 days.

A run that crashes before its release step leaves the Epic assigned. The held issues' activity, not the Epic's, shows whether the run is still working.
Refines: PAD-10-001
PAD-19-006 MUST An epic run MUST NOT hold the Bugs it plans. A planned Bug that another agent claimed first MUST be dropped when its wave starts and reported as taken elsewhere.

Holding Bugs would stop other agents fixing them for the whole run. Dropping one that was taken is cheaper than coordinating over it.

Pre-Push Mechanical PR Checks

About one triage finding in ten is mechanical: a tool could have caught it before the first push, and each one found after the push costs an execute round. These checks run before the push and again in triage, so triage stops finding them and judges only what needs judgment.

ID Priority Requirement Related
PAD-20-001 MUST pr-body-lint MUST check, each under a stable rule ID, every rule of .agents/skills/shared/pr-body-guide.md that needs no judgment, and the guide MUST list every rule ID the tool checks.

One list in two places drifts. Pinning the guide's rule table to the tool's own rule list makes a rule the tool adds, or one the guide drops, fail a test instead of a later triage.
PAD-20-002 MUST Before a Pull Request's first push, create-pr MUST run pr-body-lint on the composed body and push only when it exits 0. For an implementation Pull Request it MUST also run spec-backstop --manifest and push only when that exits 0, and the body's ## Specs section MUST carry the resolved manifest file verbatim.

Build ran the backstop but nothing required it to pass, and one body claimed exit 0 for a run that exited 1. Requiring the exit status, and pasting the file the tool resolved, leaves nothing to retype.
Depends on: PAD-20-001
Depends on: SR-12-006
PAD-20-003 MUST Before a Pull Request's first push, create-pr MUST run spec-lint --changed-since <base> and push only when it exits 0, whatever the Pull Request's type.

The marker rule for named tests (MS-10-011) and the spec line limit (PD-10-002) have a backlog, so plain spec-lint cannot fail on them. Scoped to the branch they can, and a docs Pull Request runs no test suite to catch them otherwise.
Depends on: MS-10-011
Depends on: PD-10-003
PAD-20-004 MUST pr-triage MUST run pr-body-lint --stage final first in its body phase and report each finding it prints, and MUST judge by hand only the body rules the tool does not check.

Triage keeps every check, but a rule a tool checks is not a judgment; reading it out of the guide by hand is how the mechanical findings kept recurring.
Depends on: PAD-20-001
PAD-20-005 MUST Once the Docs: line is recorded (PD-03-008), the session that opened the Pull Request MUST run pr-body-lint --stage final on its body and fix every finding before it ends.

The placeholder create-pr writes is legal before the push and a FAIL after it. The final stage is the point where the placeholder must be gone, so that is where the check runs.
Refines: PD-03-008

Project Documentation Structure

This specification defines the purpose, scope, and maintenance requirements for key project documentation files that guide AI agents and human developers.

File ID: PD

Append-Only History Write Protocol

ID Priority Requirement Related
PD-05-001 MUST Agents MUST append new entries exclusively to the end of plan/*HISTORY.md files. Inserting content at an arbitrary line number, a specific section heading, or anywhere other than the very end of the file is prohibited.
PD-05-002 MUST NOT Agents MUST NOT perform a conditional existence check (e.g., ls, test -f, os.path.exists) before appending. Instead, agents MUST unconditionally ensure the file exists using a simple guard (e.g., touch plan/HISTORY.md), then proceed directly to the append step. The prohibited pattern is the ls → if not found: create; if found: append decision tree.
PD-05-003 MUST NOT Agents MUST NOT read the full content of an append-only file in order to append a new entry. Only the final lines need to be inspected (using view_range with the tail of the file) to locate the exact end-of-file position.
PD-05-004 MUST Agents MUST use the following canonical append procedure: 1. Ensure the file exists: touch <file> (no-op if already present). 2. Write the new entry to a temporary file, then append that file to the target using bash("cat /tmp/entry.md >> plan/HISTORY.md") — or use Python open(file, 'a') for content that contains shell-special characters. Do NOT use the edit tool for appending, because edit requires a unique old_str anchor that is not guaranteed in files with repeated structural patterns. 3. Verify the entry was written correctly: read the tail with bash("tail -30 <file>") to confirm the content appears as expected.
PD-05-005 MUST NOT Agents MUST NOT edit, reorder, or remove any existing entry from a plan/*HISTORY.md file. Once written, a history entry is permanent.

Plan Section Organization

ID Priority Requirement Related
PD-06-004 MUST NOT Priority ordering is the sole responsibility of the Schedule field (Focus/Now/Next/Later/Someday) on Epics in GitHub Project #24 (PAD-02-001); a non-Epic issue takes its nearest Epic's tier. GitHub Issue titles MUST NOT encode priority information as numeric prefixes or rely on ordering to imply priority.

Priority lived in plan/PRIORITIES.md until it was archived in favour of the GitHub Project board (#693); the board's Schedule field is now the single source of ordering. Encoding priority in issue titles would reintroduce a second, silently-drifting ranking.
PD-06-005 SHOULD Every non-Epic issue on GitHub Project #24 SHOULD be wired to the Epic it belongs to (as a sub-issue), so its priority is traceable to the thematic group it advances. PAD-01-007 makes this a MUST for every open non-Epic issue.

Replaces the former requirement that plan/PRIORITIES.md entries cite issue numbers; with priorities held on the board, the equivalent linkage is the issue-to-Epic relationship rather than a hand-maintained list.

Task Granularity and GitHub Issues Coordination

ID Priority Requirement Related
PD-09-001 SHOULD Each GitHub Issue SHOULD be scoped to approximately one Pull Request worth of work — a set of changes that can be reviewed and merged in a single PR.

This sizing keeps individual issues reviewable, linkable, and trackable within the GitHub Issues UI. Issues that span multiple PRs are difficult to close cleanly and obscure progress visibility.
PD-09-002 SHOULD When an Issue would require more than one Pull Request to complete, it SHOULD either be split into multiple PR-sized Issues or elevated to a parent Epic Issue with GitHub sub-issues representing each PR-sized chunk.

An issue that spans multiple PRs cannot be closed atomically. Splitting or elevating preserves the one-PR-per-issue invariant without forcing artificial scope reduction.
PD-09-003 SHOULD Multi-step work items requiring coordinated effort across several Issues or external visibility SHOULD have a parent Epic GitHub Issue. GitHub sub-issues SHOULD be used to link the Epic to the individual Task Issues.

GitHub sub-issues provide granular traceability within the GitHub UI, making progress visible to collaborators.
PD-09-004 MUST An Epic whose scope includes changes to the canonical CaseLedgerEntry commit path (commit call sites, commit authorization/gating, or per-actor ledger content) MUST include a closing sub-issue that reviews a fresh multi-actor demo run's per-actor ledger logs before the Epic is closed. Passing the aggregate invariant test suite alone is NOT sufficient closure evidence for such an Epic. Concretely, an Epic is "in scope" for this rule if any change within it touches CommitCaseLedgerEntryNode, create_guarded_commit_case_ledger_entry_tree, or create_commit_log_entry_tree in vultron/core/behaviors/, or any call site that invokes these directly.

The aggregate invariant suite (the per-scenario harnesses in test/ci/invariants/) can pass on main throughout a ledger-path Epic while protocol-correctness bugs — such as hash-chain forks and RM oscillation loops — remain invisible. Such bugs are only detectable from a direct side-by-side review of each actor's JSONL log. Without a standing rule requiring this review at closure, later commit-path changes (e.g., a new authorization gate touching multiple call sites) can reintroduce the same class of bug undetected.
PD-09-005 SHOULD Before scheduling an Epic whose goal is to deprecate or remove an existing artifact (a field, a class, a write path), an exhaustive inventory of read and write call sites for that artifact, across all layers, including any wire-layer or adapter-layer duplicates, SHOULD be attached to the Epic issue at creation time.

Wire-layer or adapter-layer duplicates of the artifact being removed are routinely discovered mid-epic rather than at scoping time, and they end up as late-arriving prerequisites that block the epic's final removal step. An upfront inventory surfaces these dependencies before work begins rather than after most of the epic is already complete.

Line Length Outside docs/

Prose outside docs/ wraps at 88 characters (AGENTS.md § Markdown Formatting); docs/ keeps one sentence per line with no limit (SG-38). Markdown is held to it by markdownlint; spec YAML and Python doc lines, whose corpus predates the limit, are held to it per added line.

ID Priority Requirement Related
PD-10-001 MUST Markdown files outside docs/ MUST keep lines to 88 characters or fewer, except in tables and code blocks and where the overflow is one unbreakable token such as a URL. markdownlint's MD013 MUST enforce the limit in every directory except docs/ and the archives plan/history/, plan/retired-specs/ and archived_notes/.

With MD013 off for the whole repository because docs/ has no limit, the 88-character rule was checked only by eye and reached triage as findings. plan/history/ is append-only (HM-01-005) and plan/retired-specs/ keeps retired text as it was (MS-09-005), so neither can be rewrapped; nor can the retired notes in archived_notes/.
PD-10-002 MUST NOT A change MUST NOT add a line over 88 characters with whitespace past the limit to a specs/*.yaml file, or to a comment or docstring of a Python file under vultron/, test/, integration_tests/, scripts/ or .agents/.

Both corpora hold thousands of longer lines written before the limit was checked, so a whole-file rule cannot be turned on. Holding each added or edited line to the limit moves the corpus toward it with no baseline file for concurrent changes to race on.
PD-10-003 MUST A pull-request growth guard MUST fail on each line PD-10-002 forbids that the branch adds, compared with the merge base, and spec-lint --changed-since <ref> MUST report each such spec YAML line. The limit MUST be one number shared by the guard, the root markdownlint MD013 setting and ruff's max-doc-length.

The guard runs in CI for every pull request; the spec-lint flag lets create-pr catch a spec line before a docs pull request is pushed.
Depends on: PD-10-002

Spec Registry

Requirements for the structured spec registry: YAML spec files governed by Pydantic models, with linting, pytest integration, and context-generation tooling. The registry provides a single source of truth for spec IDs, prefixes, titles, and cross-reference validation.

File ID: SR

Spec Statement Semantics

ID Priority Requirement Related
SR-10-001 MUST A spec statement MUST describe an invariant property of the finished system — what it must always be true, always do, or never do. It MUST NOT describe a migration step, a one-time task, or a before/after transition.

Specs are transferable knowledge for any implementer. A statement like "X must be replaced by Y" is meaningless once the replacement is done, and misleading before it is. The correct home for migration tasks is an issue tracker. If a migration produces a lasting architectural rule, that rule — not the migration instruction — belongs in the spec.
SR-10-002 MUST A spec statement MUST be written in the present tense and describe the system as it must perpetually be.

Temporal or conditional framing ("once X holds, Y must go away") creates specs whose satisfaction is permanent after a single event. Such specs cannot be re-tested on subsequent builds and do not communicate ongoing constraints to future maintainers or implementers.
Refines: SR-10-001
SR-10-004 MUST NOT Phrases that signal migration-task framing — "must be replaced by", "must be removed", "must be updated to", "once X is done", "after migration" — MUST NOT appear in a spec statement unless they refer to a runtime state transition (e.g. a state machine trigger), not a code change. Refines: SR-10-002
SR-10-003 SHOULD When a spec's statement has become vacuously satisfied (the migration it described is complete and no ongoing constraint survives), the spec SHOULD be deleted. When a residual constraint does survive, the statement SHOULD be rewritten to express only that constraint, dropping all migration-task language.

A vacuously satisfied spec creates false confidence: it always passes any check, so it neither constrains nor informs. A partially-stale spec that mixes a completed migration instruction with a surviving rule misleads readers about which clause is still normative. Deletion or rewrite keeps the registry honest.
Refines: SR-10-001

Story Traceability

ID Priority Requirement Related
SR-11-003 MUST The spec linter MUST flag as a hard error any kind: protocol spec with priority: MUST that has no stories: entry. This check MUST be suppressible per-spec via lint_suppress: [missing_story_reference].

Protocol-level MUST requirements represent the strongest normative obligations in the spec corpus. Making story coverage a hard-error gate for this tier ensures every binding protocol requirement can be traced back to at least one stakeholder need. Suppressibility preserves an escape hatch for implementation-detail MUSTs with no direct story antecedent. The gate covers protocol only for now: user stories for behavior Vultron inherits from ActivityStreams, ActivityPub and HTTP (kind: foundation) are not yet backfilled (#2717, #4513), and the gate widens to foundation once they are (ADR-0134).
Refines: SR-11-001
SR-11-004 SHOULD The spec linter SHOULD emit an advisory warning for any kind: protocol spec with priority: SHOULD, priority: SHOULD_NOT, or priority: MAY that has no stories: entry. This check SHOULD be suppressible per-spec via lint_suppress: [missing_story_reference]. Refines: SR-11-003
Refines: MS-02-003 — SHOULD_NOT is the SHOULD tier

Relationship Type Semantics and Direction

What each rel_type asserts, which end of a relationship is upstream, which end records it, and the spec-lint checks that keep the requirements graph free of loops so that "what does a change to this requirement affect?" has one answer.

ID Priority Requirement Related
SR-13-001 MUST An implements relationship recorded on A naming B MUST mean that A states the concrete mechanism by which B is met; B is the more general requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-002 MUST A supersedes relationship recorded on A naming B MUST mean that A replaces B, wholly or in a case A names, so that where A applies B no longer binds; B is the earlier, upstream requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-003 MUST An extends relationship recorded on A naming B MUST mean that A adds obligations to B without narrowing or changing what B already requires; B is the more general requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-004 MUST A depends_on relationship recorded on A naming B MUST mean that A cannot be met unless B is also met; B is the upstream requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-005 MUST A conflicts relationship recorded on A naming B MUST mean that A and B cannot both be met as written; neither end is upstream.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-006 MUST A refines relationship recorded on A naming B MUST mean that A restates B for a narrower case or with more precision, so that meeting A in that case meets B; B is the more general requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-007 MUST A derives_from relationship recorded on A naming B MUST mean that A states a consequence that follows from B without being a narrower case of B; B is the upstream requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-008 MUST A verifies relationship recorded on A naming B MUST mean that A states a check that confirms B is met; B is the upstream requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-009 MUST A part_of relationship recorded on A naming B MUST mean that A is one component of B, which is met only when all of its parts are met; B is the more general requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-010 MUST A constrains relationship recorded on A naming B MUST mean that A bounds how B may be met without B being a narrower case of A; A is the more general requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-011 MUST A satisfies relationship recorded on A naming B MUST mean that meeting A is sufficient to meet B; B is the upstream requirement.

Eleven type names with no stated meaning let two authors record one fact with opposite verbs, which puts loops into the graph. A definition that names the upstream end gives every edge one direction to point.
SR-13-012 MUST A relationship MUST be recorded in only one of the two requirements it links: in the downstream requirement for every type but constrains and conflicts, in the constraining requirement for constrains, and in either one for conflicts. Neither requirement may then name the other under any type.

A link recorded at both ends is one fact kept twice, and the two copies drift: authors wrote the second copy with an opposite verb, which closed a loop. The links that name a requirement can be found by filtering relation_graph.recorded_relationships() on their target, so a second copy adds nothing.
SR-13-013 MUST Where a requirement is a narrower case of another, the link MUST be recorded as refines on the narrower requirement, and the more general one MUST NOT record constrains naming it.

refines and constrains are mirror images: "A refines B" and "B constrains A" state one fact from opposite ends. Naming one of them for that fact keeps the corpus from holding both. constrains stays for a general rule that bounds a requirement which is not a case of it.
Refines: SR-13-012

Case Event Log Synchronization

Requirements for the append-only canonical recorded case ledger, replication transport, conflict handling, per-peer state tracking, and retry semantics for distributed case state synchronization across Participant Actors.

File ID: SYNC

Leadership and Ownership

ID Priority Requirement Related
SYNC-06-002 SHOULD The conditions under which replication leadership changes SHOULD be documented in notes/sync-ledger-replication.md

Vultron Project Technology Stack

This specification defines the normative technology constraints and implementation requirements for the Vultron prototype implementation.

File ID: IMPLTS

Containerization and Deployment

ID Priority Requirement Related
IMPLTS-05-007 MUST Source code MUST be hosted in GitHub using git for version control.

Code Quality Tooling

ID Priority Requirement Related
IMPLTS-07-019 MUST Every entry in the ruff ignore list MUST carry an inline comment stating what the rule reports and why this project does not enforce it. An entry MAY cite a tracking issue in place of a standing rationale when the exclusion is provisional.

An unexplained exclusion is indistinguishable from an oversight, so nobody dares delete it and the list only grows. Requiring a stated reason makes the ignore block the authoritative record of what the project has deliberately chosen not to enforce, and makes tightening a legible one-line change rather than an archaeology exercise. This is the same discipline CS-23-001 imposes on a sanctioned broad except.
Refines: IMPLTS-07-017
IMPLTS-07-020 MUST A ruff finding suppressed to keep the gate green — as distinct from a rule excluded project-wide under IMPLTS-07-019 — MUST be suppressed with a per-line # noqa marker naming both the rule and the issue that tracks its removal, and RUF100 MUST remain selected.

RUF100 reports a # noqa that suppresses nothing, making ruff itself the ratchet: a new violation without a marker fails, and a fixed site whose marker was left behind also fails. That is the property set ADR-0064 required — growth fails, silent progress fails, completion forces removal — with no hand-maintained backlog. Naming the tracking issue separates a scheduled cleanup from an unjustified suppression. ADR-0094 records why file-scoped per-file-ignores cannot substitute.
Refines: IMPLTS-07-017
IMPLTS-07-021 MUST The scope of a lint, format, or type check MUST be declared in that tool's configuration file, not in the arguments of the commands that invoke it. Callers — CI jobs, pre-commit hooks, agent skills, and documented developer commands — MUST invoke the tool with no path operands.

A scope expressed as command-line arguments is duplicated at every call site and drifts silently, because nothing compares the spellings. ADR-0094 records two drifts that were live when it was written — one left code formatted but never linted, the other made some pull requests type-check less than commits did. Declaring scope once makes a scope change one edit to one file, and makes caller disagreement impossible rather than unlikely.
Refines: IMPLTS-07-017
Constrains: IMPLTS-07-005

Testability

The Vultron inbox handler must be thoroughly testable at unit, integration, and system levels. Testability enables confidence in correctness, supports refactoring, and documents expected behavior.

File ID: TB

Architecture Boundary Tests

ID Priority Requirement Related
TB-10-001 SHOULD Once the core and wire packages are fully separated (see specs/architecture.yaml and archived_notes/architecture-review.md), architecture boundary tests SHOULD be added to enforce layer separation rules

Automated boundary tests catch accidental cross-layer imports earlier than code review.
TB-10-002 MUST Architecture ratchet tests MUST use bidirectional equality: assert actual_violations == KNOWN_VIOLATIONS. A subset check (assert actual ⊆ KNOWN) is prohibited because it allows the violation set to grow without failing the test. When a violation is resolved, its entry MUST be removed from KNOWN_VIOLATIONS in the same commit.

See ARCH-18-001 for the architecture-level rule. This entry specifies the test implementation obligation.
Implements: ARCH-18-001