# Workflow gates registry

`gates.toml` in this directory is the machine-readable **workflow-gates
registry**: the single authority for each capOS workflow gate's name, command,
what it proves, applicability (slice class / hazards / path patterns), and
enforcement point.

It exists to end gate-description drift. Gate prose used to be restated across
[`../../CLAUDE.md`](../../CLAUDE.md), [`../../AGENTS.md`](../../AGENTS.md),
[`../../REVIEW.md`](../../REVIEW.md), [`../../REVIEW_CYCLE.md`](../../REVIEW_CYCLE.md),
`../../tools/check-workflow-hygiene.sh`, per-task loopyard `validation` fields,
and `../../tools/slice-review/slices.toml`; every process change meant editing
all of them. Now those places reference gates by name and read this registry.

## Authority model

- This registry is authoritative for each **named** gate.
- The `Makefile` stays authoritative for the **existence and recipe** of every
  `make` target. `[families.qemu-smoke]` captures legacy `run-*` and migrated
  `test-*` focused proofs as one family rather than enumerating hundreds of
  concrete targets. "Gate C" below is the
  [`run-*`-to-`test-*` test split](../backlog/run-targets-and-init-policy.md)
  retiring those legacy names.
- `.cargo/config.toml` `[alias]` is authoritative for `cargo` host-test aliases.

## Consumers

All consumers read the registry through `../../tools/workflow_gates.py`:

- `../../tools/check-workflow-hygiene.sh` (`make workflow-check`) validates that
  every gate-shaped token in a commit's `Evidence:` trailer resolves to a
  registered gate or a real Makefile target / cargo alias. The ordinary
  workflow base is the newest comparable merge-base from local `main` and
  `origin/main`. Ref refresh is limited to a detached, standalone clone whose
  top-level and private common Git directory match the runtime's documented
  `vibe-loop/main-verification/<repository-id>/repo` cache shape. A linked
  worktree, ordinary standalone clone, release-tag checkout, or explicitly
  pinned `WORKFLOW_BASE` never mutates refs. In a recognized reusable checkout,
  the checker force-fetches the source repository's `main` into `origin/main`,
  requires it to equal the checked-out verification `HEAD`, and updates the
  checkout's private local `main` to the same commit. A missing remote, failed
  fetch or ref update, or mismatched tip fails the check rather than using
  stale containment evidence. Runtime main integration locks serialize use of
  the reusable checkout, so successive verifications can move these private
  refs to different tips without sharing a stale boundary.

  Evidence validation in that post-integration checkout discovers the
  candidate's `Plan-Item` from the first non-empty first-parent trailer, bounds
  the candidate segment at the first different first-parent predecessor, and
  validates matching commits from the bounded ancestry. This includes merged
  topic commits owned by the candidate while excluding commits from other
  board items, including mainline work that landed between verifications. A
  fresh checkout therefore validates the candidate even when `main` and
  `origin/main` already equal `HEAD`, and commit acceptance does not depend on
  an optional `Run-Id` trailer. If no first-parent commit identifies the
  candidate, the check fails while naming `HEAD` and the missing `Plan-Item`
  trailer. Other checkouts validate `HEAD --not main
  origin/main`, excluding history contained by either mainline ref even when
  local and remote main have diverged; when neither ref exists, the ordinary
  base fallback is `HEAD`. An explicit `WORKFLOW_BASE` instead validates
  `<WORKFLOW_BASE>..HEAD` without refreshing refs.
- `../../tools/slice-review/slices.toml` names each per-slice check by
  `gate = "..."`; `review_slice.py` resolves the command from this registry.
- `tools/workflow_gates.py for-paths` resolves the named gates made mandatory
  by `paths` globs for changed repository files. Its output is a lower bound;
  task acceptance, slice class, hazards, and focused behavior proofs can add
  gates that path matching alone cannot infer.
- `tools/workflow_gates.py check-registry` fails when a named gate command no
  longer resolves to its authoritative Makefile target, Cargo alias, or
  repository script. `make workflow-check` invokes this subcommand explicitly
  through `workflow-gates-test`. The complete named-registry scan includes
  every path-mandatory gate.
- `make workflow-check` rejects a branch that adds a new literal `run-*` rule
  target to the root `Makefile`. `run` and `run-display` are the only operator
  entrypoints; focused proofs use `test-*`. The check compares target sets
  parsed by the workflow-gates resolver with the merge base, so prerequisite
  edits, target-specific variable assignments, block moves, and `run-*` to
  `test-*` renames do not mistake existing Gate C debt for a new target.
  Multi-target and variable-headed rules use the resolver's normal semantics;
  pattern rules do not represent concrete targets.
- `make workflow-check` runs `../../tools/run_target_inventory.py` as a
  failing Gate C debt ratchet. It derives every current literal `run-*` target
  from the root `Makefile`, including literal targets that share a rule head
  with variable-expanded targets, and treats every name except the `run-display`
  operator entrypoint as migration debt. Rules whose recipes only invoke QEMU
  remain debt. The inventory groups debt by the first component after `run-`
  and labels additional recipe or prerequisite evidence such as harnesses,
  assertions, and timeout wrappers. `run-target-budget.toml` records a
  grandfathered name set and count. A normal refresh writes the exact current
  set and matching count; accepting a lower count is defense-in-depth against a
  hand-edited allowlist expansion that does not also raise the recorded ceiling.
  The check fails when a non-compliant name is absent from the set or the total
  exceeds the ceiling, while removals pass against a stale baseline. `make
  gate-c-target-inventory-refresh` records only removals and refuses a larger
  set or count. A Makefile or baseline parse failure exits 2: an absent or
  unreadable inventory must not look like a zero count.
- `make workflow-check` runs `../../tools/run_target_artifact_logs.py` as a
  debt ceiling over stale `target/run-*.log` artifact paths still written by
  migrated `test-*` recipes. The tool reuses the Gate C Makefile parser and
  records the sorted unique path set and count in
  `run-target-artifact-log-budget.toml`. New paths or a count above the
  baseline fail; removals pass without changing the baseline. The check also
  compares against the workflow merge base, or the budget-introduction commit
  when the merge base predates the budget, so editing the path list and count
  cannot admit new debt. Run
  `python3 tools/run_target_artifact_logs.py --update-baseline` to record only
  reductions. `workflow-gates-test`, a prerequisite of `make workflow-check`,
  exercises the documented refresh command and rejects documentation drift.
  This ratchet contains deliberately deferred naming debt; it does not perform
  or authorize the coordinated artifact rename.
- `make workflow-check` audits command-form `make <target>` citations in every
  Git-tracked or non-ignored untracked Markdown and Typst file, plus direct
  `.yml` and `.yaml` files under `.github/workflows/`. It also audits comments
  and string literals in `tools/**/*.sh`, `tools/**/*.py`, `tools/**/*.js`,
  `tools/**/*.rs`, and `capos-js/**/*.mjs`, without treating executable Make
  invocations or ordinary uses of the verb “make” as citations. Git-ignored
  generated aggregates and nested workflow files are excluded. Inline-code and
  fenced-code commands may use leading Make options and variable assignments;
  prose beginning with “make” is not a command citation. Workflow shell lines
  also recognize wrapped Make invocations and command substitutions. Path
  exemptions record the Python release workflow because its Make invocations
  build an external Cap'n Proto source tree, and the checker's regression suite
  because it embeds deliberately invalid citations as fixture data. The checker
  imports the Makefile parser from
  `../../tools/run_target_inventory.py`, fails every reference to a missing
  target, and also fails verification instructions that resolve only to a
  retirement diagnostic followed by `exit 2`. References to live noncompliant
  `run-*` targets remain Gate C migration debt and emit warnings grouped by
  target. Their non-exempt per-target counts are pinned in
  `run-target-citation-budget.toml`; an increased count or the first citation
  of an unlisted debt target fails, while removals pass against a stale
  baseline. The workflow check also compares the working budget with the Git
  baseline so a hand-edited increase cannot admit new debt. Run
  `python3 tools/check_doc_make_targets.py --repo . --update-baseline` to
  record only reductions. New current instructions link to the existing
  command-owning page instead of repeating a debt-target citation. If a new
  location must carry the runnable command, migrate the target to its
  compliant `test-*` name through the Gate C work first, then cite that name;
  do not raise the budget or create a historical exception for a current
  instruction. `doc-make-target-exceptions.toml` is the only exemption
  authority:
  it exempts `docs/changelog.md` as historical record and records other
  historical-evidence or reconcile-owned exceptions as exact `path:target`
  entries with a task owner. An entry that no longer matches any citation
  fails as unused, so exceptions are removed with their owning reconciliation
  rather than becoming permanent filename allowlists.
- `tools/workflow_gates.py check-vocab <entries...>` validates the gate-shaped
  tokens in a loopyard task `validation` field against the registry (free-form
  ad-hoc validation commands pass) for a loopyard-side adapter or hook.

## Runtime enforcement

The capOS host configuration defines a static universal floor equal to the
prerequisites of `make check`. Each prerequisite is a separate completion
command so a newly resolved runtime contract can record one `gate_result` per
check. The ordered `[runtime].universal_gates` list in `gates.toml` is the
tracked authority that maps each opaque `completion.commands[N]` key to a
named gate and its command. `tools/workflow_gates.py runtime-list` renders that
mapping; prose does not restate it.

`tools/workflow_gates.py check-runtime-config` checks that the operational
configuration's completion commands resolve exactly from that tracked list,
that pre-review `orchestration.gates` and post-integration
`orchestration.verify_on_main` contain the same derived keys, and that the
named gates equal the `make check` prerequisite set. `make workflow-check`
runs this comparison when an operational config is present. A warm reference
run of the aggregate took 326.47 seconds and 1,619,720 KiB peak RSS; the
configuration accepts that universal cost twice.

Runtime contracts are immutable per run. A configuration change applies only
after the runtime reloads it and resolves a new contract; it does not add gates
to an already recorded contract. To credit the universal floor, compare the
contract source config with the tracked registry using
`check-runtime-config --expected-digest <contract-source-digest>`, require the
derived keys in both `gates` and `integration.verify_on_main`, and require a
passing result for every pre-review key. A narrower or digest-mismatched
contract remains narrower.

Loopyard task `validation` values are advisory requirements for the worker and
reviewer, not executable runtime input. They are descriptive task data and may
contain free-form commands, while runtime gates must be static references to
allowlisted completion commands. Workers resolve registered names through this
registry, add task-, hazard-, and behavior-specific gates, and supply their
evidence for review. Reviewers must verify that evidence; even a complete
universal gate set does not imply that the task-specific list ran.

## Loopyard vocabulary contract

Loopyard reads this same file -- there is no second registry -- but it accepts a
**narrower** vocabulary than `tools/workflow_gates.py` does. `_load_validation_gate_registry`
reads only the `[gates]` and `[aliases]` tables; it does not expand
`[families.qemu-smoke]` and does not consult the `Makefile`. So a bare `run-*` /
`test-*` token is a valid `Evidence:` trailer while being an unregistered
`validation` value, and a proof a task record needs to cite has to be a named
gate here.

Loopyard keeps a synchronized snapshot of that vocabulary -- the name list plus
a digest over it -- in its database, and refuses to execute task gates while the
snapshot and this file disagree. The `validation` field is a control field bound
to that snapshot, so a task write naming an unregistered value is rejected
outright. Two consequences for anyone editing `[gates]` or `[aliases]`:

```bash
loopyard project control-value audit-validation-gates -p capos <repo>   # classify, no writes
loopyard project control-value sync-validation-gates  -p capos <repo>   # file -> database
```

1. Renaming or removing a gate is not complete until the snapshot is
   synchronized; until then `audit-validation-gates` reports
   `registry_current=false`.
2. Synchronize **after** re-pointing the task records that name a value being
   retired, never before. The sync replaces the whole name list atomically, and
   every open record still holding a retired value then fails its next control
   write -- including the `ready` transition that would dispatch it.

The new-run-target and Markdown-target checks are high-confidence hard
failures, not reviewer heuristics. Existing unresolved future-design citations
are not converted into a hidden allowlist: the Markdown check compares the
repository-wide cited-target set with the merge base and prevents the unresolved
set from growing.

## QEMU proof outcome contract

`../../tools/qemu-proof-outcome.sh` is the shared classifier for completed QEMU
proofs whose only passing `isa-debug-exit` status is 1. Harnesses that adopt it
invoke it before their own assertions. It delegates the exact startup-stall
signature check to `../../tools/qemu-startup-stall-guard.sh`, which is also the
companion guard for harnesses that intentionally accept timeout status 124
after reaching their success marker.

