pregate: --policy-from head only adds lanes; timeout_s policy default; preflight forwards --policy-from (GST-419, GST-420)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-31 08:50:17 +00:00
1 parent 047dad78ba
commit ff71dec209
17 files changed
+423 -59

No files matched your search

+23 -10
View File
@@ -25,9 +25,20 @@ 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
says `policy head (unreviewed)`. Under it the branch's `pregate.json` may only
add test lanes over the trusted policy (the base copy, or the user-owned file
when the base has none): a new `dependencies` glob with at least one test, or
more tests on a glob the trusted policy already maps. A removed or shrunk glob,
a new `[]` declaration, or any other key that differs (runner, guards, warn,
allowlist, timeouts, the remote gate, ...) refuses the run with
[`PREGATE_HEAD_POLICY_LOOSENS`](troubleshooting.md#pregate-head-policy-loosens).
A branch behind its base is measured from the copy at its merge base and runs
the base tip's policy plus its additions. With no trusted policy at all, the
head file is a bootstrap and runs as written. `.gstack/generated.json` is still
read from the branch under `--policy-from head`. So an allowlist entry or a
`[]` declaration a branch adds counts only once it is on the base, and a lane it
adds counts on the base policy only once it lands; `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.
@@ -75,6 +86,7 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`).
| `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)) |
| `timeout_s` | seconds each runner process may take (the unit-file group, each serial file, each guard without its own `timeout_s`, and the remote gate's deadline) when `--timeout` is not passed; 1 to 3600, default 600. `gstack-ship-preflight` and the `finish` fallback run pregate without `--timeout`, so this is how a repo with long lanes sets theirs |
| `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)) |
@@ -86,7 +98,7 @@ 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)); `<slug>` 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
(under `--policy-from head` it is the trusted policy the head file is compared with), it lives outside the repository so a PR still
cannot reach it, and the record names it (`policy user <path>`) 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
@@ -207,7 +219,8 @@ Two stages with separate clocks, both printed:
`*_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
`"**/test_driver/**": []`) under `dependencies` on the base branch and let CI run them
(a `[]` only on the branch is refused under `--policy-from head`); 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 `<crate>/tests/`, holds a `#[cfg(test)]` (any cfg predicate naming
@@ -429,7 +442,7 @@ the supervision above. Close the obligation one of two ways:
`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`;
which runs the declared `remote_gate` now (under `--policy-from head`, still the trusted copy's gate;
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
@@ -548,10 +561,10 @@ are named and left to the paid PR gate; a selection of only paid files reports
`tests=0`, which never clears the obligation. The command is the same either way
(`--remote` always needs a cap; the keyless path spends nothing).
pregate kills the gate at its `--timeout` (default 600 s) with a 5 s grace, so
the script finishes before that deadline: it reads the deadline from
`PREGATE_TIMEOUT_S`, else from the `gstack-pregate` command it runs under (its
`--timeout`, else 600), reserves 120 s for teardown plus 15 s margin, and tells
pregate kills the gate at its `--timeout` (default the policy's `timeout_s`, else
600 s) with a 5 s grace, so the script finishes before that deadline: it reads the
deadline from `PREGATE_TIMEOUT_S`, which pregate sets to that same timeout, else from
the `gstack-pregate` command it runs under (its `--timeout`, else 600), reserves 120 s for teardown plus 15 s margin, and tells
the VM to stop at whichever comes first, that point or the cap's wall (at most
45 minutes, minus 3 for teardown). A deadline that leaves the VM under 300 s
refuses before any VM (`PREGATE_REMOTE_TIMEOUT_TOO_SHORT`). At create time it
+30 -7
View File
@@ -3361,7 +3361,10 @@ for files with no behaviour) under `dependencies` in `.gstack/pregate.json` (or
Other non-Markdown documentation goes in `empty_lanes` on the base branch.
Dart files under `integration_test/` or `test_driver/` need a device, so pregate
never runs them: declare `"**/integration_test/**": []` (and
`"**/test_driver/**": []`) under `dependencies` and let CI run them.
`"**/test_driver/**": []`) under `dependencies` on the base branch and let CI run
them. A `[]` declaration added only on the branch is refused under
`--policy-from head` ([`PREGATE_HEAD_POLICY_LOOSENS`](#pregate-head-policy-loosens)),
so land it in its own PR first.
With no policy anywhere (no base copy, no user-owned file) an unmapped file is
the warning [`PREGATE_NO_POLICY`](#pregate-no-policy) with a starter policy
printed, not this error; `gstack-pregate init` writes that draft and lists the
@@ -3385,13 +3388,33 @@ two routes:
1. Land the policy change in its own PR first (the reviewed route), then merge
the base into this branch and rerun.
2. Rerun with `--policy-from head --allow-repo-commands`. The run reads the
branch's copy and its record says `policy head (unreviewed)`, so a
reviewer sees that the gate ran under a policy nobody has reviewed yet;
`gstack-ship-queue` never accepts a head-policy run as a remote mirror.
2. Rerun with `--policy-from head --allow-repo-commands` (in /ship,
`gstack-ship-preflight --tests-only --policy-from head`). The run uses the
trusted policy plus the lanes the branch adds, and its record says
`policy head (unreviewed)`, so a reviewer sees that the gate ran under a
policy nobody has reviewed yet; `gstack-ship-queue` never accepts a
head-policy run as a remote mirror. A head policy that does anything but add
test lanes is refused ([`PREGATE_HEAD_POLICY_LOOSENS`](#pregate-head-policy-loosens)).
An allowlist entry the branch adds to its own `pregate.json` is read the same
way: it clears a finding only once it is on the base.
An allowlist entry or a `[]` declaration the branch adds to its own
`pregate.json` counts only once it is on the base.
<a id="pregate-head-policy-loosens"></a>
### `PREGATE_HEAD_POLICY_LOOSENS`
**Meaning.** Under `--policy-from head`, the branch's `.gstack/pregate.json`
does more than add test lanes over the trusted policy (the base branch's copy,
or the user-owned file when the base has none). The message lists each
difference: a `dependencies` glob removed, a test dropped from a glob, a new
`[]` declaration, a deleted file, or another key (`runner`, `guards`, `warn`,
`allowlist`, `timeout_s`, `remote_gate`, ...) that differs. A branch behind
its base is compared with the copy at its merge base, so a lane the base added
later is not a difference. Nothing ran and no record was written.
**Fix.** Keep every key of the trusted policy as it is and only add
`dependencies` globs with at least one test, or tests on an existing glob. Land
any other change, a `[]` declaration included, in its own PR on the base branch
first, then merge the base into this branch and rerun.
<a id="pregate-lane-failed"></a>
### `PREGATE_LANE_FAILED`