Files
gstack/docs/pregate.md
T
cadbd0af15 v1.91.116.0 feat: field list 2 wave B: gstack-ship-queue, approval classes, state-machine review, pregate timing check, update-check main moves (#3235)
* fix(capy): the install trailer says behind=<n> and its fix, so a stale install shows in every relayed trailer (field list 2 item 13, #2378)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(capy): a lease records its holder's start identity, so a pid reused after a wake is reclaimed; a skipped session-start upgrade ends fail behind, never ok (GBRA-105, GBRA-106)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(lease): stale needs proof (unreadable or malformed identities keep the lease), ps identity is timezone-independent, and reclaim never deletes a concurrent renewal

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(autoplan): enforced round cap and spend budget, deferred close, public export without machine paths, opt-in durable checkpoints (field list 2 W5-W7)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(lease): one directory lock for publish, prune and reclaim; reclaim rereads the record judged stale, so a renewal is never deleted or hidden

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(lease): the directory lock names its holder; broken only when the holder is dead or its pid reused, unlock removes only its own lock, touch exits 3 under a live holder

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(lease): breakers serialize on .lock.break and reread the lock before removing it; gstack-skill-start reports a lease the held lock kept out

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(lease): name the double-crash limit of breaking a dead lock

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(capy): --for review-voice install profile, no-Drive bootstrap command, reviewer prompt trailer (field list 2 W11)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 89bf556c11)

* test: pin review-voice row, helpers row and the no-Drive fallback in existing install tests

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 7f6c70459a)

* test: the DRIVER template names the reviewer trailer relay (pregate lane)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 2545d9a6d2)

* test(context-budget): capy skill eager ceiling for the review-voice subagent paragraph (W11; ratchet protocol, capture value)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(eval-plan): prereg diagnostics dry run (W8) and .gstack/eval-models.json lint (W9), WIP

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit b509096218)

* feat(eval-plan): W8/W9 tests, docs/eval-models.md, troubleshooting anchors, /eval-plan steps

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 9335e45de2)

* test(eval-plan): eval-plan/SKILL.md names only defined codes and accepted flags

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit a7a974d268)

* test(review-voice): the launch line points at the verdict's bun-only directory (#3233's launch contract)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(third-party-actions): the Aside drive-offer detector matches the product name, not the English idiom (paid run 38988927989 tpa-absent-linux false positive)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): export refuses artifact paths outside the run before writing; a deferred close keeps earlier undecided findings; reopening CEO grants Eng its rerun (review of #3234)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(restamp): gate-ahead keeps the lane CHANGELOG entry when both PRs took the same tentative stamp (GBRA-102 diagnosis fixture)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 43c75a6bb6)

* test(tree-receipt): a real gbrain stamp pair is same-modulo-stamps under gbrain committed policy (W1 acceptance)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 6cf53f9fed)

* feat(ship-policy): gate_command, full_e2e and min_gstack_version; unknown keys stay fail-closed; validate prints introducing versions

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 878a7123d7)

* feat(ship-queue): gstack-ship-queue ready, stamp, verify and gate-ahead (WIP, tests next)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 409a5957ad)

* test(ship-queue): stamp journal and resume, verify from a separate clone, gate-ahead attempts, ready

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit ade554087b)

* docs(ship-policy): Run a shared merge queue; new keys and compatibility rule; troubleshooting anchors; /ship pointer

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 7fc73e8892)

* fix(ship): shared-queue pointer rides the show-effective ship: line (ship skeleton has 2 bytes of parity headroom)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 5b33d4a015)

* test(windows): curate the shared-queue tests out of the Windows subset (shebang spawns)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 8deee14728)

* test(fixtures): register the gbrain stamp-pair fixture as free-only

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 563a07aa03)

* test(ship-queue): receipt digest contract names lib/ship-queue.ts for its pregate lane

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 501637c258)

* feat(pregate): timing check flags elapsed-time bounds and sleep-then-assert in added test lines (W4)

lib/pregate/checks/timing.ts prints TIMING_ASSERTION: <file>:<line> <pattern>
and a TIMING_ASSERTIONS summary; gstack-timing-ok: <reason> in any comment
syntax on the line or the line above escapes one. It warns
(PREGATE_TIMING_ASSERTION): measured on gstack (9 of 47 test-touching
commits, 263 hits in the tree) and gbrain (37 of 95 commits; 31 of 38 sites
of the five timing-flake fixes), blocking would stop a fifth to two fifths
of test changes. Adds the /review checklist rule and the /investigate
timing-flake playbook.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit a3b86b10b2)

* feat(update-check): an equal VERSION with a moved main is an upgrade, via the compare API (W12)

When VERSION matches but ls-remote's main SHA differs from this checkout's
HEAD, bin/gstack-update-check asks the GitHub compare API (status, ahead_by)
through the existing receipted curl and prints
UPGRADE_AVAILABLE <ver> <ver>+<n> <sha>; a diverged or unreadable compare is
CHECK_FAILED (unknown), and behind/identical is up to date. Never fetches.

The cache gains an IDENTITY line (head, compared remote SHA, upstream): an
outcome replays only for the same HEAD and upstream, a same-remote refresh
reuses it without another compare, and a git checkout ignores a legacy
one-line cache (non-git installs keep the one-line format). The snooze is
keyed by the SHA. gstack-upgrade's question, snooze note and What's New step
handle the four-field line; gstack-skill-end reports it.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 421e66d2a4)

* fix(pregate): treat planning and evidence markdown as an empty lane

A docs-only diff with no .gstack/pregate.json failed lanes as
PREGATE_NO_LANE. Planning and evidence Markdown is now a docs/no-tests
empty lane. Unmapped source files still fail closed.

Co-authored-by: Nick <surlymo@apache.org>
(cherry picked from commit e8793e59e8)

* fix(pregate): keep test-read markdown on its test lane

Unmarked Markdown that no test depends on is a docs/no-tests empty lane.
A file a test reads, including a fixture, still selects that test through
the import graph. Unmapped source and unreferenced test-tree files still
fail closed.

Co-authored-by: Nick <surlymo@apache.org>
(cherry picked from commit 7aabbddf3d)

* test(pregate): reviewer edges for the docs/no-tests empty lane (#3215)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 2fb3fa8613)

* test(pregate): requires-remote on a gbrain-shaped repo (ci:ubicloud, UBI_OWNER) (W10)

A base pregate.json with gbrain's dependencies and remote_gate
"bun run ci:ubicloud" (a package script whose stub needs UBICLOUD_API_KEY,
names VMs from UBI_OWNER and prints PREGATE_REMOTE). Under 8 cores the test
tier is one obligation naming the command; --remote passes the caller's
UBI_OWNER and key through and clears it on the report; without the key the
gate's own error shows and the obligation stays uncleared. #3190's path
needed no change.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 4d69ff02df)

* test(pregate): the ship receipt counts the timing check (7 checks)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit cb03ef67e7)

* test(context-budget): gstack-upgrade eager ceiling for the W12 equal-version upgrade question (ratchet protocol, capture value)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(approval-classes): gstack-approval-classes and ship-receipt citation enforcement (W2, WIP)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 847c7dae3d)

* feat(approval-classes): tests, docs, /review and /ship wiring (W2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 269bd5b8b5)

* feat(review): state-machines specialist and gstack-state-matrix round-trip check with a captured #6792 regression (W3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 1a634979c6)

* fix(review-army): keep the --simplification force flag pinned; approval test timeouts

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 789d392bce)

* test(free-fixtures): register the state-matrix-6792 fixture

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 1bacef296d)

* test(state-matrix): cover state-machines NEVER_GATE in gstack-specialist-stats

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 859063d56f)

