# The pre-gate: generated files, cheap checks, issue links
`/ship` Step 5 opens with `gstack-pregate`: the free checks that catch what the
multi-agent wave kept paying remote gates to find (stale generated files, a
scratch script at the repo root, a nightly-only test still asserting an old
constant, a Windows-only change merged with its lane unrun, a `| tail`-hidden
failure). Plan: `docs/designs/MULTI_AGENT_WAVE_2026_10_10.md` C4, C6, C8.
Result codes: [docs/troubleshooting.md](troubleshooting.md#pre-gate-generated-file-registry-gstack-pregate-issue-links).
Three bins, all over `lib/`:
| Bin | Pin file (read from `origin/`) | Does |
|---|---|---|
| `gstack-regen` | `.gstack/generated.json` | generated files regenerate, never hand-merge |
| `gstack-pregate` | `.gstack/pregate.json` | two-stage cheap gate, `pregate.json` record, publication check |
| `gstack-issue-links` | — | `Fixes #N` only on explicit closure intent; Related PRs and validated flake issues |
## Trust boundary
A pin file names commands. Its presence never authorizes execution: both tools
read the committed copy on `origin/` (a PR cannot smuggle a command into
its own gate; `gstack-pregate run` alone also reads a user-owned file outside
the repository when the base has none, see [pregate v1](#pregate-v1)) and run commands only with `--allow-repo-commands`, which `/ship`
passes. Standalone, `gstack-regen check` lists the commands and exits 3
(`REPO_COMMANDS_NOT_ALLOWED`); `gstack-pregate` marks `regen` and `lanes`
`incomplete`, never `pass`. `--policy-from head` (a PR that introduces or
changes a pin file) is accepted only with `--allow-repo-commands` and the record
says `policy head (unreviewed)`. So a lane or allowlist entry a branch adds to
its own `pregate.json` counts only once it is on the base; `PREGATE_NO_LANE`'s
fix line says so when the branch edits the file
([troubleshooting](troubleshooting.md#pregate-no-lane-own-policy)). An unresolvable base exits 3 with
`POLICY_SOURCE_UNAVAILABLE`. Globs are repo-relative (no `..`, no absolute
paths, at most 64 per list); commands are one line under 512 bytes.
## `.gstack/generated.json` (generated-v1)
```json
{ "$schema": "https://github.com/garrytan/gstack/docs/pregate.md#generated-v1",
"entries": [ { "id": "skill-docs", "command": "bun run gen:skill-docs --host all",
"outputs": ["*/SKILL.md", "*/sections/*.md"], "inputs": ["*/SKILL.md.tmpl", "hosts/**"] } ] }
```
- `gstack-regen check` regenerates every entry (or `--only `, or `--touched
` for entries whose inputs or outputs intersect) in a scratch
`git worktree` seeded from the working tree, with `node_modules` linked rather
than copied, and reports stale outputs (`REGEN_STALE`, exit 1) without touching
the tree. When outputs differ but `package.json` declares dependencies and
`node_modules` is missing, the entry is `REGEN_DEPS_MISSING` (install, then
rerun) instead of stale. An entry with no `command` is tracked only (goldens regenerated by
another entry).
- `gstack-regen write` regenerates in place: what `gstack-restamp` and the
Step 3 conflict rule use. On a conflict in a registered output, take either
side, then `gstack-regen write`; the agent never hand-merges a generated file.
- `list`, `validate`, `init [--explain] [--force]`.
gstack's entries: `skill-docs` (every host render, the agents digest, llms.txt,
the doctor component table) and `ship-goldens` (`bun scripts/gen-ship-goldens.ts`,
the byte-pinned `/ship` renders in `test/fixtures/golden/`).
## `.gstack/pregate.json` (pregate-v1)
| Key | Meaning |
|---|---|
| `runner` | the local test command with `{files}` (gstack: `bun test {files}`); unset, it is detected (bun, vitest, jest, pytest, `node --test`, and `flutter test`/`dart test` per Dart package root, through fvm when that package or an ancestor pins it with `.fvmrc` or `.fvm/fvm_config.json` and fvm is on PATH; `cargo test -p ` per Rust crate, run from the root of the workspace the crate is a member of (Cargo's `members` globs, `exclude` and `package.workspace`), else the crate directory; a JS/TS test whose nearest package.json is a nested package declaring vitest or jest (a dependency or its `test` script) runs from that package, with package-relative paths, through the package manager its `packageManager` field or the nearest lockfile up to the repo root names: `pnpm exec`, `yarn`, `bun x`, else `npx`; every other JS/TS test keeps the root detection); nothing detected, a Dart SDK, cargo or package manager missing from PATH, a `.dart` file with no `pubspec.yaml` above it, a `.rs` file outside any crate, or one in a crate whose name Cargo would reject (anything but letters, digits, `_` and `-`), is `incomplete` (the runnable groups still run, so a real failure still fails) |
| `dependencies` | glob → test files for subprocess targets, runtime reads and generated inputs the import graph cannot see; `[]` declares "no lane needed" (docs, licences) |
| `empty_lanes` | extra globs for the docs/no-tests empty lane. Any Markdown file already qualifies with no pin file when no test depends on it (the same import graph as other files: a quoted path, a `join()` of path segments, an import, or a `dependencies` entry). reStructuredText and AsciiDoc count as Markdown here. README, CHANGELOG, CONTRIBUTING, LICENSE, AGENTS and CLAUDE qualify even without `.md`, and so does licence text under any vendored name (`roboto-LICENSE.txt`, `LICENSE-MIT`, `licenses/*.txt`). Source code and files under a test directory never qualify. A test that reads the file still selects that test |
| `selection` | `workflow.yml/job` → a command printing the lane's file list, for matrices a planner computes (gstack: `scripts/test-free-shards.ts --list`) |
| `warn` | check ids downgraded to warn; `secrets` and required lanes never |
| `strays.allow` | intentional root files or new top-level directories |
| `local` | `workflow.yml/job` lanes a local run satisfies: a platform-agnostic toolchain (an `ubuntu-latest` suite that runs the same on a Mac), or a repo whose CI is off. Their tests run here instead of becoming `requires-remote`, except that on a machine under 8 cores with a `remote_gate` or a remote `test_backend` the small-machine rule still records the test tier as a `requires-remote` obligation. `--explain` notes an id that matches no workflow lane |
| `guards` | `[{ id, when: [globs], command, fix?, timeout_s? }]`: repo commands a lane must pass when a touched file matches `when` (manifest checks, subprocess goldens the import graph cannot see); see [guards](#guards) |
| `serial_patterns` | test-file globs the `lanes` check runs one process per file, apart from the unit files (default `**/*.serial.test.*`) |
| `sealed` | sealed or eval output that must never reach a public destination; read by the pre-push hook and `gstack-pregate sealed` (see [sealed output](#sealed)) |
| `price_file` | repo-relative path of the paid gate's call-plan data file (`GSTACK_EVAL_CALLS v1`); `gstack-eval-plan price-gate` prices from it and never executes anything ([docs/eval-calls.md](eval-calls.md)) |
| `stall_timeout_s` | seconds a test command may print nothing before its process group is killed and the check is `incomplete` (`PREGATE_STALLED`); default 300, `0` disables; `--stall-timeout` overrides (see [supervision](#supervision)) |
| `remote_gate` | the repo's remote test gate command; under 8 cores the local test tier becomes a `requires-remote` obligation naming it, and `--remote` runs it (see [remote gate](#remote-gate)) |
| `allowlist` | `[{ finding, paths?, pattern?, reason }]`: benign findings the `secrets` check accepts and names as allowlisted, by gstack-redact finding id or non-secret category, path globs and a pattern anchored at the finding; never `env.value` (see [allowlist](#allowlist)) |
Only the `origin/` copy or the user-owned file below is honoured, so a
downgrade is reviewed (or is the user's own) before it takes effect. A repo that
keeps `.gstack/` out of git (a shared repo where gstack is one contributor's
tool) can hold the same file at `/projects//pregate.json`. The
state root is `GSTACK_STATE_ROOT`, else `GSTACK_HOME`, else `GSTACK_STATE_DIR`,
else a gstack plugin's data directory, else `~/.gstack` ([docs/state-root.md](state-root.md)); `` is what
`~/.claude/skills/gstack/bin/gstack-slug` prints as `SLUG=` in the repository
(`GSTACK_PROJECT_SLUG` overrides it). It is read only when the base carries none
(never under `--policy-from head`), it lives outside the repository so a PR still
cannot reach it, and the record names it (`policy user `) and fingerprints
its bytes (`policy_digest`, and each check's `inputs.policy`); `verify` and the
ship receipt treat an edited or deleted user file, or one that appears (or a
link on its path that starts pointing nowhere) after a run under the defaults,
as `PREGATE_STALE`. A path that is relative, resolves inside the repository, or
reaches it through any symlink on the way (a state root linked into the
checkout, or a link hop inside it, even when the last hop points back outside)
is ignored, since a PR could commit it or choose the file; `--explain` says
so, or says `(absent)` when no file is there. The slug and state root come from
the caller's environment, never the checkout's `.env`: the bin's shebang passes
`--no-env-file`, and a bare `bun gstack-pregate` launched beside any `.env` file
refuses (`POLICY_SOURCE_UNAVAILABLE`). `verify` reads both paths from the
record, not from its own environment.
It covers `gstack-pregate run` only: `sealed` and `price_file` are read from the
base branch's copy, so the user file refuses them (`PREGATE_CONFIG_INVALID`)
rather than holding a policy nothing reads. A user file that exists but cannot
be read, or a symlink on its path that points nowhere, refuses the same way
instead of falling back to defaults. A base copy git cannot read (and
`git ls-tree` cannot confirm is absent) refuses with `POLICY_SOURCE_UNAVAILABLE`
instead of handing the run to the user file.
`gstack-pregate validate` / `init` cover the working-tree file.
**No policy yet.** A repository with no `.gstack/pregate.json` on the base
branch and no user-owned file runs under the defaults, and a touched file that
maps to no test is then a **warning**, not the hard `PREGATE_NO_LANE`: the
`lanes` row is `warn` with `PREGATE_NO_POLICY`, the selected tests still run
(a red lane is still `fail`, an unknown platform matrix is still a
`requires-remote` obligation), and the table prints a starter policy drafted
from the repository (`PREGATE_STARTER:` lines: the detected runner, the test
directories, the CI lanes, then the JSON between `begin` and `end`, with a
`dependencies` entry per unmapped touched file mapped to the tests nearest it
or sharing its stem, never `[]`, and a `TODO` line for each file nothing
covers). `run` writes nothing; `gstack-pregate init` is the explicit write.
Once any policy exists (committed on the base, or the user-owned file), an
unmapped file is `PREGATE_NO_LANE` again: the warning is for the first /ship
in a new repository (three Captains lost their first pass to it), never a way
to hide missing coverage. `--policy-from head` with a head policy is strict too.
`gstack-pregate init` writes a draft built from the repo, not a fixed example.
It prints the languages it found and the runner each language's tests would
get, then maps every file the import graph cannot place to the tests of the
nearest directory above it that holds at most 25 of them, up to the
256-entry schema limit. The draft never
writes `[]`, which would clear a file without a test. A file with no nearby
test, and a lane whose matrix a planner computes, is printed as a `TODO`.
Review each entry before committing the file on the base branch.
## `.gstack/always-run.json`
`{ "tests": [globs] }` names whole-tree tests that scan every file of a kind (tripwires, ratchets, conformance and fixture inventories). A diff-selected run never maps a touched file to them, so they used to pass the selected gate and then fail CI (GST-98). When a branch selects any test, owes a lane, or edits this file, the `lanes` selection adds every tracked test file a glob matches. `--explain` prints each one as ` (always_run, .gstack/always-run.json)`, and a glob that matches no test as ` matches no test file`. A docs-only branch adds nothing.
The list lives in its own file, not in `pregate.json`, because older pregate readers reject unknown keys there. A repository that ignores `.gstack/` (gstack does) tracks the file with `git add -f`, as it does `pregate.json`. A missing file means an empty list. A file that is not JSON, or whose `tests` is not a list of non-empty strings, stops the check with that message. gstack's own list matches 16 test files; the 13 with a seed in `scripts/free-test-durations.json` (recorded 2026-10-19) total about 13 s there, and `bun-entry-isolation`, `launch-env-echo-ratchet` and `review-packets-removed` have no seed yet.
## `gstack-pregate`
```
gstack-pregate [--stage preflight|tests|all] [--only ] [--allow-repo-commands] [--policy-from base|head]
[--timeout ] [--stall-timeout ] [--remote --spend-cap ] [--run ] [--explain] [--json]
gstack-pregate verify | clear --remote --run-url | lanes | validate | init
```
Two stages with separate clocks, both printed:
**Stage 1, mechanical preflight** (the two-minute promise; gstack measured
~2.5 s for `regen` and under 200 ms for each of the others):
- `regen`: every registry entry whose inputs or outputs are touched is fresh
(`gstack-regen check` in a scratch worktree). A fresh entry with touched
outputs then runs once more with those outputs deleted; the ones the command
recreates byte-identically are the check's `proven` paths, which
`gstack-ship-receipt carry` reads. The verdict comes from the first run: an
output the second run does not recreate, or a second run that fails, is
simply not proven.
- `secrets`: `gstack-redact` over the branch's added lines (merge-base to the
working tree, untracked files included). HIGH, or MEDIUM in the `secret`
category, fails; other MEDIUM findings warn. Cannot be downgraded.
- `strays`: a new file at the repository root, a new top-level directory, or a
scratch name (`zz-*`, `tmp*`, `scratch*`) anywhere.
- `literals`: for every changed exported constant or named timeout/retry
literal, the whole test tree (nightly- and Heavy-only files included) is
searched for the old value; a consumer the branch did not update fails with
`file:line`.
- `timing`: added or changed test lines that compare an elapsed wall-clock or
CPU time against a bound, or sleep and then assert, print
`TIMING_ASSERTION: :` and a
`TIMING_ASSERTIONS: found= escaped= files=` summary. It warns
(`PREGATE_TIMING_ASSERTION`); it does not block. See [timing](#timing).
- `guards`: each guard in the active policy (`.gstack/pregate.json`, or the
user-owned file) whose `when` globs match a
touched file (renames included) runs its `command` in its own process, under
the verified launch Bun, with its own `timeout_s` (default `--timeout`). A
non-zero exit fails pregate naming the guard and its `fix`
(`PREGATE_GUARD_FAILED`); a guard that could not start, or whose own binary
is off PATH (exit 127), is the machine's, `PREGATE_ENVIRONMENT`
(`incomplete`), not a guard failure; a timeout is `incomplete`; without
`--allow-repo-commands` the selected guards are listed and the check is
`incomplete`. Guards are read from the base branch's copy (or the
user-owned file when the base carries none) and cannot be
downgraded to warn. See [guards](#guards).
**Stage 2, test execution:**
- `lanes`: touched files map to tests through `lib/pregate/import-graph.ts`
(static imports ∪ repo-path string literals ∪ the `bin/` →
`test/*.test.ts` convention ∪ `dependencies`; Dart `import`/`export`/`part`
directives resolve `package:/x` through every tracked `pubspec.yaml`, and
`*_test.dart` files under a `test/` directory are tests, except under `integration_test/` and
`test_driver/`: they need a device, so pregate never runs them, and a touched
one has no lane until you declare `"**/integration_test/**": []` (and
`"**/test_driver/**": []`) under `dependencies` and let CI run them; Dart files, device tests
included, are source code, so a `dependencies` `[]` declaration clears them and `empty_lanes`
does not); in a Rust crate (the nearest Cargo.toml with a `[package]` name) a `.rs` file is a
test when it sits under `/tests/`, holds a `#[cfg(test)]` (any cfg predicate naming
`test`) or a test attribute (`#[test]`, `#[tokio::test]`), and every `.rs` file of the crate and its Cargo.toml select the crate's tests and
those of every crate whose manifest depends on it, directly or through other crates (cargo
runs a crate's tests, not a file, so the lane is the crate; a crate with no tests still has
no lane, and a workspace-only Cargo.toml or Cargo.lock needs a `dependencies` entry),
transitively through
non-test files; deleted and renamed files select the tests that named them.
A removed test (a deleted one, or a rename's old name) that no remaining
test selects owes no lane, and prints `removed (no lane owed): —
` (the `lanes` detail adds `N removed path(s)`), when it is a JS, TS,
Python, Dart or bats test and no remaining file imports it (directly or
through a directory it reads; otherwise `still imported by …`). It runs
the tests of each remaining non-Markdown file that mentions its name
without the final extension anywhere (`alpha.test`, `test_alpha`:
a path with or without the extension, or a dotted module), such as a curation list (`files naming it run: …`), and fails closed when
such a file has no test (`still named by …, which no test covers`). A test
renamed to another test path then defers to its new name (`old name of
`). A deleted test, or one renamed to a non-test path, also runs its
owning suite, the remaining tests that directly exercise the files its
merge-base text exercised (`owning suite runs: …`; test helpers and
fixtures are not subjects), and names each file left with no test at all
(`coverage removed: has no remaining test`, and `M subject(s) lost
their last test` in the detail). Every other removal owes what an edit
owes, as before: a deleted or renamed Go or Ruby test (it may define
helpers its package still uses), a deleted test whose merge-base text
cannot be read (a symlink, invalid UTF-8, over 1 MiB), and any removed
non-test path, a renamed source file's old name included, are still
`PREGATE_NO_LANE` until a test or a `dependencies` entry covers them. A
modified file whose only test the branch deleted still has no lane, and a
remaining test that reads a deleted file or directory is selected. /review
lists a deleted test as a gate edit (RH-1) either way.
An extensionless JS/TS import (relative, a tsconfig/jsconfig `paths` alias or
`baseUrl`) tries the exact path, then `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`,
`.cjs`, `.mts`, `.cts`, then `index.ts`, `index.tsx`, `index.js`, `index.jsx`
and `index.mjs` inside the directory; the first file that exists wins.
A touched source file with no lane is `FAIL lanes — has no test lane`
once any policy exists (the base copy, the user-owned file or `--policy-from
head`); with no policy anywhere it is the `warn` `PREGATE_NO_POLICY` with a
starter printed ([no policy](#no-policy)); never a pass. Markdown that no
test depends on is a docs/no-tests empty lane
instead (`docs/no-tests: N documentation file(s)`), including when the base
branch has no `.gstack/pregate.json`. The dependency is the same import graph
as every other file, so a test that reads the Markdown (a fixture or a doc)
still selects that test. `empty_lanes` adds globs for other documentation;
it cannot clear a source file or a file under a test directory.
Files no test runner loads take a second empty lane by the same rule
(`no-tests (assets and tool config): …`): images, fonts, media and PDFs
outside snapshot, golden and fixture directories, plus an allowlist of tool
configuration (`.editorconfig`, `.gitignore`, `.pre-commit-config.yaml`,
lint and format config, `.vscode/`, `.idea/`, `.impeccable/`, and GitHub
issue templates, CODEOWNERS and dependabot config). Stylesheets (they import
each other), any other dotfile (`.rspec`, `.husky/` hooks, `.env*`, version
pins), CI workflows and actions, and `.gstack/` still need a lane.
A release manifest the branch only re-stamped is waived as
version-stamp-only (`version-stamp-only (no lane needed): …`): the file's
diff against the merge base changes nothing but its version values. Three
file kinds qualify, each by its own exact rule (lib/pregate/stamp-only.ts).
A JSON file (`*.json`) whose two sides parse to objects of the same shape
(the same own keys in the same order in every object, the same length in
every array, the same type at every leaf), whose only differing leaf is the
document's top-level `version` with a bare version on both sides (`1.2.3`,
`v1.2.3.4`, `1.2.3-rc.1`; never a range), and whose text is line-for-line
identical once that line has its old quoted version replaced by the new
one. A plist whose differing lines are all `` versions under
`CFBundleShortVersionString` or `CFBundleVersion`. A
`VERSION` or `VERSION.txt` file (any case) holding one bare version, with at
most a trailing LF, on both sides. Texts are compared byte for byte outside
the stamped value, so a line-ending change or a BOM is content. Nothing
nested in JSON qualifies, so a dependency pin or range, a lockfile entry, a
catalog or a package named `version` is never a stamp; nor is a changed
description, a new, removed or reordered key, a floor such as
`minHostVersion`, `schemaVersion` or `MinimumOSVersion`, a reflow, a change
the parser would erase (a big integer, a duplicate key), a file that is not
valid UTF-8 or is over 1 MiB, a mode change or a symlink on either side, an
added, deleted or renamed manifest, or any other file kind: each keeps its
`PREGATE_NO_LANE`. The rule runs only after the import graph, the
declarations, the regen registry and both empty lanes found nothing, so a
manifest a test reads still selects that test, and never under a test,
fixture, golden, snapshot, `.gstack/` or CI-workflow tree, whose JSON a test
may compare against by a path the graph cannot see.
`--explain` prints the verdict per file (`version-stamp-only against
(version 1.0.0 → 1.0.1)` or `not version-stamp-only
()`).
In a Django project (a manage.py or code importing django, plus Python
code, not a string or comment, that assigns `INSTALLED_APPS` or
`ROOT_URLCONF`, or a config file names `DJANGO_SETTINGS_MODULE`), the files
Django loads by name get `framework` edges: every Python test depends on
each settings module; a dotted string in a settings module and an
`include("app.urls")` argument count as imports; an app's migrations,
`admin.py`, `templates/`, `static/`, `templatetags/`, `management/`,
`fixtures/` and `locale/` files select that app's tests and the tests that
reach its `models.py` (the settings module, so the suite, when the app has
no tests of its own); a template under a project-level `templates/`
directory selects the suite through the settings module.
Lanes and platform tiers come from `.github/workflows/*.yml`
(`gstack-pregate lanes` prints them; a test path a step names resolves against
that step's `working-directory`, else the job's or workflow's
`defaults.run.working-directory`): local ones run through the runner under
`set -o pipefail`, judged on the runner's summary line (zero tests ran is a
failure, a `| tail`-hidden exit is still an exit; cargo's `test result:` lines, one per
test binary and doc-tests included, are summed); a platform lane this
machine cannot run becomes a `requires-remote` obligation naming
`workflow.yml/job`, unless `local` names that lane. `--explain` prints each test's provenance and every
uncertain mapping (a planner-computed matrix without a `selection` manifest
selects conservatively). Test files matching `serial_patterns` (default
`**/*.serial.test.*`) run one process per file after the unit files. The
runner and every guard run with the verified launch Bun first on PATH: the
first of `$BUN_INSTALL/bin` and `~/.bun/bin` whose Bun satisfies gstack's and
the project's `engines.bun` (the row prints `runtime: bun from `),
so a Capy host's older image Bun cannot flip the verdict.
- Every test-tier command (the lanes runner, each guard, the `--remote` gate)
runs supervised: see [supervision](#supervision). `--timeout` bounds each
runner process (the unit-file group, each serial file, each guard without
its own `timeout_s`), not their total; the stall timeout catches a hung or
silent one sooner.
- Under 8 cores, a `remote_gate` in the active policy or a remote `test_backend`
(`gstack-config set test_backend ubicloud`) records the local test tier as a
`requires-remote` obligation instead of running it: see [remote gate](#remote-gate).
Every check records the input identity it ran against (the working-tree
fingerprint before and after, the selection, the command hash); a tree that
moved during a check, a timeout or a missing tool is `incomplete`, never `pass`.
The record is `.gstack/tmp/pregate.json` (`gstack-artifact validate --as pregate`).
**Publication.** `gstack-ship-receipt write --pregate .gstack/tmp/pregate.json`
(Step 18) refuses unless the record is current for the tree being published
(`PREGATE_STALE`: rerun after the final stamp), green (`PREGATE_LANE_FAILED`,
`PREGATE_INCOMPLETE`, or `PREGATE_ENVIRONMENT` when every incomplete check is
the machine's) and every `requires-remote` obligation carries a receipt
(`PREGATE_REMOTE_UNCLEARED`: dispatch the job, then
`gstack-pregate clear --remote --run-url `). The ship
receipt then carries `pregate` and `requires_remote`. `gstack-pregate verify`
runs the same check standalone.
**By tree.** Every record `gstack-pregate` writes is also mirrored, byte for
byte, to `/pregate/by-tree/.json` (the state root from
docs/state-root.md). `gstack-ship-queue ready` reads the checkout's
`.gstack/tmp/pregate.json` first; when that file is missing or was recorded
for another tree, or is current but still waits on a remote receipt, it looks
the current tree up in the mirror and uses that record when it verifies,
printing `PREGATE: … (from the state root's pregate/by-tree/.json,
recorded by another checkout)`. This is for the lane whose `gstack-pregate
--remote` had to run from a separate clone (a worktree cannot host it): the
clone's record clears the lane's own checkout as long as both hold the same
tree. Only a run that holds the tests stage stands in (what `--stage tests
--remote` writes, `checks=[lanes]`; a `--only timing` or preflight-only run
is "a run without the tests stage"), and it must be recorded against
`origin/` and not under `--policy-from head`;
otherwise the `PREGATE:` line says why the mirror cannot stand in. A record
whose own checks failed or stayed incomplete is final and no mirror overrides
it. An explicit `--pregate ` that verifies is used as before; a mirror
for a different tree changes nothing. Writing the mirror is best effort (a
temp file renamed into place) and never fails the record itself.
Unknown flags exit 2 with usage on every verb (an agent told to run a flag this
gstack lacks never gets a green it did not ask for). Tier 2 checks still to come
(`ratchets`, `lint`, `links`, `hermetic`, `patches`, `integrator`) are named in
`--only`'s help.
## Supervision: progress, stalls, deadlines
GBRA-95 (gbrain, 4 cores): a core edit selected 3,683 local files, 358 of them
serial; the test stage printed nothing for 90 minutes: each runner process
ran up to its own `--timeout` (one per serial file) with its output buffered
until it returned, and nothing noticed a process that had gone quiet. Now each
test-tier command:
- runs in its own process group (`setsid`), with stdout and stderr appended to
a log file, never a pipe, so a backgrounded grandchild that keeps the output
open cannot hold the check; after the command exits, the group's leftover
members are killed;
- reports live on stderr (stdout stays the table, or one JSON document with
`--json`):
```
PREGATE_TEST: / start
PREGATE_TEST: / pass|fail
PREGATE_HEARTBEAT: