* 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>
7.6 KiB
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.
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.
.gstack/generated.json (generated-v1)
{ "$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 checkregenerates every entry (or--only <id>, or--touched <paths>for entries whose inputs or outputs intersect) in a scratchgit worktreeseeded from the working tree, withnode_moduleslinked rather than copied, and reports stale outputs (REGEN_STALE, exit 1) without touching the tree. An entry with nocommandis tracked only (goldens regenerated by another entry).gstack-regen writeregenerates in place: whatgstack-restampand the Step 3 conflict rule use. On a conflict in a registered output, take either side, thengstack-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 |
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 checkin a scratch worktree).secrets:gstack-redactover the branch's added lines (merge-base to the working tree, untracked files included). HIGH, or MEDIUM in thesecretcategory, 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 withfile:line.
Stage 2, test execution:
lanes: touched files map to tests throughlib/pregate/import-graph.ts(static imports ∪ repo-path string literals ∪ thebin/<name>→test/<name>*.test.tsconvention ∪dependencies), transitively through non-test files; deleted and renamed files select the tests that named them. A touched file with no lane isFAIL lanes — <file> has no test lane, never a pass. Lanes and platform tiers come from.github/workflows/*.yml(gstack-pregate lanesprints them): local ones run through the runner underset -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 arequires-remoteobligation namingworkflow.yml/job.--explainprints each test's provenance and every uncertain mapping (a planner-computed matrix without aselectionmanifest 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.
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.