# 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) 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)`. 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. 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}`); without it `lanes` is `incomplete` | | `dependencies` | glob → test files for subprocess targets, runtime reads and generated inputs the import graph cannot see; `[]` declares "no lane needed" (docs, licences) | | `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 | | `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)) | Only the `origin/` copy is honoured, so a downgrade is reviewed before it takes effect. `gstack-pregate validate` / `init` cover the working-tree file. ## `gstack-pregate` ``` gstack-pregate [--stage preflight|tests|all] [--only ] [--allow-repo-commands] [--policy-from base|head] [--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). - `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`. - `guards`: each guard in `.gstack/pregate.json` 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 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 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`), transitively through non-test files; deleted and renamed files select the tests that named them. A touched file with no lane is `FAIL lanes — has no test lane`, never a pass. Lanes and platform tiers come from `.github/workflows/*.yml` (`gstack-pregate lanes` prints them): 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); a platform lane this machine cannot run becomes a `requires-remote` obligation naming `workflow.yml/job`. `--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 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`) 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. 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. ## Guards and the lane-done rule A lane runs `gstack-pregate run --allow-repo-commands` on its branch before it reports done, and its handoff carries the `PREGATE:` line (the coordinator contract says so word for word). The unit tier covers the changed files' tests; guards cover the repo's mechanical gates the import graph cannot see: ```json { "guards": [ { "id": "serial-manifest", "when": ["test/**/*.serial.test.ts", "scripts/serial-files.tsv"], "command": "bun test test/scripts/serial-files.test.ts", "fix": "add the file's row to scripts/serial-files.tsv" } ] } ``` `dependencies` still maps globs to extra *tests* the lanes check selects; a guard is a *command* with its own fix line. Older gstack refuses unknown keys (`PREGATE_CONFIG_INVALID`), so a repo commits `guards` only after the gstack that reads them ships. The worked gbrain configuration is in [gbrain worked configs](#gbrain-worked-configs). ## Sealed output: the pre-push hook and `gstack-pregate sealed` ```json { "sealed": { "paths": ["heldout/**"], "transcripts": ["runs/**/transcript*.jsonl"], "answer_markers": ["SEALED ANSWER:"], "seed_patterns": ["shuffle_seed=\\d+"], "local_paths": true } } ``` A repo with a `sealed` key gets a configured local check against accidental publication whenever the managed pre-push hook (`gstack-redact install-prepush-hook`, no reinstall needed) or the standalone preflight runs: ``` gstack-pregate sealed [--range ] [--remote origin] [--base ] [--json] ``` - **Boundary.** Every commit the push publishes: `git rev-list --not --remotes=` (and the remote tip when the ref exists); a destination with no refs means everything reachable. `--range` names the commits explicitly. - **Rules.** `sealed.path` and `sealed.transcript` check every path a commit adds, renames or copies, whatever its content (binary, empty, deleted again in a later commit); `sealed.answer`, `sealed.seed` and `sealed.local_path` (`/home//`, `/Users//`, `C:\Users\\`) check each commit's added lines. Findings print rule, `file[:line]` and commit, never the text. - **Policy.** The union of the destination base branch's `sealed` and the pushed commit's: a push can tighten it, never relax it. A malformed or unreadable policy refuses. Waivers come only from the base branch's `.gstack/sealed-allow.tsv` (`rulepathshareason`) and are listed on every push. - **Visibility.** `sealed.visibility` from the base branch, else a cache under the state root (`sealed-visibility.json`: a `public` answer for 24 hours, a `private` one for 1 hour before it may turn a refusal into a warning), else `gh repo view --json visibility` (5 s). Unknown, unreadable or newly created counts as public. Public refuses (`SEALED_OUTPUT_REFUSED`, exit 1); private warns. Capy's pull-request tool may not run local git hooks, so an agent runs `gstack-pregate sealed` before any push or PR creation in a repo with a `sealed` config (the coordinator contract says so). Hosted CI on the public repository only detects a bypass after the upload. ## `gstack-pregate regate`: does the gate still cover HEAD ``` gstack-pregate regate --against --bundle [--require ] [--pr ] [--from ] [--predecessor-pr | --predecessor-ref ] [--base ] [--json] gstack-pregate --against ... the same verb ``` It evaluates a tree receipt now (never a stored one) between the gated head and HEAD under the base branch's ship policy, so the repo's `stamp_files` and `stamp_patterns` decide what a stamp is ([ship-policy](ship-policy.md#tree-receipt)). Reuse needs an evidence bundle produced on the gated tree, the same execution identity, and a predecessor head that is still the recorded one (read from `--from`, default `.gstack/tmp/gate-ahead.json` when it names this gated head). It prints each differing file (a non-stamp difference with its first line), then: ``` STAMP_ONLY: yes|no files= [changed=] LANE: