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, ../../AGENTS.md,
../../REVIEW.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
Makefilestays authoritative for the existence and recipe of everymaketarget.[families.qemu-smoke]captures legacyrun-*and migratedtest-*focused proofs as one family rather than enumerating hundreds of concrete targets. “Gate C” below is therun-*-to-test-*test split retiring those legacy names. .cargo/config.toml[alias]is authoritative forcargohost-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’sEvidence:trailer resolves to a registered gate or a real Makefile target / cargo alias. The ordinary workflow base is the newest comparable merge-base from localmainandorigin/main. Ref refresh is limited to a detached, standalone clone whose top-level and private common Git directory match the runtime’s documentedvibe-loop/main-verification/<repository-id>/repocache shape. A linked worktree, ordinary standalone clone, release-tag checkout, or explicitly pinnedWORKFLOW_BASEnever mutates refs. In a recognized reusable checkout, the checker force-fetches the source repository’smainintoorigin/main, requires it to equal the checked-out verificationHEAD, and updates the checkout’s private localmainto 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-Itemfrom 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 whenmainandorigin/mainalready equalHEAD, and commit acceptance does not depend on an optionalRun-Idtrailer. If no first-parent commit identifies the candidate, the check fails while namingHEADand the missingPlan-Itemtrailer. Other checkouts validateHEAD --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 isHEAD. An explicitWORKFLOW_BASEinstead validates<WORKFLOW_BASE>..HEADwithout refreshing refs. -
../../tools/slice-review/slices.tomlnames each per-slice check bygate = "...";review_slice.pyresolves the command from this registry. -
tools/workflow_gates.py for-pathsresolves the named gates made mandatory bypathsglobs 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-registryfails when a named gate command no longer resolves to its authoritative Makefile target, Cargo alias, or repository script.make workflow-checkinvokes this subcommand explicitly throughworkflow-gates-test. The complete named-registry scan includes every path-mandatory gate. -
make workflow-checkrejects a branch that adds a new literalrun-*rule target to the rootMakefile.runandrun-displayare the only operator entrypoints; focused proofs usetest-*. The check compares target sets parsed by the workflow-gates resolver with the merge base, so prerequisite edits, target-specific variable assignments, block moves, andrun-*totest-*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-checkruns../../tools/run_target_inventory.pyas a failing Gate C debt ratchet. It derives every current literalrun-*target from the rootMakefile, including literal targets that share a rule head with variable-expanded targets, and treats every name except therun-displayoperator entrypoint as migration debt. Rules whose recipes only invoke QEMU remain debt. The inventory groups debt by the first component afterrun-and labels additional recipe or prerequisite evidence such as harnesses, assertions, and timeout wrappers.run-target-budget.tomlrecords 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-refreshrecords 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-checkruns../../tools/run_target_artifact_logs.pyas a debt ceiling over staletarget/run-*.logartifact paths still written by migratedtest-*recipes. The tool reuses the Gate C Makefile parser and records the sorted unique path set and count inrun-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. Runpython3 tools/run_target_artifact_logs.py --update-baselineto record only reductions.workflow-gates-test, a prerequisite ofmake 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-checkaudits command-formmake <target>citations in every Git-tracked or non-ignored untracked Markdown and Typst file, plus direct.ymland.yamlfiles under.github/workflows/. It also audits comments and string literals intools/**/*.sh,tools/**/*.py,tools/**/*.js,tools/**/*.rs, andcapos-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 byexit 2. References to live noncompliantrun-*targets remain Gate C migration debt and emit warnings grouped by target. Their non-exempt per-target counts are pinned inrun-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. Runpython3 tools/check_doc_make_targets.py --repo . --update-baselineto 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 complianttest-*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.tomlis the only exemption authority: it exemptsdocs/changelog.mdas historical record and records other historical-evidence or reconcile-owned exceptions as exactpath:targetentries 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 taskvalidationfield 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]:
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
- Renaming or removing a gate is not complete until the snapshot is
synchronized; until then
audit-validation-gatesreportsregistry_current=false. - 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
readytransition 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:
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
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.