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
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
Secrets Management
Artifact Integrity
Maintenance
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 || 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. || 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
Documentation maintenance
Tutorials
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
Explanation
File metadata
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.
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.
Site information architecture — stakeholder types, levels, and routing
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).
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 | --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
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
Skill and AGENTS.md Integration
Timestamp Field and Future-Date Rejection
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.
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
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
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
Cross-References and Sources
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)
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
Maintenance
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
Task Claiming Protocol
Size Labeling
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
Ingest-Idea — Docs-Only PR and Issue Creation
Orphan Recovery — Stale Claim Sweeper
Merge Conflict Recovery
Review-Priorities — Issue Triage
Update-Plan — Issue Creation
GitHub Actions Requirements
Bundle Selection and Execution
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
Plan Section Organization
Task Granularity and GitHub Issues Coordination
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 rulesAutomated 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 |