mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 15:57:33 +01:00
The guards check runs each .gstack/pregate.json guard whose globs match a touched file in its own process with its own timeout (PREGATE_GUARD_FAILED, never downgradable, incomplete without --allow-repo-commands). The lanes runner and guards get the first Bun in $BUN_INSTALL/bin or ~/.bun/bin that satisfies gstack's and the project's engines.bun; serial_patterns files run one process each. price_file replaces the unreleased price_command key: a paid gate is priced from a call-plan data file, never by running a command. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
181 lines
10 KiB
Markdown
181 lines
10 KiB
Markdown
# 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/<base>`) | 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 |
|
||
|
||
## Trust boundary
|
||
|
||
A pin file names commands. Its presence never authorizes execution: both tools
|
||
read the committed copy on `origin/<base>` (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.
|
||
|
||
<a id="generated"></a>
|
||
## `.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 <id>`, or `--touched
|
||
<paths>` 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/`).
|
||
|
||
<a id="pregate-v1"></a>
|
||
## `.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.*`) |
|
||
| `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/<base>` 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 <check,..>] [--allow-repo-commands] [--policy-from base|head] [--explain] [--json]
|
||
gstack-pregate verify | clear --remote <workflow.yml/job> --run-url <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/<name>` →
|
||
`test/<name>*.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 — <file> 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 <v> from <dir>`),
|
||
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 <workflow.yml/job> --run-url <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.
|
||
|
||
<a id="guards"></a>
|
||
## 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).
|
||
|
||
<a id="issue-links"></a>
|
||
## `gstack-issue-links`
|
||
|
||
```
|
||
gstack-issue-links [--base <ref>] [--issue <n>]... [--ci-run <id>]... [--out issues.json] [--json]
|
||
```
|
||
|
||
Reads `origin/<base>..HEAD`. `Fixes #N` only on explicit closure intent: a
|
||
`Fixes`/`Closes`/`Resolves #N` trailer line or `--issue N`. A bare mention, a
|
||
partial fix, a deferred ask and every cross-repo `owner/repo#N` are `Refs`. Each
|
||
closure issue is read through `gstack-issue-guard`; one that cannot be read is
|
||
`Refs #N (unreadable …)` with `ISSUE_UNREADABLE` (exit 1), never `Fixes`. Each
|
||
`--ci-run <id>` is searched among open issues whose body names it and rendered
|
||
`Flake: #N (run <id>)`. The printed lines go under `## Issues` in the PR body;
|
||
`--out` writes the object `gstack-ship-receipt write --issues @file` embeds.
|