`qemu-proof-outcome-adoption.toml` is the machine-readable authority for the
adoption state of every QEMU harness reached from a `test-*` target. It records
`status-1-only`, `accepts-124`, and `exit-0` classifications. The unclassified
list is grandfathered migration debt. `make workflow-check` follows literal
prerequisites and recursive `$(MAKE)` calls, rejects debt growth relative to
the workflow base (or the registry-introduction commit when the base predates
the registry), and `make qemu-proof-outcome-adoption-refresh` records only
removals. The checker derives each classified harness's contract from helper
invocation and executable QEMU-status handling, then requires the recorded
class to match. A derived `status-1-only` harness must invoke
`qemu-proof-outcome.sh`. An `accepts-124` harness must invoke
`qemu-startup-stall-guard.sh` before its status and transcript assertions for
every status-bearing QEMU boot whose accepted outcomes include 124. The checker
also applies that accepts-124 guard rule to legacy `tools/qemu-*.sh` harnesses
outside `test-*` reachability, without adding those harnesses to the adoption
registry.

The helper returns 2 for invalid invocation and 3 when status 124 accompanies
an existing, empty kernel UART log; exit 3 means the proof did not start
observably and must be rerun, not that its assertions failed. All other
outcomes fall through to the harness's status and transcript assertions.
Proofs that intentionally accept status 124 use the companion guard rather than
the status-1-only helper.

When that exact status-124/empty-UART signature fires, the guard appends one
JSON Lines record to
`../../target/qemu-startup-stall-diagnostics.jsonl` before returning 3. The
record has schema version 1 and contains the UTC timestamp; proof name derived
from the kernel-log basename; kernel and terminal/QEMU log paths and byte
sizes; stdout/stderr TTY booleans; the host's three load-average values and
online CPU count; `/dev/kvm` presence and read/write accessibility; and the
first `qemu-system-x86_64 --version` line. Unavailable facts are JSON `null`.
The retained tail is lock-protected and limited to 256 records and 256 KiB;
diagnostic-capture failure does not change the established exit code or
message. Non-stall outcomes do not create or modify the artifact.

Summarize the retained artifact without running a proof:

```bash
python3 tools/qemu_startup_stall_report.py
python3 tools/qemu_startup_stall_report.py --json
python3 tools/qemu_startup_stall_report.py path/to/diagnostics.jsonl
```

The default path is `target/qemu-startup-stall-diagnostics.jsonl`. A missing or
empty artifact reports zero stalls. The report includes totals by proof,
first/last timestamps, stdout and stderr TTY posture, KVM presence and
accessibility, QEMU versions, and one-minute load average normalized by the
recorded online CPU count. The fixed load buckets are less than 0.5, 0.5 to
less than 1, 1 to less than 2, and at least 2 per CPU, plus `unavailable`.
`--json` emits the same report with sorted object keys.

The reader accepts only the guard's schema version 1 record shape and enforces
the same 256-record and 256-KiB ceilings. It rejects malformed, structurally
invalid, unsupported-schema, or changing input rather than producing a partial
summary. It reads only the selected artifact and does not modify it. The report
describes conditions recorded when stalls occurred; it does not establish that
TTY posture, KVM access, host load, QEMU version, or any other recorded
condition caused a stall.

The standing recovery instruction remains a serialized rerun without a PTY.
The guard does not retry automatically: the artifact is diagnostic evidence
for resolving the startup mechanism, not authority to alter a proof's
pass/fail contract.

## Using the reader

```bash
python3 tools/workflow_gates.py list                       # registered gates + families
python3 tools/workflow_gates.py resolve run-smoke          # -> make run-smoke
python3 tools/workflow_gates.py for-paths docs/status.md    # path-mandatory gates
python3 tools/workflow_gates.py check-registry
{
  git diff --no-renames --name-only "$(git merge-base HEAD main)"
  git ls-files --others --exclude-standard
} | sort -u | python3 tools/workflow_gates.py for-paths --stdin
python3 tools/workflow_gates.py check-evidence "make run-net; git diff --check"
python3 tools/workflow_gates.py check-vocab "make workflow-check" run-smoke
python3 tools/workflow_gates.py runtime-list
python3 tools/workflow_gates.py check-runtime-config <contract-source-path> \
  --expected-digest sha256:<contract-source-digest>
```

Path globs use repository-relative POSIX paths. `*`, `?`, and character classes
match inside one path component; a complete `**` component spans zero or more
components. Output is sorted as `<gate-name><TAB><command>` so both agents and
scripts can consume it without scraping prose.

Named gate commands use one of three auditable forms: `make <target>`, `cargo
<alias>`, or a repository-relative script path, optionally preceded by
`python3`, `python`, `bash`, or `sh`. Make and Cargo commands name exactly one
target or alias; a Make target must have an explicit rule head because a stale
`.PHONY` declaration alone exits successfully without running a recipe or
prerequisite. Script commands may carry arguments after the path. A deliberately
aspirational entry must carry a non-empty `liveness_exemption` reason in
`gates.toml`. The exemption is not a compatibility alias and must not preserve
a command after its implementation authority is renamed or removed.

## Markdown links and reachability

`../../markdown-links.toml` declares the reader entrypoints and maximum link-hop
bound used by `../../scripts/check-md-links.py`; its optional `exclude` list
names generated Markdown paths that are not source documents. The checker
validates relative Markdown targets and requires every working-tree Markdown
file under `docs/` to be reachable from a configured root. Its staged mode
builds the graph from the full working tree but limits broken-link and orphan
reports to staged Markdown files, so an unchanged index can make a newly staged
page reachable without making unrelated existing files part of the pre-commit
result. `make workflow-check` separately runs the full-tree mode so deleting an
index link cannot hide an unchanged subtree from pre-review enforcement.

Reachability follows Markdown links, not directory depth. An orphan diagnostic
names the closest reachable document in the undirected link graph when one
exists, providing a likely index to connect without treating reverse links as
reader reachability.

## Documentation budgets

`doc-budgets.toml` is the repository-specific authority for Markdown size
ratchets and structural prose-run ceilings. `make workflow-check` reads it
without modifying it. `make doc-budget-refresh` is the explicit write path:
after documentation shrinks, it lowers size baselines toward their targets and
grandfathered structural baselines to the measured values. A size baseline
stops at its target, and refresh never raises either kind of baseline. A
required increase is therefore a direct, reviewable edit to the TOML file.

Before each deadline, content between a target and its recorded baseline warns;
growth above the baseline fails. After the deadline, the target is the hard
limit. Structural checks warn above 4,000 bytes of unbroken prose and fail above
12,000 bytes unless the file has a recorded lower-only grandfather baseline.
Fenced code, lists and their indented continuations, tables, and blockquotes do
not contribute to that prose measurement.

The staged pre-commit check evaluates only size-budget groups intersecting the
staged paths and structural limits for staged Markdown files. A budget past its
deadline therefore blocks changes to its governed documents, not unrelated
source-only commits. Staging `doc-budgets.toml` evaluates the complete
configuration and corpus.

## Documentation source line width

`md-line-length.toml` bounds how wide a source line under `docs/` may be, so a
documentation edit diffs as words rather than as a whole-line replacement.
`../../tools/check_md_line_length.py` runs from `make workflow-check` and from
the staged pre-commit hook; `make md-line-length-fix` is the rewrap path and
`make md-line-length-refresh` the baseline write path.

The line limit applies only where Markdown can express the break. The checker
reflows a candidate paragraph itself and reports a line only when that reflow
lowers the paragraph's widest line, so a width forced by one unbreakable atom --
a long URL, an inline code span, a whole inline link -- is neither reported nor
rewritten. Reflow refills at the paragraph's own widest conforming line rather
than at the ceiling, so a paragraph wrapped at 80 columns stays there. A
continuation line never begins with an atom that would start a different block,
so a rewrap cannot turn prose into a spurious bullet, heading, or quote.

Code is exempt outright, because its content is verbatim. That covers a fence
at any indentation, including one nested inside a list item, and an indented
code block four columns past the enclosing item's content column. The checker
tracks open list items for exactly that reason: without them a six-column list
continuation and a six-column code block are indistinguishable.

Two line kinds cannot carry a source line break and are measured against their
own ceilings with per-page baselines instead: pipe-table rows, which GFM gives
no row continuation, and front-matter lines, which the parser in
`../../tools/mdbook-doc-metadata/mdbook-doc-metadata.js` reads one at a time so
a folded scalar is truncated at the break. `make md-line-length-refresh` lowers
a baseline to the measured width and seeds one for a page that has none; it
never raises an existing entry, so widening an already-baselined table or
front-matter value fails until it is restructured or shortened, or the TOML is
edited directly for review. A baseline naming a file outside the configured
scope fails, so an exemption cannot outlive the line it was granted for.
`docs/AGENTS.md` states the authoring rule and the reasoning behind each exempt
category.
