Files
gstack/docs/pregate.md
T
Garry Tanandcapy-ai[bot] 0bf9d50808 v1.91.107.0 fix: the GBRA-40 field list (one Bun for install and check, rec gate approval, pregate never silent) (#3190)
* fix(autoplan): empty findings fences, repo-relative rows, PLAN_REALITY flags, pending/--all/provider (Lane B: B3 B6 B7 B8 B9 B10 B11)

B3  parseFences: the body group is optional (lazy), so an LF/CRLF empty fence is zero
    findings in both the canonical and record grammars; opener without closer still
    RESULT_FINDINGS_MISSING.
B6  plan-reality/3: item/section references are not quantities; backtick phrases are
    exempt only from the plan-scope rule-phrase check (B-f); a measured number inside
    backticks stays flagged in review scope. vaguePlanTerms() feeds an advisory
    `flagged: PLAN_REALITY ...` line under the p1 approval item (never blocking).
B7  canonical rows: absolute repo paths become repo-relative; snapshot/active-plan
    citations become the plan file name with the source line mapped (or dropped).
B8  `status` prints `PENDING: <id> phase=<p> voice=<v> result=<file>` per open attempt.
B9  `gstack-gate render --all`; paged output says to relay every page.
B10 `gstack-plan-reality check --plan <snapshot>`: a number in a "quoted" span copied
    verbatim from the reviewed plan needs no receipt (reviewer scope only).
B11 submit defaults the provider to host-subagent when --runner is omitted.

Lane B files: lib/autoplan-reconcile.ts lib/plan-reality.ts lib/autoplan-gate.ts
lib/gate-list.ts lib/autoplan-plan-text.ts lib/autoplan-run.ts bin/gstack-gate
bin/gstack-plan-reality scripts/resolvers/plan-reality.ts autoplan/sections/ceo-phase.md
autoplan/sections/eng-phase.md test/plan-reality.test.ts test/autoplan-gate-artifacts.test.ts

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

* fix(ubicloud): ubi-runner up/run -S accepts only Ubicloud's storage sizes, before any API call (A11)

Files: scripts/ubicloud/ubi-runner.sh, test/ubicloud-runner.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(issue-links): promotion candidates, Related PRs, validated flake issues, body/receipt consistency (Lane D: D1-D3)

D1: the GBRA-40 fix wave's gbrain #6279 body had no `## Issues` section and no
receipt (it was not composed by /ship, so gstack-issue-links never ran), and
#6252 appeared only as a subject parenthetical `(#6252)`, which the closure
rule correctly treats as Refs. The rule is kept. Same-repo references are now
printed as promotion candidates (`(promote: fix #N)`, `candidates` in --json),
and `gstack-ship-receipt write --issues ... --body` refuses a body that lacks a
line its issues object records (ISSUE_LINKS_MISSING), so a /ship body can no
longer drop its closing lines.

D2: open PRs by other authors whose closing set overlaps this PR's Fixes are
`Related: #N (@author)` (paged GraphQL closedByPullRequestsReferences; viewer
and this branch excluded; cross-repo PRs keep owner/repo#N). `Supersedes` and
`Contributed by` only with --supersedes/--credit; --no-auto-links skips the
lookup; the stderr preview names each added line's source. A failed lookup is
ISSUE_LOOKUP_FAILED (installer form with docs anchor) and keeps the other lines.

D3: a --ci-run match becomes a Flake line only for a bot-opened issue of this
repository that names that run (flake:json first_seen.run_id or this repo's
actions/runs URL) and a test; duplicates of one test keep the lowest number;
every refused match prints its reason.

Files: bin/gstack-issue-links, bin/gstack-ship-receipt, lib/issue-links.ts,
lib/ship-receipt.ts, lib/headless-artifacts.ts (ship-receipt issues.related),
lib/result-codes.ts (ISSUE_LOOKUP_FAILED, ISSUE_LINKS_MISSING,
resultLineWithDocs), bin/gstack-doctor-components.sh (regenerated),
docs/pregate.md, docs/troubleshooting.md, test/ship-issue-intent.test.ts,
test/helpers/pregate-fixture.ts (gh shim: repo view, api graphql, full issue rows).

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

* fix(ship): link issues by intent at publish and handoff; Fixes: trailer at commit (Lane D: D1, D2, D-a)

- Step 15: a commit fixing a user-named issue carries a `Fixes: #<n>` trailer
  (offset by tightening Step 15 so the skeleton stays at 80,998 <= 81,000).
- pr-body: an explicit "Issue links" step runs gstack-issue-links before the
  body is composed (previously only a placeholder inside the body template);
  interactive runs ask once about `(promote: fix #N)` references and add
  `--supersedes`/`--credit` only on the owner's confirmation; unattended runs
  keep Refs and list unpromoted candidates, Related lines and lookup failures
  in the completion message; `ISSUE_LINKS_MISSING` from the receipt write.
- handoff (no-PR lanes): runs gstack-issue-links and passes --issues to the
  receipt; the completion message gains an `issues:` line.
- Regenerated ship/SKILL.md and sections; refreshed the four ship goldens.
- Parity: union 309,431 / 187,706 = 1.6485 (> 1.6450 by 655 bytes); maxSizeRatio
  raised to the measured 1.6485 with its justification, as previous waves did.

Files: ship/SKILL.md.tmpl, ship/SKILL.md, ship/sections/pr-body.md.tmpl,
ship/sections/pr-body.md, ship/sections/handoff.md.tmpl, ship/sections/handoff.md,
test/fixtures/golden/{claude,codex,factory,agy}-ship-SKILL.md,
test/helpers/carve-guards.ts (ship maxSizeRatio).

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

* fix(install): one Bun resolution contract, versioned runtime store, verdict per (checkout, project), drift and wrapper rows (A1-A3, A5, A6, A8, A-c, A-d, A-f, A-h, A-i)

- bin/gstack-bun-version.sh: GSTACK_BUN_DEFAULT=1.4.3 beside the 1.4.2 floor,
  gstack_bun_install_version (GSTACK_BUN_INSTALL_VERSION override),
  gstack_bun_store, portable gstack_realpath, and gstack_bun_resolve (pin and
  default in the store, $BUN_INSTALL/bin, ~/.bun/bin, PATH; first candidate
  satisfying every pin; shim-marked files never returned). gstack_bun_launch_dir
  is replaced by it.
- lib/pregate/runtime.ts: resolveBun, the TypeScript twin; launchRuntime runs
  the Bun the install verdict recorded under the admission condition and
  explains the skip otherwise.
- bin/gstack-capy-install: install and --check resolve through the contract;
  verified archives publish atomically to <state root>/runtimes/bun/<v>/bun
  under ~/.gstack-runtime.lock and ~/.bun/bin is never written; the "no Bun
  satisfies" line names the candidates judged; --check reports
  BUN_RESOLUTION_DRIFT with one converging fix.
- bin/gstack-install-verdict.sh, lib/install-verdict.ts: schema 2 verdicts
  keyed by (checkout, project) with pin, bun_path, bun_version, bun_rule;
  schema 1 path still read; admission binds test-running workflows to the
  project and refuses a running Bun the verdict did not record.
- bin/gstack-doctor-check.sh, lib/doctor-components.ts: wrapper core row
  (CAPY_WRAPPER_STALE by active provenance, WARN for stale Drive copies),
  runtime drift row, shim note, install-version note.
- lib/result-codes.ts: BUN_RESOLUTION_DRIFT, BUN_INSTALL_VERSION_INVALID;
  CAPY_WRAPPER_STALE fix names the workaround and the owner action.

Files: bin/gstack-bun-version.sh, bin/gstack-capy-install,
bin/gstack-doctor-check.sh, bin/gstack-doctor-components.sh (generated),
bin/gstack-install-verdict.sh, lib/doctor-components.ts,
lib/install-verdict.ts, lib/pregate/runtime.ts, lib/result-codes.ts,
test/bun-resolve.test.ts, test/gstack-capy-install.test.ts,
test/gstack-doctor.test.ts, test/install-verdict.test.ts,
test/pregate-guards.test.ts

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

* fix(issue-links): --author for the Related lookup when gh runs as an app (Lane D: D2)

On a Capy machine the gh viewer is `capy-ai[bot]`, not the owner (checked live
against garrytan/gbrain #6252), so "other authors" needs the PR author named:
`--author <login>` overrides the viewer; the pr-body Issue links step says to
pass it when gh runs as an app.

Files: bin/gstack-issue-links, docs/pregate.md, ship/sections/pr-body.md.tmpl,
ship/sections/pr-body.md, test/ship-issue-intent.test.ts,
test/helpers/carve-guards.ts (ship maxSizeRatio re-measured: 309,483 / 187,706 = 1.6488).

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

* docs(troubleshooting): BUN_RESOLUTION_DRIFT and BUN_INSTALL_VERSION_INVALID anchors; CAPY_WRAPPER_STALE covers the wrapper row (A5, A8, A-f, G1)

Files: docs/troubleshooting.md, bin/gstack-capy-install, bin/gstack-doctor-check.sh, test/gstack-capy-install.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(install): marked per-directory bun and bunx shims for fresh Capy shells (A4, A-e)

gstack-capy-install (install mode, Capy machines only, GSTACK_BUN_SHIM=0 opts
out) writes ~/.local/bin/bun and bunx under ~/.gstack-runtime.lock. The shim
bakes the store, verdict directory, checkout and install default, walks up to
.git without spawning git, runs the Bun the (checkout, project) verdict
recorded, else a store Bun matching the .bun-version/package.json subset
(stderr names the --check to run when none matches), else the install
default, else the next unmarked bun on PATH. A foreign file or dangling link
is never touched. GSTACK_BUN_SHIM_EXPLAIN=1 prints project, record, choice and
rule; the doctor's runtime row shows the shim and its target.

Files: bin/gstack-capy-install, test/gstack-capy-install.test.ts, test/gstack-doctor.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(pregate): test stage never hangs silently; small machines record the remote gate (Lane C, GBRA-95)

C1 diagnosis (gbrain 92f0ea87, a core edit selecting 3,683 local files, 358
serial; Bun 1.4.2 and 1.4.3 identical): --timeout was applied per runner
spawn (unit group plus one per serial file), so the check was unbounded, and
spawnSync buffered all output until it returned: 360 s with 0 bytes on stdout
and stderr. A backgrounded child holding stdout made spawnSync wait the full
timeout and then report exit 0 (a silent wait, then green). Not the Bun 1.4.2
spawnSync hang.

C2/C-b: test-tier commands (lanes runner, guards, the --remote gate) run via
lib/pregate/supervise.ts: own process group (setsid), stdout/stderr appended
to a log file (no pipes), async supervision; CheckDef.run may return a
promise and runPregate/withFingerprint await it (a rejection is incomplete).
Leftover group members are killed after exit; SIGINT/SIGTERM to pregate
SIGKILLs running groups, writes no pregate.json, exits 130/143. Windows:
taskkill /T /F (reparented or detached descendants can survive; documented).

C3/C-a: --timeout bounds the whole check; groups past the deadline are not
started. --stall-timeout <s> (default stall_timeout_s, else 300; 0 disables)
kills a group whose log stops growing and records PREGATE_STALLED naming the
last file started. Live stderr: PREGATE_TEST per file (bun test adapter; any
runner for one-file runs), PREGATE_HEARTBEAT after 60 s without progress,
PREGATE: tests <verdict>. --json stdout unchanged.

C4/C-c: under 8 cores, a base-branch remote_gate or a non-local test_backend
records the local tier as a requires-remote obligation with its command
(pregate.json/remote_gate or test_backend/<backend>). --remote runs the
declared remote_gate now (needs --allow-repo-commands, never --policy-from
head) under --spend-cap/GSTACK_SPEND_CAP_USD; it clears only on
PREGATE_REMOTE: tree=<tested tree> tests>0 failed=0 with exit 0, and reports
spent/reserved/unknown.

G1: new code PREGATE_STALLED has its result-codes row, troubleshooting anchor
and the installer-form error line (CODE; url#anchor).

Files (G2): lib/pregate/supervise.ts (new), lib/pregate/types.ts,
lib/pregate/context.ts, lib/pregate/index.ts, lib/pregate/checks/lanes.ts,
lib/pregate/checks/guards.ts, lib/pregate/config.ts, lib/headless-artifacts.ts,
lib/result-codes.ts, bin/gstack-pregate, bin/gstack-doctor-components.sh
(generated), docs/pregate.md, docs/troubleshooting.md,
test/pregate-supervise.test.ts (new), test/pregate-remote.test.ts (new).

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

* docs(ship): Step 5 pre-gate text for stalls and the small-machine remote gate (Lane C-c)

The pre-gate paragraph now says `incomplete` blocks (a stall is incomplete)
and that `requires-remote` covers the remote gate under 8 cores, cleared by
`gstack-pregate clear` or `--remote --spend-cap <usd>` only under a cap the
user gave. Kept within the ship parity size cap (union 308,771 of 308,776
bytes; base 308,767), so the `--explain` hint moved to docs/pregate.md. The
handoff's escalation line already names any `requires-remote` lane; unchanged.

Files (G2): ship/sections/tests.md.tmpl, ship/sections/tests.md (generated).

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

* fix(install): one ok-trailer grammar with requested, pins-skipped and latest notes; bootstrapper revision 2 exports origin's VERSION (A9, A-a)

Notes ride only on the ok trailer in fixed order; the failure head is
unchanged, pinned by a test that runs the Capy skill's dispatch examples
against real doctor output. The latest note comes only from
GSTACK_CAPY_REMOTE_VERSION, which the bootstrapper sets from a fetch in this
run and unsets otherwise (a stale FETCH_HEAD never reaches the installer).

Files: bin/gstack-capy-install, bin/gstack-capy-session,
capy/skill/gstack/scripts/install.sh, capy/skill/gstack/SKILL.md,
test/gstack-capy-install.test.ts, test/capy-skill-wrapper.test.ts,
test/gstack-doctor.test.ts

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

* fix(upgrade): every success path certifies the caller's project; a pins failure keeps the move, ends on gstack: fail pins, exit 1, no pending marker (A7, A-b)

gstack-upgrade captures the caller's directory and git toplevel before any
cd. Already-current, fast-forwarded and resumed-pending paths run
bin/gstack-capy-install --project <it> on Capy (installing a verified pinned
Bun) or gstack-doctor --check --require runtime,pins elsewhere. The
reinstall's own trailer is held back when a project certification follows,
so no ok precedes a pins failure; a RUNTIME_PIN_CONFLICT stop becomes
gstack: fail pins — <conflict>. --check and --ff-only-no-reinstall certify
nothing.

Files: bin/gstack-upgrade, test/gstack-upgrade-bin.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(install): fresh-install log lines name the clone and every host installed (A10)

gstack-capy-install prints `cloned <repo> at <sha> (<version>)` for a checkout
it cloned, or one the bootstrapper (revision 2, GSTACK_CAPY_CLONED) cloned in
this run, instead of `reusing`. setup's final line is
`gstack ready (<hosts>)` from the same rows as its summary table, including
the --refresh-registered upgrade summary.

Files: bin/gstack-capy-install, bin/gstack-capy-session,
capy/skill/gstack/scripts/install.sh, setup, test/gstack-capy-install.test.ts,
test/host-conformance.test.ts

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

* fix(pregate): --remote runs the gate only when local tests are selected (Lane C4)

With only platform lanes selected, --remote no longer launches the remote
gate with an empty file list; the existing no-local-lane path applies.

Files (G2): lib/pregate/checks/lanes.ts.

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

* fix(autoplan): one-word rec, verified pairing, sealed attempt results, deferred reopen (Lane B: B1 B2 B4 B5)

B1/B-b/B-c  `rec` (aliases: all, approve all, accept all, take all recommendations)
    takes every recommendation, plan changes included; `as-is` keeps every
    direction. The render offers `rec` first and prints what it resolves to
    with gate_rev and rec_hash. `gstack-autoplan recommend` changes one
    recommendation before relay (provenance recorded, p1 and decisions follow)
    and bumps rec_rev, so a `rec` without the new --rec-hash is refused
    (GATE_REC_STALE). An agent answer needs the owner's words verbatim
    (--owner-reply <text>|-, --owner-reply-file; OWNER_REPLY_REQUIRED); a
    parseable owner reply that resolves differently from the relay is refused
    (GATE_OWNER_REPLY_MISMATCH). Supersedes field-wave R1 per the owner's
    gate_rev 1 decision.
B2/B-a  reconcile associates by title first; a file:line-only match is
    `unverified` (associated, never confirmed). `b` is recommended only for a
    confirmed pair with the same `changes` and one shared proposal (override)
    or the same normalized remedy; the basis says which. Negative control and
    positives copied from run-20261014225954 (test/fixtures/autoplan-pairing).
B4/B-e  each attempt writes <phase>-<voice>.<attempt>.md; submit refuses another
    attempt's file (ATTEMPT_MISMATCH) and binds a sealed 0444
    <phase>-<voice>.<attempt>.bound.md copy, so a late write changes nothing
    bound; reopen keeps sealed copies in place.
B5/B-d  answer records accepted changes as amendments (status amend_pending) and
    reopens nothing; revise reopens what changed. `revise --settle <id|d4>`
    discharges a packaging/ordering change on an unchanged plan
    (AMENDMENT_NOT_SETTLEABLE otherwise). APPLY/NEXT/status lines say which.

DRIVER.md and the reply table move to the rec grammar.

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

* feat(autoplan): durable checkpoint and resume --from; Capy checklist (Lane B: B12)

With GSTACK_AUTOPLAN_CHECKPOINT_DIR set, every command that leaves a run at
rest (gate_pending, amend_pending, complete, incomplete) publishes
<dir>/<run>/: checkpoint.json listing every file's sha256, the run under run/,
the owner's plan file under plan-input/ when it lives outside the run, and
restore.md as the human index. Publication swaps in a fully written sibling.
Checkpointing at every rest state, not only gate_pending, keeps accepted
decisions from rolling back on restore.

`gstack-autoplan resume --out <new dir> --from <checkpoint>` checks
containment (no absolute or .. paths, regular files only) and every hash
before publishing the run with one rename. It restores a missing plan file
into the run, journals `restored` with both hosts, and takes the lock.
CHECKPOINT_INVALID and CHECKPOINT_TARGET_EXISTS each get a result-codes row
and a troubleshooting anchor. Journal and revise staging paths are now
run-relative, so a moved run recovers.

DRIVER.md and the Capy skill's autoplan section get a short Capy checklist:
always drive gstack-autoplan, never present a hand review as an autoplan run,
relay every gate page with rec as the default, pass --owner-reply, and point
the checkpoint at the private project-user Drive folder.

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

* A-g: run the quick free tier on GSTACK_BUN_DEFAULT in CI; drift test pins it

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

* docs(autoplan): unattended contract and reply-unparsed fix move to rec, sealed results, settle and checkpoints (Lane B)

docs/unattended.md: rec/as-is/rec except grammar, --owner-reply and recommend/--rec-hash, unverified pairing and the shared-proposal rule for b, amend_pending and revise --settle, attempt-owned result paths with the sealed .bound.md copy, render --all, and the checkpoint/resume --from paragraph. GATE_REPLY_UNPARSED's fix names rec/as-is (DRIVER.md regenerated).

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

* docs/capy-install: resolver, versioned store, bun shim, trailer notes; wrapper minimum 2

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

* fix(pregate): --timeout bounds each runner process again, not the whole lanes check

Lane C made --timeout a deadline for the whole lanes check, so a repo whose
unit group and serial files each fit in 600 s but exceed it together ended
incomplete and blocked /ship. --timeout is per runner process again (unit
group, each serial file); the stall timeout still catches hung or silent
processes. A timed-out process is incomplete naming its last file, and the
next serial file still runs. Regression: three 1.5 s serial files under
--timeout 3 pass; one 5 s file is incomplete.

Files (G2): lib/pregate/checks/lanes.ts, bin/gstack-pregate (help),
docs/pregate.md, test/pregate-supervise.test.ts.

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

* fix(pins): CI exact pins for this platform are alternatives; the installer prefers a non-CI exact pin, else the install default among CI alternatives, else the highest verified (integration item 1)

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

* fix(pregate): clear validates the remote gate receipt; test_backend run URLs are attested

`gstack-pregate clear` accepted any --run-url for a small machine's
obligation. A pregate.json/remote_gate obligation now clears only with
--receipt <file> holding the gate's PREGATE_REMOTE: line, validated as
--remote validates it (tree equals the tested tree, tests > 0, failed = 0;
shared parseRemoteReport/remoteReportProblem); a run URL is usage (exit 2)
and a bad receipt is PREGATE_RECEIPT_INVALID (exit 1, obligation unchanged).
A test_backend/<backend> obligation has no machine-checkable receipt: its
--run-url is recorded as attested:<url> and the output says attested, not
validated. Platform lanes are unchanged. Hints name the right clear form.

New code PREGATE_RECEIPT_INVALID: result-codes row, troubleshooting anchor,
installer-form error line, regenerated doctor components.

Files (G2): lib/pregate/index.ts, lib/pregate/checks/lanes.ts,
bin/gstack-pregate, lib/result-codes.ts, bin/gstack-doctor-components.sh
(generated), docs/pregate.md, docs/troubleshooting.md,
test/pregate-remote.test.ts.

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

* fix(issue-links): derive this PR's author when gh runs as an app, so handoff and pr-body exclude the owner's own PRs (Lane D fix 1)

The handoff ran gstack-issue-links without --author, and the pr-body step only
asked the agent to pass one, so under Capy's app identity (viewer
`capy-ai[bot]`) the owner's own open PRs were listed as `Related:`. The bin now
derives the author itself: --author, else the gh viewer, else (for a `…[bot]`
viewer) the HEAD commit's GitHub user, else the owner of a user-owned
repository; with none it keeps every PR and reports ISSUE_LOOKUP_FAILED.
`--json` gains `author: {login, source}`; the stderr preview names it. Both ship
steps work unchanged, so the pr-body `--author` clause is dropped (union
309,487 -> 309,435 bytes, under the unchanged 1.6488 cap; skeleton 80,998).

Files: bin/gstack-issue-links, lib/result-codes.ts (ISSUE_LOOKUP_FAILED row),
docs/troubleshooting.md (#issue-lookup-failed), docs/pregate.md,
ship/sections/pr-body.md.tmpl, ship/sections/pr-body.md,
test/helpers/pregate-fixture.ts (gh stub: PR-author query),
test/ship-issue-intent.test.ts.

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

* fix(wrapper): GSTACK_CAPY_MIN_WRAPPER back to 1; an active wrapper below the minimum is a WARN; gstack-upgrade never leaves a pending marker for the wrapper row and maps every installer stop to a trailer (integration items 2, 9)

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

* fix(shim): validate and contain store pins, read the 'none' record outside projects and in the checkout, fall back in the resolver's order, stay quiet without a pin; quote baked paths; publish bunx and replace store entries without a window (integration items 3, 4, 6, 9)

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

* fix(install): --check fails BUN_RESOLUTION_DRIFT only when the recorded Bun is gone, changed or no longer satisfies the pins; a newer candidate is a note and the recorded Bun stays the launch Bun (integration item 5)

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

* fix(install): one final 'gstack ready (claude, codex)' after both host setups (integration item 7)

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

* fix(pregate): lanes and guards validate the install verdict like admission and return incomplete (INSTALL_UNVERIFIED, BUN_RESOLUTION_DRIFT) before launching; the checkout binds the 'none' verdict (integration item 8)

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

* docs: CI pin alternatives, shim order and containment, drift note, wrapper WARN, final ready line

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

* fix(autoplan): settle after or with a revise, title pairs need the same remedy, legacy gates refuse rec (integration items 1, 7, 8)

1. A gate with a scope change (uc1) and a packaging change (uc2) answered
   `rec` deadlocked: after `revise --plan v2 --incorporates uc1` reopened the
   phases, `revise --settle uc2` compared against the first phase snapshot
   (now undefined) and was refused forever. Settle evidence is now the plan
   hash recorded when the owner accepted the change (Amendment.plan_sha256)
   or a later plan_revised hash, plus "the plan file has no unrevised edits".
   `--settle` may ride the same revise as `--plan/--incorporates` and binds
   to the revision's hash. Settling the last amendment after a reopen leaves
   the run `running` (the reopened phases rerun) instead of approving it.
   NEXT and PLAN_AMEND_PENDING list the settle step before the edit step.
7. A title association alone no longer confirms: the same remedy confirms,
   opposite remedies (one keeps, drops or negates what the other changes)
   disagree, different or missing remedies stay `unverified`. The pairing
   fixture gains same-title negative controls (opposite and different
   remedies) and a same-remedy positive, for ordinary findings and user
   challenges.
8. A gate.json without rec_rev (written by 1.91.101.0, whose render said
   `all` leaves b items pending) refuses rec/all/approve all/`all except`
   with GATE_REC_STALE until a re-render binds --rec-hash; explicit tokens
   still answer. New gates carry rec_rev: 0.

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

* fix(autoplan): checkpoints never touch the live run; generations, newest-valid restore, validated staging (integration items 2-6)

2. checkpointRun canonicalizes the collection and the run directory (symlinks
   resolved through the deepest existing ancestor) and refuses, before
   touching anything, when they are the same or one contains the other
   (CHECKPOINT_OVERLAP); restore applies the same check to source, target and
   staging. Before, <root>/<run> equal to the live run replaced it.
3. Publication writes an immutable generation (generations/g<revision>-…),
   renames it into place complete, then switches current.json with one
   rename; the previous generation is kept, so an interruption at the publish
   boundary (fault hook checkpoint-before-pointer) leaves the last checkpoint
   current. A generation never replaces a newer one (higher run revision):
   "CHECKPOINT: skipped … newer". Publication and restore hold the
   collection's .lock; restore also holds <out>.lock.
4. Restore checks every listed file's real path stays inside the generation
   (an ancestor symlink no longer copies outside files), parses run.json and
   runs validateRun on the staged run before publishing; a refusal leaves
   --out absent. readState reroots plan_input and plan_source for moved runs,
   and a restore with the owner's plan file gone points both at the restored
   copy, so a second restore cycle and a later revise work.
5. A checkpoint failure after a committed command prints
   "CHECKPOINT: failed for <run>: … ; the command's result stands" on stderr
   and keeps the command's exit code.
6. `gstack-autoplan checkpoints --from <dir>` lists generations newest first
   (run, gate_rev, status, revision, time, hash ok|bad, current).
   `resume --from <collection or root>` restores the newest generation that
   verifies, printing each corrupt newer one it skipped; a root holding
   several runs must name one.

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

* chore: budget the Capy skill's autoplan checklist and trim its install notes

Files: capy/skill/gstack/SKILL.md, test/fixtures/context-budget.json (capy/skill/gstack ceiling only: 2364 tok x 1.10)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: release 1.91.102.0 (tentative stamp) — GBRA-40 field list

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

* test: supply _setup_ready_line to setup-tail harnesses, register the pairing fixture, make the admission test runner-Bun independent; announce shim writes

Files: test/setup-hook-syntax-gate.test.ts, test/redact-prepush-hook.test.ts, test/setup-playwright-best-effort.test.ts, test/helpers/free-fixtures-data.ts, test/pregate-guards.test.ts, bin/gstack-capy-install
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci: run quick-bun-default on the ubicloud-standard-8 class the free suite uses

The ubuntu-24.04 runner timed out two Swift typecheck tests at 30 s (render @Observable marker, ios-qa-regen); both pass on Bun 1.4.3 on Ubicloud.

Files: .github/workflows/free-tests.yml
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(pregate): read bun test's GitHub Actions group headers; exclude pregate-remote from the Windows subset; give quick-bun-default the free suite's flake policy

Files: lib/pregate/supervise.ts, test/pregate-supervise.test.ts, scripts/lib/windows-curation.ts, .github/workflows/free-tests.yml
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: run the Capy wrapper and shim doctor-row tests on POSIX only (Git Bash prints MSYS paths)

Files: test/gstack-doctor.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: restamp 1.91.107.0 (1.91.106.0 is claimed by #3209)

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

* fix(pregate): spawn the supervised shell through lib/script-invocation (#3197 tripwire)

The supervisor now resolves bash via bashCommandInvocation, so Windows uses
Git Bash instead of a PATH bash, and an unresolvable bash reports spawn-failed.
guards.ts and lanes.ts no longer spawn bash (they run through supervise.ts),
so their KNOWN_SITES entries are removed.

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

* test(qa-functional): disable auto gc and maintenance in the fixture repo

On eval-slices (9), qa-functional-webhook-fix failed mid-run with 100+
'directory watch lost: .git/objects/..' entries and forbidden writes to
.git/info/refs and .git/objects/pack. That is git's detached auto
maintenance repacking the fixture under the write observer. This applies the
same gc.auto=0 / maintenance.auto=false config qa-callers-fixture uses.

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

* test(autoplan-guard-pty): say Phase 2 starts without waiting for user input

On eval-slices (1), haiku published Phase 1 and then ended its turn with
'Waiting for next invocation to continue with step 8'. Step 5 tells it to end
its turn while the reviewer runs, so step 8's 'In a later message' read as a
later user turn. Phase 2 never started, the guard logged 2 of the 4 expected
allows, and the run hit its deadline. Step 8 now says to continue in the next
assistant message, still separate from the step 7 publication message.

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>
2026-10-16 23:09:23 +00:00

25 KiB
Raw Blame History

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; 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.

.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 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/).

.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
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)
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)
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)
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)

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.

  • 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.

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 test-tier command (the lanes runner, each guard, the --remote gate) runs supervised: see 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.

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.

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.

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.

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:

{ "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.

Sealed output: the pre-push hook and gstack-pregate sealed

{ "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.

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). 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).

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.

{
  "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.

{
  "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.

{ "sealed": {
    "paths": ["heldout/**"],
    "transcripts": ["runs/**/transcript*.jsonl"],
    "answer_markers": ["SEALED ANSWER:"],
    "seed_patterns": ["shuffle_seed=\\d+"],
    "local_paths": true } }

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).