mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 22:45:02 +01:00
* docs(designs): gstack field wave plan (2026-10-12) and the shared simulated Capy host fixture
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(pregate): price_command config key for the non-executing call plan a paid gate is priced from
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(pregate): repo guards a lane must pass, verified launch Bun and per-file serial runs in lanes, unknown flags exit 2
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>
* fix(pricing): Sonnet 5.5, Gemini 3.x and gbrain embedding rows from fetched pages; Opus 4.7 at its current list price
Each new row carries its source URL and fetch date (2026-10-12). Embedding
models bill input only, so they live in EMBEDDING_PRICING beside PRICING
(eval arms never resolve to them); pricingRow also accepts gbrain's
provider:model ids (voyage:voyage-4). claude-opus-4-7 moves from 15/75 to the
5/25 the Anthropic pricing page lists; benchmark-runner's expected costs follow.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(eval-plan): price-gate prices a paid gate from a call-plan data file, never executing it
gstack-eval-plan price-gate --gate "<exact command>" reads a GSTACK_EVAL_CALLS
v1 file (--plan-file, else the base policy's price_file at HEAD), checks its
binding (gate command string equality, head with only the plan file changed
since, every input's sha256 at HEAD), prices each call purpose (aggregate
totals) and each one-call reservation, and writes a receipt binding the price
to the gate, head, inputs and plan bytes. Unpriced models print unknown, never
zero (EVAL_PRICE_MISSING); a missing plan or no call lines is
EVAL_GATE_UNPRICED unless --allow-unpriced records an owner override; over
--cap-usd / --smoke-cap-usd is EVAL_GATE_OVER_CAP; malformed or partial files
refuse EVAL_DRY_RUN_FAILED and stale ones EVAL_PLAN_STALE. dry-run --cap adds
the same reservation-fits-cap check. Wire format, receipt and the reference
emitter (formatEvalCalls): docs/eval-calls.md.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(eval-plan): the approval step prices every paid gate with price-gate
Step 4 runs gstack-eval-plan price-gate for each paid gate the plan names and
puts its GATE_PRICE line in the ask; the dry-run checklist documents
reservation-fits-cap under --cap.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(sealed): sealed-output guard over every published commit, in the pre-push hook and gstack-pregate sealed
A repo with a sealed key in .gstack/pregate.json (base branch union the
pushed commit, so a push can tighten but never relax) refuses publishing
sealed paths, transcripts, quoted answers, seeds or local home paths to a
public or unknown destination (SEALED_OUTPUT_REFUSED); private warns.
Path rules see every added, renamed or copied path in each published
commit (git rev-list <local> --not --remotes=<remote>), text rules each
commit's added lines; waivers only from the base branch allow file; a
malformed policy fails closed; visibility caches public 24 h, private 1 h.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(eval-plan): only an undeclared plan is unpriced; a missing price_file is stale, a bad base policy refuses
price-gate's EVAL_GATE_UNPRICED (and --allow-unpriced) now covers only no
plan declared at all or a plan with zero call lines. A declared price_file
missing from the HEAD commit is EVAL_PLAN_STALE (exit 1, not overridable); an
unresolvable origin/<base> is POLICY_SOURCE_UNAVAILABLE and an invalid
.gstack/pregate.json is PREGATE_CONFIG_INVALID (both exit 3).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(restamp): the repo's stamp rule (stamp_files, literal stamp_patterns) and restamp_command in the staging worktree
stamp_files is an allowlist applied before every stamp rule; stamp_patterns
are bounded literal {version} templates shared by the restamp writer and the
tree receipt. restamp_command runs first in the staging worktree with the
target version (argument and GSTACK_RESTAMP_VERSION), needs a clean caller
tree, skips gstack's version writer and keeps output confinement; a failure
or a wrong version writes nothing. migrations { dir, id } is the policy key
gstack-pregate regate reads.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(pregate): gstack-pregate regate --against <gated-head> with per-lane reuse and migration collisions
Reuse comes only from a tree receipt evaluated now, with a bundle produced
on the gated tree, the current execution identity and an unmoved, known
predecessor head. Each lane prints reusable/rerun/not-reusable; REGATE:
not-needed needs stamp-only, every bundle and --require lane reusable (a
stamp-sensitive lane reruns) and a finished migration lookup. Migration
numbers the branch adds are checked against the base tree and every other
open PR (gh pr list --limit 1000; 100-file lists refetched with gh api
--paginate); any lookup that cannot finish is unknown and blocks
not-needed (MIGRATION_COLLISION). Bare --against routes to the verb.
docs/pregate.md gains the regate section and the gbrain worked configs.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(contract): lanes run gstack-pregate (and the sealed guard where configured) before done; handoff carries the gstack trailer and PREGATE: line
The coordinator contract gains a 'Before done' paragraph and the handoff
order GSTACK_RESULT, gstack: ok|fail <rev>, PREGATE:, gate summary; the
spec section is regenerated and docs/unattended.md states the same rule.
A new test pins the terms and their order; the byte-identity pins stay.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* v1.91.98.0: pregate guards, priced paid gates, regate and the sealed-output guard
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: drop the helper-only Capy host fixture test (test-of-test ratchet); lane tests exercise the helper
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: price-gate --gate-file keeps free text out of the /eval-plan fence; shortest dependency traces; bounded test spawns
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci(windows): curate out the new pregate/restamp tests that spawn bin/ shebang scripts through runBin
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>
353 lines
18 KiB
Markdown
353 lines
18 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.*`) |
|
||
| `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)) |
|
||
|
||
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="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>]... [--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.
|