mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
v1.91.77.0 feat(ship): two-stage gstack-pregate, gstack-regen generated-file registry, intent-based issue links (multi-agent wave C-pregate) (#3121)
* v1.91.77.0 feat(ship): two-stage gstack-pregate, gstack-regen registry, intent-based issue links (multi-agent wave C-pregate) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(pregate): keep the bin-driving pregate tests off the Windows subset; pin files are free-only for eval selection The three C-pregate tests that spawn bin/gstack-regen, bin/gstack-pregate, bin/gstack-issue-links and bin/gstack-ship-receipt do so through test/helpers/pregate-fixture.ts runBin, so the Windows curation's source scan of the test file never saw a bin/ spawn and shipped them to windows-latest, where CreateProcess cannot exec a shebang (14 failures in windows-free-shard 2 and 5). Each is now a KNOWN_WINDOWS_INCOMPATIBLE entry with the precise reason. .gstack/generated.json and .gstack/pregate.json join FREE_ONLY_PR_FILES: no paid case reads gstack's own pin files (the tools read origin/<base> of the repo they run in, and paid cases run in fixture repos), so touching them no longer restores every gate case. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(harness-version): record the FREE_ONLY_PR_FILES pin-file entries as non-behavioral scripts/test-pr-profile.ts is a pinned shared-harness file; the two .gstack pin-file entries change selection only, never a case verdict. 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>
This commit is contained in:
1 parent
4e5bf1e454
commit
8584d6ff49
47 files changed
+3394
-21
No files matched your search
+140
@@ -0,0 +1,140 @@
|
||||
# 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 |
|
||||
|
||||
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`.
|
||||
|
||||
**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).
|
||||
|
||||
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.
|
||||
|
||||
Tier 2 checks (`guards`, `ratchets`, `lint`, `links`, `hermetic`, `patches`,
|
||||
`integrator`) are named in `--only`'s help and land in a follow-up pre-gate PR.
|
||||
|
||||
<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.
|
||||
@@ -1934,3 +1934,140 @@ runner, this is missing coverage.
|
||||
(unattended machines); or set the API key and pass `--runner api`; or run the
|
||||
review on a cross-family subagent and submit its file with `--runner
|
||||
host-subagent --result <file> --model <id>`.
|
||||
|
||||
## Pre-gate: generated-file registry, gstack-pregate, issue links
|
||||
|
||||
Both tools read their pin file (`.gstack/generated.json`, `.gstack/pregate.json`)
|
||||
from `origin/<base>`, never the working tree, and run repo-declared commands only
|
||||
under `--allow-repo-commands` or inside `/ship`. Details: docs/pregate.md.
|
||||
|
||||
<a id="regen-registry-invalid"></a>
|
||||
### `REGEN_REGISTRY_INVALID`
|
||||
|
||||
**Meaning.** `.gstack/generated.json` is not valid JSON, has an unknown key, a
|
||||
missing `id`/`inputs`/`outputs`, or a duplicate id.
|
||||
|
||||
**Fix.** `gstack-regen validate` prints every error with its JSON path; compare
|
||||
with `gstack-regen init --explain` and fix the file on the base branch.
|
||||
|
||||
<a id="regen-containment"></a>
|
||||
### `REGEN_CONTAINMENT`
|
||||
|
||||
**Meaning.** A registry glob escapes the repository (`..`, absolute, more than 64
|
||||
per list) or a command is not one line under 512 bytes.
|
||||
|
||||
**Fix.** Repo-relative globs only; one line per command.
|
||||
|
||||
<a id="regen-stale"></a>
|
||||
### `REGEN_STALE`
|
||||
|
||||
**Meaning.** A registered output differs from what its command produces in a
|
||||
scratch `git worktree` of the current tree: the output is stale or hand-edited.
|
||||
|
||||
**Fix.** Run the printed command (`gstack-regen write --only <id>` does it in
|
||||
place) and commit the outputs. Never hand-merge a generated file: on a conflict
|
||||
take either side, then regenerate.
|
||||
|
||||
<a id="regen-command-failed"></a>
|
||||
### `REGEN_COMMAND_FAILED`
|
||||
|
||||
**Meaning.** A registered command exited non-zero in the scratch worktree.
|
||||
|
||||
**Fix.** Run it by hand in the repository and read its output; the scratch
|
||||
worktree has no `node_modules` of its own, so a command that needs them must
|
||||
reference the repository's (gstack's entries do through `bun run`).
|
||||
|
||||
<a id="pregate-config-invalid"></a>
|
||||
### `PREGATE_CONFIG_INVALID`
|
||||
|
||||
**Meaning.** `.gstack/pregate.json` has an unknown key, a wrong type, a glob
|
||||
that escapes the repository, a runner without `{files}`, or `secrets` in `warn`.
|
||||
|
||||
**Fix.** `gstack-pregate validate`. `secrets` and required-lane obligations
|
||||
cannot be downgraded; every other check id in `warn` is honoured only from the
|
||||
base branch's copy.
|
||||
|
||||
<a id="pregate-secrets"></a>
|
||||
### `PREGATE_SECRETS`
|
||||
|
||||
**Meaning.** `gstack-redact` found a HIGH finding, or a MEDIUM finding in the
|
||||
`secret` category, in the lines this branch adds.
|
||||
|
||||
**Fix.** Rotate the credential, remove it from the added lines (history
|
||||
included), rerun. Not downgradable.
|
||||
|
||||
<a id="pregate-strays"></a>
|
||||
### `PREGATE_STRAYS`
|
||||
|
||||
**Meaning.** A file was added at the repository root, in a new top-level
|
||||
directory, or under a scratch name (`zz-*`, `tmp*`, `scratch*`).
|
||||
|
||||
**Fix.** Delete or move it. An intentional root file goes in `strays.allow`.
|
||||
|
||||
<a id="pregate-literals"></a>
|
||||
### `PREGATE_LITERALS`
|
||||
|
||||
**Meaning.** A changed exported constant (or a named timeout/retry literal) still
|
||||
appears with its old value in a test file the branch did not update — nightly-
|
||||
and Heavy-only files included.
|
||||
|
||||
**Fix.** Update every listed `file:line`, or keep the old value.
|
||||
|
||||
<a id="pregate-no-lane"></a>
|
||||
### `PREGATE_NO_LANE`
|
||||
|
||||
**Meaning.** A touched file maps to no test through static imports, repo-path
|
||||
string literals, the `bin/<name>` → `test/<name>*.test.ts` convention or a
|
||||
`dependencies` declaration. That is a failure, not a pass.
|
||||
|
||||
**Fix.** Add a test that names the file, or declare its lane (an explicit `[]`
|
||||
for files with no behaviour, such as docs) in `.gstack/pregate.json`. `--explain`
|
||||
prints every mapping and why.
|
||||
|
||||
<a id="pregate-lane-failed"></a>
|
||||
### `PREGATE_LANE_FAILED`
|
||||
|
||||
**Meaning.** A local lane the touched files select exited non-zero under
|
||||
`set -o pipefail`, or reported zero tests (ZERO-RUN).
|
||||
|
||||
**Fix.** Read the printed lane log, fix the failure, rerun
|
||||
`gstack-pregate --stage tests`. A `| tail`-hidden exit is still an exit.
|
||||
|
||||
<a id="pregate-incomplete"></a>
|
||||
### `PREGATE_INCOMPLETE`
|
||||
|
||||
**Meaning.** A check timed out, its tool is unavailable, repo commands were not
|
||||
allowed, or the working tree changed while the check ran. Incomplete is never
|
||||
pass and blocks publication.
|
||||
|
||||
**Fix.** Pass `--allow-repo-commands`, install the tool, raise `--timeout`, or
|
||||
keep the tree still, then rerun.
|
||||
|
||||
<a id="pregate-stale"></a>
|
||||
### `PREGATE_STALE`
|
||||
|
||||
**Meaning.** `pregate.json` was recorded against a different working-tree
|
||||
fingerprint than the one being published (the final stamp, a regen, any edit).
|
||||
|
||||
**Fix.** Rerun `gstack-pregate` after the final generation and stamp; Step 18
|
||||
refuses to write the ship receipt otherwise.
|
||||
|
||||
<a id="pregate-remote-uncleared"></a>
|
||||
### `PREGATE_REMOTE_UNCLEARED`
|
||||
|
||||
**Meaning.** A `requires-remote` obligation (a platform lane this machine cannot
|
||||
run, named by `workflow.yml/job`) has no receipt.
|
||||
|
||||
**Fix.** Dispatch that job, then
|
||||
`gstack-pregate clear --remote <workflow.yml/job> --run-url <url>` (or
|
||||
`--bundle-lane <label>` from an evidence bundle). The ship receipt carries each
|
||||
obligation with its receipt.
|
||||
|
||||
<a id="issue-unreadable"></a>
|
||||
### `ISSUE_UNREADABLE`
|
||||
|
||||
**Meaning.** An issue named by a `Fixes #N`/`Closes #N` trailer or `--issue N`
|
||||
could not be read through `gstack-issue-guard`.
|
||||
|
||||
**Fix.** Check `gh auth status` and the number. Until it reads, the link is
|
||||
rendered as `Refs #N`, never `Fixes`.
|
||||
Reference in new issue
Block a user