* test(fixtures): register the pregate-timing fixtures as free-only (W4)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-queue): full_e2e needs bound gate evidence in ready and verify, CI reads every check-run page and commit status, next: quotes its paths (review #3235 P1-1, P1-5, P2-1)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(pregate): timing reads a native diff and never passes an unread one

The timing check's git diff honored diff.external, textconv and
diff.noprefix, so a configured driver hid every added line and the check
passed. Run it with --no-ext-diff --no-textconv and explicit a/ b/
prefixes, and report a git diff that fails or does not finish as
incomplete (PREGATE_INCOMPLETE), which a warn downgrade does not soften.

Review of #3235, sibling of P1-4 (lib/pregate/checks/timing.ts:189).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(approval-classes): only affirmative APPROVED evidence counts, explicit tokens are authoritative, native diffs fail closed (review #3235 P1-2, P1-3, P1-4)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(state-matrix): detect reads the native diff, ignoring diff.external and textconv (review #3235 P1-4)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(e2e): review-army-state-matrix behavior case on the #6792 fixture (field list 2, W3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(review-army): tighten the State Machines dispatch line so /ship stays under its parity size ratio with main's #3231 review-army text

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(approval-classes): only top-level approval lines count; quoted, fenced, indented-code and commented text never does (review #3235 r2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(approval-classes): approval evidence is the first non-blank line only, at column 0 (review #3235 r3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(autoplan-budget): bind the reservation expectation to lib/autoplan-budget's default estimate (test-of-test ratchet)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: keep gstack-upgrade under its size budget, exempt state-matrix's fetch hint, fit the periodic plan's 37 slices

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(evals): the docs observer accepts the spawned-prefixed gstack-skill-start --help read

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(windows): curate capy-review-voice out of the Windows subset (POSIX launcher contract)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: changelog entry for field list 2 wave A

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: stamp v1.91.115.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: the scope-signal clarity check follows helper-printed signals; the hermetic update-check fixture writes the identity cache

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* refactor: move the release-path result codes to lib/result-codes-release.ts (module-size ratchet); fit the periodic plan's 38 slices

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: changelog entry for field list 2 wave B

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: stamp v1.91.116.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: copy lib/result-codes-release.ts wherever a fixture copies lib/result-codes.ts; the scope-signal check requires the helper's computed emission

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: state-matrix stats test resolves its slug under its own state root (home containment)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(windows): curate pregate-docs-lane-review out of the Windows subset (runBin shebang, like the other pregate fixtures)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(windows): curate pregate-remote-gbrain out of the Windows subset (runBin shebang, like pregate-remote)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Nick <surlymo@apache.org>
2026-10-17 03:32:16 +00:00

523 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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; 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/<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) |
| `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). README, CHANGELOG, CONTRIBUTING, LICENSE, AGENTS and CLAUDE qualify even without `.md`. 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 |
| `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)) |
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]
[--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--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`.
- `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: <file>:<line> <pattern>` and a
`TIMING_ASSERTIONS: found=<n> escaped=<n> files=<n>` summary. It warns
(`PREGATE_TIMING_ASSERTION`); it does not block. See [timing](#timing).
- `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 source file with no lane is `FAIL lanes — <file> has no test lane`,
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.
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 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 base-branch `remote_gate` 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`) 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="supervision"></a>
## 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: <n>/<total> <file> start
PREGATE_TEST: <n>/<total> <file> pass|fail <seconds>
PREGATE_HEARTBEAT: <label> running <s>s; last file <file>; log <bytes> bytes, last growth <s>s ago (<log>)
PREGATE: tests <verdict> in <s>s — lanes: <detail>
```
Per-file lines come from a progress adapter where the runner's reporter
provides them: `bun test` (its `<file>:` headers) is the only adapter today;
a one-file run (each serial file) reports its file from the launch for any
runner. Other runners (node --test, vitest, jest, pytest) get heartbeats
only. A heartbeat follows each 60 s with no progress line;
- is killed as **stalled** when its log does not grow for the stall timeout
(`--stall-timeout <s>`, else `stall_timeout_s`, else 300; `0` disables): the
check is `incomplete` with `PREGATE_STALLED` and names the last file
started; the next serial file still runs;
- is killed as **timed out** when that runner process reaches `--timeout`
(per runner process, so many serial files that each fit still pass); the
check is `incomplete` and the next serial file still runs.
A kill is SIGTERM to the group, then SIGKILL after 5 s (a group that ignores
SIGTERM still dies). SIGINT or SIGTERM to `gstack-pregate` itself SIGKILLs every
running group, writes no `pregate.json` and exits 130 or 143. Windows has no
process groups here: the kill is `taskkill /T /F` on the shell's tree, which
misses a descendant that was reparented or started detached.
<a id="remote-gate"></a>
## Small machines and the remote gate
Under 8 cores (`os.availableParallelism()`), when the base branch's
`.gstack/pregate.json` declares `remote_gate` or `gstack-config get
test_backend` is not `local`, the lanes check runs nothing locally: it records
the selected local files as a `requires-remote` obligation with the command to
run, `pregate.json/remote_gate` (the declared command) or
`test_backend/<backend>` (gstack's `scripts/ubicloud/ubi-runner.sh run --src
<repo> -- '<runner> <files>'`). Without either, the tier runs locally under
the supervision above. Close the obligation one of two ways:
- after running it, `gstack-pregate clear --remote pregate.json/remote_gate
--receipt <file>` with the gate's output, validated as `--remote` validates it
(a run URL is refused, `PREGATE_RECEIPT_INVALID` for a bad receipt), or
`gstack-pregate clear --remote test_backend/<backend> --run-url <url>`, which
has no machine-checkable receipt and is recorded as `attested:<url>`; or
- `gstack-pregate --stage tests --remote --spend-cap <usd> --allow-repo-commands`,
which runs the declared `remote_gate` now (never under `--policy-from head`;
a `test_backend` without `remote_gate` cannot be run this way). The gate runs
in the repository root with `PREGATE_TREE` (the working-tree fingerprint
being tested), `PREGATE_FILES_FILE` (the selected files, one per line) and
`PREGATE_SPEND_CAP_USD`, and must print
`PREGATE_REMOTE: tree=<tree> tests=<n> failed=<n> [spent=<usd>]`, reporting the
tree it actually tested. The obligation clears only when that tree is
`PREGATE_TREE`, `tests` > 0, `failed` = 0 and the gate exits 0; anything else
leaves it uncleared and the check `incomplete`. The cap defaults to
`$GSTACK_SPEND_CAP_USD`; there is no default spend. Spend prints as
`spent, reserved, unknown`: a gate that prints no `spent=` is `unknown`,
counted at the cap.
<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="timing"></a>
## Timing assertions
`timing` reads only the lines the branch added or changed in test code (test
files and files under `test/`, `tests/`, `spec/` or `__tests__/`; never
fixture trees). It reports three patterns:
| Pattern | Example |
|---|---|
| `wall-time-bound` | `expect(Date.now() - t0).toBeLessThan(400)`, `expect(elapsedMs).toBeLessThanOrEqual(2_000)`, `assert elapsed < 0.5` |
| `cpu-time-bound` | `expect(cpuMs(() => parse(x))).toBeLessThan(150)`, `assert time.process_time() - c0 < 0.2` |
| `sleep-then-assert` | `await Bun.sleep(50); expect(calls).toBe(1)` (a fixed sleep, not a polling loop) |
A comparison with `0`, a named bound (`timeoutMs`, `TTL_MS`), a timestamp
ordered against a bare `Date.now()` and a `while (… Date.now() < deadline)`
poll are not reported. Changing the bound of an existing assertion is a changed
line, so it is reported.
**Escape.** When the bound is the contract (a hang check with wide headroom, a
CPU-time bound on a pure function), put `gstack-timing-ok: <reason>` in a
comment, in any comment syntax (`//`, `#`, `/* */`, `--`), on the line or
the line above. The reason is required; an empty one is still reported with
`(gstack-timing-ok needs a reason)`. `--explain` lists every escape and its
reason.
**Warn, not block (measured 2026-10-16).** The detector was run over:
- gstack's own test tree: 263 hits in 123 of 1,566 test files (176
`wall-time-bound`, 87 `sleep-then-assert`), mostly real timing assertions,
most of them generous "does not hang" bounds.
- gstack's last 51 commits on main: 9 of the 47 that touched tests would have
been flagged (29 hits; #3190 alone has 7).
- gbrain's last 100 commits on master: 37 of the 95 that touched tests would
have been flagged (147 hits).
- The five gbrain timing-flake fixes (#6050, #6493, #6726, #6746, #6862): 31 of
38 fix sites were flagged before the fix. The misses are flakes with no
timing comparison on the line (an injected deadline in #6050, a
`Promise.race` with a sleep and a deadline parameter in #6862) and ratio or
percentile checks computed far from the clock read. The correct fix in
#6726 (wall clock to thread CPU time) is still a bound and is reported 21
times.
Blocking would stop about a fifth of gstack's and two fifths of gbrain's
test-touching commits, including correct fixes, so the check warns. A repo
that wants it stricter keeps the warning in its review rule (the /review
checklist has the same rule) and /investigate's flake playbook says what to do
with a timing flake: never raise the bound.
<a id="sealed"></a>
## 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 <a..b>] [--remote origin] [--base <branch>] [--json]
```
- **Boundary.** Every commit the push publishes: `git rev-list <local sha>
--not --remotes=<remote>` (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/<user>/`, `/Users/<user>/`, `C:\Users\<user>\`) 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` (`rule<TAB>path<TAB>sha<TAB>reason`) 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.
<a id="regate"></a>
## `gstack-pregate regate`: does the gate still cover HEAD
```
gstack-pregate regate --against <gated-head> --bundle <evidence-bundle.json> [--require <lane,...>]
[--pr <n>] [--from <gate-ahead.json>] [--predecessor-pr <n> | --predecessor-ref <ref>]
[--base <branch>] [--json]
gstack-pregate --against <gated-head> ... 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=<n> [changed=<n>]
LANE: <label> reusable|rerun|not-reusable (<reason>) one per bundle lane and per --require lane
MIGRATION_COLLISION: none (...) | unknown (...) | <id> <file> vs <origin/main|PR #n> <file>
REGATE: not-needed | needed (<every reason>)
```
`REGATE: not-needed` (exit 0) only when the tree is stamp-only, every lane in
the bundle and in `--require` is reusable (a stamp-sensitive lane reruns, so it
needs a regate), the predecessor head is known and unmoved, and the migration
lookup finished with no collision. Anything else is `needed` (exit 1); no
bundle prints `REGATE: needed (no gate evidence for <head> ...)`.
Migrations come from the ship policy's `migrations` (`dir`, `id` regex with
one capture group). The numbers this branch adds under `dir` are compared with
the base branch's tree and with every other open PR
(`gh pr list --state open --limit 1000 --json number,headRefOid,files`; the PR
itself is skipped by head sha or `--pr`). A PR listing 100 files is truncated
by GitHub, so its full list is fetched with `gh api --paginate`. A failed
query, 1000 listed PRs or an unfetchable file list is
`MIGRATION_COLLISION: unknown (...)`, which blocks `not-needed`
([`MIGRATION_COLLISION`](troubleshooting.md#migration-collision)).
<a id="gbrain-worked-configs"></a>
## gbrain worked configs
gbrain commits these on master after the gstack that reads the new keys ships
(older gstack refuses unknown keys). Its `.gitignore` ignores `.gstack/*`
today, so the change also adds `!.gstack/pregate.json` and
`!.gstack/ship-policy.json`; both changes are gbrain's.
`.gstack/pregate.json`: guards for the gates the import graph cannot see.
```json
{
"dependencies": {
"test/**/*.serial.test.ts": ["test/scripts/serial-files.test.ts"],
"scripts/serial-files.tsv": ["test/scripts/serial-files.test.ts"],
"src/cli.ts": ["test/cli-goldens.test.ts"],
"src/commands/**": ["test/cli-goldens.test.ts"],
"src/core/operations*.ts": ["test/cli-goldens.test.ts"]
},
"guards": [
{ "id": "serial-manifest", "when": ["test/**/*.serial.test.ts", "scripts/serial-files.tsv", "scripts/run-serial-tests.sh"],
"command": "bun test test/scripts/serial-files.test.ts",
"fix": "add the serial file's row to scripts/serial-files.tsv" },
{ "id": "cli-goldens", "when": ["src/cli.ts", "src/cli/**", "src/commands/**", "src/core/operations*.ts", "src/core/ops/**"],
"command": "bun test --timeout=60000 test/cli-goldens.test.ts test/cli-dispatch-phase.test.ts test/export-surface-golden.test.ts",
"fix": "bun run regen:all, then justify each changed golden in the PR body" },
{ "id": "regen-check", "when": ["src/**", "skills/**", "templates/**", "plugin/**"],
"command": "bun scripts/regen-all.ts --check",
"fix": "bun run regen:all and commit the regenerated files" },
{ "id": "migration-order", "when": ["src/core/schema-migrations/**"],
"command": "bun scripts/check-schema-migration-order.ts",
"fix": "renumber the migration to the next free number on master" }
]
}
```
`.gstack/ship-policy.json`: the stamp rule and migrations. On a pure restamp
of a gbrain master tree from 0.60.188.0 to 0.60.189.0 the tree receipt
classifies all 16 changed files as stamps under this policy; an added line in
`src/core/behavior-change-notice.ts` stays a change.
```json
{
"bump": "patch",
"queue_mode": "stamp-at-merge",
"changelog": "CHANGELOG.md",
"stamp_files": [
"VERSION", "package.json", "CHANGELOG.md", "openclaw.plugin.json",
".claude-plugin/plugin.json", ".codex-plugin/plugin.json",
"plugin-variants/*/.claude-plugin/plugin.json", "plugin-variants/*/.codex-plugin/plugin.json",
"plugin-variants/*/README.md", "plugin/README.md",
"BOOTSTRAP_FOR_AGENTS.md", "templates/bootstrap/template-repo/README.md",
"src/core/behavior-change-notice.ts"
],
"stamp_patterns": [
"\"version\": \"{version}\"",
"since: '{version}'",
"<!-- gbrain-plugin-tree-stamp: {version} -->",
"<!-- gbrain-template-stamp: {version} -->",
"<!-- gbrain-runbook-stamp: {version} -->"
],
"migrations": { "dir": "src/core/schema-migrations", "id": "^v(\\d+)-" }
}
```
gbrain's `bun run release:restamp` fetches master, picks its own version,
refuses a dirty tree and commits, so it cannot be `restamp_command` as it
stands; the stamp rule above lets gstack's own restamp writer do the work.
A gbrain mode that takes the version and writes only stamps would make it one.
gbrain-evals `.gstack/pregate.json`: the sealed paths. The globs below are
placeholders in the shape the guard reads; gbrain-evals fills in its own
held-out and transcript layout.
```json
{ "sealed": {
"paths": ["heldout/**"],
"transcripts": ["runs/**/transcript*.jsonl"],
"answer_markers": ["SEALED ANSWER:"],
"seed_patterns": ["shuffle_seed=\\d+"],
"local_paths": true } }
```
<a id="issue-links"></a>
## `gstack-issue-links`
```
gstack-issue-links [--base <ref>] [--issue <n>]... [--ci-run <id>]... [--supersedes <pr>]... [--credit <pr>]...
[--no-auto-links] [--author <login>] [--repo <owner/repo>] [--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 subject's `(#N)` included), a partial fix, a deferred ask and every
cross-repo `owner/repo#N` are `Refs`. Same-repo references are printed as
promotion candidates (`reference #N ← <commit line> (promote: fix #N)` on
stderr, `candidates` in `--json`): the owner's `fix #N` reply reruns with
`--issue N`; an unattended run keeps them `Refs` and lists them in the
handoff. 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>)` only for a bot-opened issue of this repository
that names that run (its `flake:json` `first_seen.run_id`, or this repository's
`actions/runs/<id>` URL) and a test (`flake:json` `file › test`, or a
`Flake: <test>` title). Two issues for the same test keep the lower number;
every refused match is printed with its reason.
Open PRs by other authors (not this branch, and not this PR's author: `--author`,
else the `gh` viewer; when the viewer is an app such as `capy-ai[bot]`, the HEAD
commit's GitHub user, else the owner of a user-owned repository, else the gap is
`ISSUE_LOOKUP_FAILED`) whose closing
set overlaps this PR's `Fixes` are `Related: #N (@author)`, read page by page
from each issue's closing-PR references, in unattended runs too. `Supersedes #N`
replaces that line and `Contributed by @author` is added (the credit lines of
`lib/contributor-mode.ts`) only with `--supersedes N` / `--credit N`, after the
owner confirms.
`--no-auto-links` skips the lookup. A failed lookup prints `ISSUE_LOOKUP_FAILED`
and keeps the other lines. The stderr preview names the source of each line.
The printed lines go under `## Issues` in the PR body; `--out` writes the
object (`fixes`, `refs`, `flakes`, `related`) `gstack-ship-receipt write
--issues @file` embeds. With `--body`, that write refuses a body missing any of
those lines (`ISSUE_LINKS_MISSING`).