Merge remote-tracking branch 'origin/main' into gsta-145/local-path-param-expansion

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

# Conflicts:
#	CHANGELOG.md
#	docs/pregate.md
This commit is contained in:
garrytan committed 2026-10-29 16:24:31 +00:00
commit 82d9d82c2e
206 files changed
+10243 -863

No files matched your search

+4 -3
View File
@@ -181,9 +181,10 @@ The agent should:
4. Verify with a test spawn
**Native skills or ACP.** `./setup --host openclaw` (or `./setup` run inside an
OpenClaw session) gives OpenClaw's own agent gstack's skills: one `gstack`
router skill in `$OPENCLAW_STATE_DIR/skills`, rendered with OpenClaw's tool
names and pointing at the installed gstack, so the agent runs gstack's
OpenClaw session) gives OpenClaw's own agent gstack's skills: the five task
skills (review, investigate, qa, ship, plan-eng-review) listed directly and one
`gstack` router skill for the rest in `$OPENCLAW_STATE_DIR/skills`, rendered
with OpenClaw's tool names and pointing at the installed gstack, so the agent runs gstack's
workflows and helpers itself. Use ACP when the work should happen in a Claude
Code session with the full install (hooks, Claude tool names); use native
skills when the OpenClaw agent should do it. Both can be installed together.
+1
View File
@@ -92,6 +92,7 @@ gstack/
│ ├── test/ # Integration tests
│ └── dist/ # Compiled binary
├── agents-digest/ # Committed 2KB instruction-tier rules digest (gstack-AGENTS.md) for rules-reading hosts
├── modules/ # loadouts.json (+ loadouts.schema.json): model rungs, pinned modules and named loadouts; read by scripts/modules.ts, rendered by bin/gstack-modules (docs/loadouts.md)
├── extension/ # Chrome extension (side panel + activity feed + CSS inspector)
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-lint/ = the static and render tiers behind bin/gstack-design-lint, ported from hallmark's sloplint; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
│ └── diagram-render/ # Vendored mermaid + excalidraw runtimes, built into one offline bundle the renderer loads
+8
View File
@@ -783,6 +783,14 @@ once, plus cleanup. Each still has 120 seconds of model work. Its 17 workflow
judges own their deadline and abort signal, with five seconds for terminal
recording inside a ten-second Bun grace; the other 11 retain their existing
120-second Bun timeout. Late responses cannot create records or cache passes.
When a judge's abort signal cancels its request, `callJudge`
(`test/helpers/llm-judge.ts`) prints one `llm-judge-aborted` JSON line to
stderr: elapsed time, wrapper `attempts` and `retryWaitsMs` (429 waits),
`httpRequests` and `httpStatuses` (so SDK retries show), `connectedMs`,
`firstEventMs` and `lastEventMs`, the open `block` type, `thinkingChars` and
`textChars`, `messageId`, `requestId` and the `reason` (at most 200
characters). It holds counts and ids only, never content, and changes no
request, score or verdict.
The ship documentation file reserves 4,920 seconds for four 600-second cases and
eight 300-second fault cases, run once, plus cleanup; in CI each case runs as its
+1 -1
View File
@@ -60,7 +60,7 @@ depends on measured duration or an explicit `QUICK_CORE` entry, not this table.
| CLI/API/webhook QA without browser setup | `qa-functional-fixture`, `qa-functional-evidence`, `qa-lazy-sections` | `skill-e2e-qa-functional`: CLI and webhook report sessions | Linux/macOS free; selected PR gate; Windows only where curated |
| Report-only preserves local and remote authority | `qa-only-capability`, `qa-functional-observer`, `qa-functional-observer-atomic`, `qa-caller-authority` | Independent report-only sessions with synthetic owned endpoints/auth | Linux kernel observation; free callback controls plus selected PR gate |
| Repair reproduces the defect, adds a failing regression and rechecks adjacent behavior | `qa-fix-loop-fixture`, `qa-functional-evidence` | `skill-e2e-qa-functional-fix`: CLI and webhook repair sessions | Free controls plus selected PR gate |
| Review and Ship actually explore | `qa-exploratory-callers`, `qa-caller-report-observer`, `qa-checkpoint-evidence` | `skill-e2e-qa-callers`: actual Review/Ship callers | Free captures plus selected PR gate |
| Review and Ship actually explore | `qa-exploratory-callers`, `qa-caller-report-observer`, `qa-caller-review-plan`, `qa-checkpoint-evidence` | `skill-e2e-qa-callers`: actual Review/Ship callers | Free captures plus selected PR gate |
| Smoke expiry preserves required plan checks | `qa-deadline`, `qa-deadline-selection`, `qa-browser-deadline-evidence` | `ship-exploratory-plan-checks` | Free deadline/dispatch controls plus selected PR gate |
| Late changes invalidate affected results | `qa-caller-freshness-order`, `qa-deadline-publication-observer`, `shared-libs-revalidation-prompt` | `ship-exploratory-late-input` and the existing late-input documentation handoff | Free stale-input controls plus selected PR gate |
| Documentation completes before publication and respects protected files | `docsync-authority`, `docsync-atomic-writes`, `docsync-report-interface`, `docsync-lifecycle-interface` | `skill-e2e-ship-docsync`, `skill-e2e-docsync-spawned` | Free state/permission controls; registered gate/periodic scenarios retain their tiers |
+16 -5
View File
@@ -138,8 +138,9 @@ this rule shipped cites this section by name.
**What the lane sends.** The plan and its gate in plain language a person outside the
project can follow: what changes, why now, what the change does, what it costs, the lane's
pick, and the recommended one-word reply. `gstack-owner-brief render --captain --out <run dir>`
prints that from the plan's `## Before → after`, `## Why it matters`,
`## What caused this work` and `## Big decisions` sections, with the cost lines
prints that from the plan's `## ELI5`, `## Before → after`, `## Why it matters`,
`## What caused this work` and `## Big decisions` sections (also saved as `eli5` in
`gate.json`, so `gstack-gate render` opens with them), with the cost lines
(`GATE_SPEND:`, `GATE_REC_COST:`) and a last line for the controller; a plan missing a
section exits `CAPTAIN_BRIEF_INCOMPLETE`, so the lane adds the section and renders again
([capy/skill/gstack/captain.md](../capy/skill/gstack/captain.md) § 3 lists them). The lane
@@ -156,10 +157,12 @@ controller and been answered into the run
every accepted plan change has been incorporated (`revise`), every reopened phase has
finished, and the run reports `APPROVED` (`status=complete` after `export`). An answered
gate that still prints `PLAN_AMEND_PENDING` or `REOPENED:` is not a completed plan, and an
imported approval row alone is not completion.
imported approval row alone is not completion. A later gate revision that asks nothing new
is answered with the owner's recorded words and says `CARRIED:`; that is the owner's word,
not a new ask ([carry-forward](unattended.md#carry-forward)).
**The record.** `gstack-approvals import --autoplan <run dir> --by "<owner>"` writes the
approval row (date, the ask, the owner's exact words, the lane that answered as relayer,
**The record.** `gstack-approvals import --autoplan <run dir> --by "<owner>"` (with
`--stack <b1,b2,...>` for a stacked PR set) writes the approval row (date, the ask, the owner's exact words, the lane that answered as relayer,
what it covers); `gstack-approvals render` puts it in the report and the PR body. Before
training the PR the controller compares that row's words and `gate_rev` with the ask it
relayed and the reply it carried back; a row that does not match is not READY (shape A).
@@ -231,6 +234,14 @@ a push included. The sequence that works unattended on Capy:
10. After the slot call, the stamp commit keeps the pin's receipt; re-READY with the same
receipt, no second /ship.
When the base moves under a reviewed branch, merge it in; never rebase. A merge that
leaves the product diff unchanged carries the receipt with `gstack-ship-receipt carry`
(pregate at HEAD, no review rerun), and a merge whose resolution changed product code
needs a fresh /ship. A rebase carries only onto a tree the receipt already covers with
the same product diff; any other rebase is a full /ship
([ship-policy.md](ship-policy.md#receipt-carry)). A controller never asks a lane to
rebase a reviewed branch.
## Merge trains
gstack's queue tool drives trains when the repository's `.gstack/ship-policy.json` carries
+20 -2
View File
@@ -155,7 +155,16 @@ and `/proc` start time; elsewhere `ps -o lstart`), because a pid is reused after
a reboot or a machine wake. `bin/gstack-upgrade`, and so the session-start
self-upgrade, refuses with [UPGRADE_BUSY](troubleshooting.md#upgrade-busy)
while a lease held by a live process outside its own chain exists, naming each
holder as `pid <p> (<age>m <command>)`. The team-mode SessionStart hook
holder as `pid <p> (<age>m <command>)`. The install-mode self-upgrade also
defers while its own chain holds a live lease under 120 minutes old. An install
run from inside a workflow (/ship's docs step running `install.sh --for
document-release`, #3571) prints `SELF_UPGRADE: skipped (a gstack run in this
session holds a workflow lease on this installation: ... (UPGRADE_BUSY))`,
leaves the checkout where the run started and stays ok with `behind=`. Each
`gstack-skill-start` renews the lease. On Capy the leased pid is the machine's
long-lived `capy-server`, so the deferral ends when the lease turns 120
minutes old, not when the process exits; `bin/gstack-upgrade` run by hand
moves the checkout sooner. The team-mode SessionStart hook
(`bin/gstack-session-update`) honors the same leases: it logs
`SKIP leased pid=<p>/<age>m` to `analytics/session-update.log` and tries again at
the next session start, without writing its hourly throttle stamp. A lease whose
@@ -274,7 +283,16 @@ keeps the lease. `bin/gstack-lease list` shows `alive|dead|stale`, and
honoured, the resolved Bun (`bun_path`, `bun_version`, `bun_rule`, and
`bun_install` when this run downloaded it), the resolved Node when the
project pins one (`node_path`, `node_version`, `node_rule`), the trailer, the remote
minimum, the launch PATH and the time. Admission refuses a workflow whose
minimum, the launch PATH and the time. An ok run (install or `--check`) at
the same checkout, project, HEAD and Bun as an earlier ok verdict keeps what
that verdict certified, so one machine stays admitted for /ship and its
`--for document-release` docs step (#3561): `required` becomes the union,
pins count as planning-only only when both runs skipped them, and the
verdict's `for` lists every workflow, comma-separated (`for=ship,document-release`;
the installer prints `the install verdict at <file> keeps for=<old> and adds
<new> (for=<list>)`, and the trailer still names this run's workflow). A
failing run, a new HEAD or another Bun replaces the verdict, and so does any
workflow install over a review-voice verdict. Admission refuses a workflow whose
running Bun is not the one the verdict recorded
([BUN_RESOLUTION_DRIFT](troubleshooting.md#bun-resolution-drift)), and so
do the pregate's lanes and guards, which also refuse an invalid verdict
+23
View File
@@ -69,3 +69,26 @@ Raw transcripts stay under `<out>/runs/`.
- **Trials are fixed before the run.** No trials are added after a result. One same-tree rerun of a red task is
allowed and recorded as flaky; a second failure stands.
- **Blinded adjudication.** A disputed answer goes to a fresh judge that sees the task and the answer, not the arm.
## Notes for implementers (2026-10-28)
Offered to gstack-evals' Lane A (interface v0.4). Nothing above changes; these are conventions gstack's task files
already assume, and one optional field.
- **State root.** gstack skills keep state under one root (docs/state-root.md): `GSTACK_STATE_ROOT` wins, then
`GSTACK_HOME`. Set both to `<work dir>/.gstack-home` for the agent, `setup` and `check`; `GSTACK_HOME` alone loses
to a `GSTACK_STATE_ROOT` inherited from the caller's shell, and the agent then writes the caller's real state. A
task's `seed.sh` creates `.gstack-home` and lists it in `.git/info/exclude`, so `git diff` checks ignore it and
`check` reads state there (/learn's `add` and `prune-stale`, /office-hours' design-doc and mockup checks).
- **`resumable`** (optional, in `verdict.json` and any error record): `true` when an INCOMPLETE run can be finished
with `--resume <out>`, which runs only cells that never ran, under the same lock and trial count, so it adds no
trials after a result; `false` for a terminal stop, which needs a fix and a fresh run. A missing field reads as
`false`.
- **Lock.** A paid run needs `tasks.lock.json` committed beside the tasks file; `/write-skill`'s diet step 6 runs
`--lock` and commits it.
- **Sidecar.** Extensions live in `tasks.ext.json` beside the tasks file, keyed by task id, so this format stays
unchanged. gstack's free test checks only that each entry names a task in `tasks.json` and uses known keys.
- **Interactive tasks.** /office-hours' `must` patterns grade a headless run that ends where the skill stops for
input, as upstream skill-diet's runs do. On a host that answers questions from the sidecar's `user` replies, the final answer is
the session's last message, so the sidecar's `judge` line carries the behavior there. Open for Lane A: grade
`must` over the run's assistant messages on such hosts, or keep it on the final answer and rely on `judge`.
+24 -2
View File
@@ -24,6 +24,14 @@ neither does `./setup --local`.
**What it writes.** Copies in the harness's own skills folder, never edits to
`openclaw.json`, Hermes's `config.yaml` or any context file you did not name.
A context block is opt-in: `gstack-harness register openclaw|hermes|generic
--context <file>` writes the marked block into that file only, after printing
the file and its scope (`profile` inside the harness's own home, else
`project`). Setup, upgrades and refresh never add one, a block you delete stays
deleted, a second `--context` file keeps the first (the ledger keys blocks by
file), and uninstall restores each file byte for byte. It refuses `SOUL.md`,
and for Hermes a new file that would outrank rules Hermes loads now
(`.hermes.md`/`HERMES.md` load before `AGENTS.md`): `HARNESS_CONTEXT_REFUSED`.
Every file goes into a ledger, `$GSTACK_STATE_ROOT/harness/<id>.ledger`, with
its sha256. Registering again changes only what changed; a folder or skill name
gstack did not write stops it (`HARNESS_UNOWNED_ENTRY`); `gstack-uninstall`
@@ -35,10 +43,24 @@ when it is the one gstack wrote; an edited block is left for you. `./setup
`gstack-harness status` lists them and runs the harness's own skill list where
it has one (`visible to harness: yes`). Errors: [troubleshooting](troubleshooting.md#agent-harnesses-gstack-harness).
**Registered, listed, used.** These are three separate facts, and this page
claims only the first two. *Registered*: gstack's files are in the harness's
skills folder and its ledger (`gstack-harness status`). *Listed*: the harness
puts the skill in the skills section of the model's prompt (`visible to
harness: yes`, from the harness's own skill list). *Used*: in a task that fits,
the model opens the matching skill and runs its first step. Use rates are
pending HARN-5's fixed panel (OpenClaw and Hermes chat mode, five trials per
cell). When they land they are measured under the eval adapter's
`GSTACK_SESSION_KIND=spawned` marker, which auto-decides gstack's questions, so
they say nothing yet about interactive sessions. Two levers aim at use:
each router line opens with the skill's own trigger sentence ("Use when asked
to ..."), and each Hermes description opens with a trigger list of at most 57
characters, the part of a description Hermes's skills index shows.
| Harness | What gstack registers | Install | Verify | Remove |
|---|---|---|---|---|
| OpenClaw | one `gstack` router skill (one description line in every request; its body lists every skill and its path) in `${OPENCLAW_STATE_DIR:-~/.openclaw}/skills/gstack`; `--skills all` registers every skill as its own top-level folder | `./setup --host openclaw`, or `./setup` inside OpenClaw | `openclaw skills list --json` shows `gstack` (source `openclaw-managed`, eligible) | `gstack-harness unregister openclaw` |
| Hermes | every skill, as `gstack-<skill>` in one `gstack` category, in `${HERMES_HOME:-~/.hermes}/skills/gstack` (Hermes opens only skills it has indexed) | `./setup --host hermes`, or `./setup` inside Hermes | `hermes skills list` shows the `gstack-*` skills in category `gstack` | `gstack-harness unregister hermes` |
| OpenClaw | default `--skills hybrid`: `gstack-review`, `gstack-investigate`, `gstack-qa`, `gstack-ship` and `gstack-plan-eng-review` as top-level skills plus one `gstack` router skill whose body lists every other skill and its path, in `${OPENCLAW_STATE_DIR:-~/.openclaw}/skills`. With OpenClaw 2026.10.6's 19 eligible bundled skills, hybrid's skills section is 10,290 characters, full format, under its 18,000-character budget; `--skills all` (17,971, compact format, descriptions cut) lists every skill as its own folder, `--skills router` (6,701) only the router | `./setup --host openclaw`, or `./setup` inside OpenClaw | `openclaw skills list --json` shows `gstack` and the five task skills (source `openclaw-managed`, eligible) | `gstack-harness unregister openclaw` |
| Hermes | every skill, as `gstack-<skill>` in one `gstack` category, in `${HERMES_HOME:-~/.hermes}/skills/gstack`. Hermes's skills index lists only skills registered as folders, so a router would leave the rest out of the listing (`skill_view` still loads an unlisted skill by name, but nothing tells the model it exists) | `./setup --host hermes`, or `./setup` inside Hermes | `hermes skills list` shows the `gstack-*` skills in category `gstack` | `gstack-harness unregister hermes` |
| dots (feder-cr) | the `gstack` router in `/home/dot/skills/gstack`, which the Dot lists every turn | `./setup` on the Dot's computer, or `gstack-harness register dots` | the Dot's Skills page, or `GET /skills` | `gstack-harness unregister dots` |
| QM | **not available yet.** QM imports a skill pack by cloning the whole repository and caps it at 32 MiB and 5,000 files; gstack's repository is about 73 MiB, so `gstack-harness register qm --yes` stops before sending anything ([why](troubleshooting.md#harness-qm-pack-too-large)) | meanwhile, paste `gstack-harness register generic --print` into the agent's instructions | ask it for a gstack skill | delete the pasted text |
| Muse Code, Grok Build | nothing: both read `~/.claude/skills`, so the Claude install (`./setup`) is what they see, with Claude tool names | `./setup` | `gstack-harness status` runs `muse skills list` / `grok inspect` and reports *discovered (Claude-rendered, unverified)* | `gstack-uninstall` |
+40
View File
@@ -0,0 +1,40 @@
# Module loadouts
gstack can render a smaller or larger set of its instructions for a given model. Each toggleable piece is a **module** with an id. A **loadout** is a named set of modules, and `modules/loadouts.json` maps each model **rung** to a loadout. Today every rung loads `full`, which is the same render gstack has always shipped. A loadout changes only when the shared skill eval (GSTA-2) measures a smaller one that matches `full` within its preregistered margin.
Install-time model detection, which picks a loadout for the model you run, ships with the first loadout that is not `full`. Until then, loadouts are an eval tool: `bin/gstack-modules render` produces any loadout as a byte-exact tree with a lock file.
## Terms
| Term | Meaning |
|---|---|
| Module | One of four kinds. `skill:<dir>` is a whole skill. `section:<skill>/<id>` is one entry of `<skill>/sections/manifest.json`. `overlay:<name>` is one `model-overlays/<name>.md`; excluding a base overlay also removes the base's text from every overlay that inherits it; the inheriting overlay's own text stays. `preamble:<id>` is one piece of the shared preamble (`PREAMBLE_PIECES` in `scripts/resolvers/preamble.ts`). |
| Pinned | A module no loadout can remove where `full` renders it: the start command, plan-mode rules, the question format, completion status, the unattended profile, /ship's review army and adversarial pass, the router and `/gstack-upgrade`. A skill's `preamble-tier` still applies, so a tier-1 skill has no question-format section in any loadout. |
| Rung | How much scaffolding a model gets: `frontier`, `strong`, `mid`, `light`, or `unknown`. Model ids are matched after lowercasing and stripping a `provider/` prefix (`anthropic/claude-haiku-5-5` is `mid`). Rungs are separate from the frontier/smart tiers in `lib/model-catalog.ts`, which choose which model runs a role. |
| Loadout | `{ base, exclude, include, preamble_tier_max, note }`. `full` (include `*`) is always present. An unknown model gets `default.unknown`, which is `full`. |
## Commands
```bash
bin/gstack-modules list [--json] [--pinned] # every module id, kind, source, pin, source bytes and sha256
bin/gstack-modules validate # check modules/loadouts.json against the catalog
bin/gstack-modules render --host claude --loadout full --out-dir ~/arms/control
bin/gstack-modules render --host claude \
--modules 'full,-section:office-hours/second-opinion,-section:office-hours/visual-design' \
--out-dir ~/arms/oh-lean
```
`render` writes the whole host tree into a staging directory beside `--out-dir`. It checks that no rendered file still points at an excluded module, writes `loadout.lock.json` last (loadout name or `adhoc`, the normalized spec and its sha256, host, model, every excluded id, and the sha256 and byte count of every file), then swaps the result into place. A failure leaves an earlier render at `--out-dir` untouched. Claude section links name `--out-dir`, so point the eval harness at that directory. Pass the same `--model`, `--install-root` and `--plan-mode` the install under test uses, so the arms differ only by the modules. Codex renders default to the host's generic `gpt` overlay, while `./setup` uses `gpt-6-astra`.
`bun run gen:skill-docs --loadout <name> --out-dir <dir>` and `--modules <spec> --out-dir <dir>` apply the same filter to a generation run without the staging, dangling-reference check or lock file; a selection with exclusions and no `--out-dir` is refused with `MODULE_SPEC_INVALID`, because the committed tree is always the `full` render. A non-`full` render adds `<!-- loadout: <name> -->` to each SKILL.md header.
## Building an eval arm
1. Render the control with `--loadout full` and each arm with `--modules full,-<id>,…`. Every refusal names its code and fix ([troubleshooting](troubleshooting.md#module-loadouts-gstack-modules)).
2. Preregister the lock files' `spec_sha256` and per-file sha256 values with the eval.
3. Excluding a section removes its STOP pointer, its Section-index row and its file. The step becomes `> Not loaded in the <loadout> loadout: skip <trigger>. Do not offer it, and treat it as not run.` Skeleton text that belongs to the section, such as office-hours' Phase 3.5 offer, sits inside `{{IF_SECTION:<id>}}…{{/IF_SECTION}}` in the template and goes with it.
4. The autoplan runner (`gstack-autoplan`) reads review methodology from the source tree, not from a rendered loadout, so autoplan-runner arms always measure `full` methodology.
## Changing a loadout
Add or edit an entry under `loadouts` and cite the GSTA-2 run (preregistration, margin, per-rung result) in its `note` and in the PR body. A model-specific loadout that wins a preregistered comparison is allowed even if it breaks the expected nesting (light ⊇ mid ⊇ strong). Committed SKILL.md files are always the `full` render, so the parity suite, the SKILL.md size guard, the size ledger and the catalog and context budgets keep measuring `full`.
+48 -5
View File
@@ -25,7 +25,10 @@ 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
says `policy head (unreviewed)`. So a lane or allowlist entry a branch adds to
its own `pregate.json` counts only once it is on the base; `PREGATE_NO_LANE`'s
fix line says so when the branch edits the file
([troubleshooting](troubleshooting.md#pregate-no-lane-own-policy)). An unresolvable base exits 3 with
`POLICY_SOURCE_UNAVAILABLE`. Globs are repo-relative (no `..`, no absolute
paths, at most 64 per list); commands are one line under 512 bytes.
@@ -146,7 +149,12 @@ Two stages with separate clocks, both printed:
~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).
(`gstack-regen check` in a scratch worktree). A fresh entry with touched
outputs then runs once more with those outputs deleted; the ones the command
recreates byte-identically are the check's `proven` paths, which
`gstack-ship-receipt carry` reads. The verdict comes from the first run: an
output the second run does not recreate, or a second run that fails, is
simply not proven.
- `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.
@@ -763,11 +771,13 @@ with a timing flake: never raise the bound.
```json
{ "sealed": {
"paths": ["heldout/**"],
"paths": ["heldout/**", "**/sealed/**"],
"transcripts": ["runs/**/transcript*.jsonl"],
"allow_paths": ["**/sealed/*/MANIFEST.json", "**/protected/MANIFEST.json"],
"answer_markers": ["SEALED ANSWER:"],
"seed_patterns": ["shuffle_seed=\\d+"],
"local_paths": true } }
"local_paths": true,
"case_insensitive": true } }
```
A repo with a `sealed` key gets a configured local check against accidental
@@ -789,8 +799,41 @@ gstack-pregate sealed [--range <a..b>] [--remote origin] [--base <branch>] [--js
default such as `${VAR:-…}` or `${VAR-…}`) check each commit's added lines.
Known limit: a path glued to the text before it, by a hyphen (`docs-…`,
`$VAR-…`, `${VAR}-…`) or a letter (`-I…`, `-L…`), is not matched. Findings print rule, `file[:line]` and commit, never the text.
- **Globs.** `paths`, `transcripts` and `allow_paths` match the whole
repo-relative path from the repository root. `*` and `?` stay inside one
path segment; `**/` spans zero or more directories; a `**` not followed by
`/` matches across directories (`heldout/**` covers every depth under
`heldout/`); `[` and `{` are literal. So `sealed/**` misses `tasks/x/sealed/a.json`:
to guard a `sealed/` directory wherever it sits, write `**/sealed/**`.
- **Case.** Matching is case-sensitive. `case_insensitive: true` folds case
for `paths` and `transcripts` only, so `**/heldout/**` also catches
`HELDOUT/` and `HeldOut/`. Separator and spelling variants (`hold-out/`,
`held_out/`, `heldout-v2/`) are not folded: list each as its own glob.
The effective value is the base branch's OR the pushed one (it only tightens).
- **Exceptions.** `allow_paths` exempts publishable files that sit next to
sealed data (a `MANIFEST.json` of hashes and counts) from `sealed.path` and
`sealed.transcript`, applied after the deny globs and before waivers. They
are read from the destination base branch only: an exception a push adds
takes effect once it lands on the base branch, so a branch can never widen
its own. They never fold case. An entry whose file name contains `**`, or
is only wildcards before its extension (`**`, `sealed/*`, `**/*.json`,
`sealed/**.json`), is refused; a partial wildcard such as `MANIFEST*.json`
is accepted and matches every file it spells, so name the file. An exempt file is checked by the
text rules only, so configure `answer_markers` when you exempt files next to
sealed data. Each exempt path is listed on every push (`exempt <rule>
<file> commit <sha> (sealed.allow_paths <glob>)`) and counted as
`exempt=<n>` on the `SEALED:` line.
- **Version skew.** A gstack older than the release whose CHANGELOG adds
`sealed.allow_paths` refuses `allow_paths` and `case_insensitive` as
unknown keys, so every push and `gstack-pregate run` in the repository
fail on that machine. Raise `min_gstack_version` in
`.gstack/ship-policy.json` to that release before landing either key.
- **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
pushed commit's: a push can tighten it, never relax it (`allow_paths` come
from the base branch alone, and a pushed deny glob cannot override one). The
`SEALED:` line names both as `policy=<remote>/<base> + commit <sha12>`.
`gstack-pregate sealed` only checks and never pushes, so confirm a push with
`git ls-remote <remote> <ref>`. 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.
+86 -20
View File
@@ -128,26 +128,75 @@ applies no edits is its exit too.
<a id="review-depth"></a>
## Review depth by diff class
`gstack-review-plan --base <base>` classifies the diff and prints the reviewers
the next step dispatches (`REVIEW_CLASS:`, `REVIEW_PLAN:`, one `DISPATCH:` per
reviewer with its effort, one `SKIP:` per reviewer left out, `--json`). A mixed
diff takes its most restrictive class; a `gstack-diff-scope` error fails closed
to code, and code with the gate scan unavailable fails closed to gate.
`gstack-review-plan --base <base>` classifies the diff, picks a **depth** and prints
the reviewers the next step dispatches: `REVIEW_CLASS:`, `REVIEW_DEPTH: <d>
runtime_lines=<n> rules=<hash> reasons=<…> codes=<…>`, `GATE_SCAN: tagged=<n>|unavailable
head=<sha>`, one `DEPTH_ERROR:` per input that failed closed, one `DISPATCH:` per reviewer
with its effort and a shell-quoted `DIFF_CMD:` (plus `DIFF_EXCLUDE: <name> none` when it
reads everything), one `SKIP:` per reviewer left out, and `--json` with the same fields.
A plan that fails closed to full exits 0; a diff git cannot list exits 1.
| Class | What it is | Pass 1 dispatches |
**Classes** are unchanged. A mixed diff takes its most restrictive class: **docs**
(markdown no agent or gate reads), **tests** (test files and fixtures), **code**
(everything else, including every `SKILL.md` and template, markdown in a skill
directory, `*/sections/**`, `scripts/resolvers/**`, rule docs, checklists and bins) and
**gate** (code plus auth or migration scope, a hunk the gate scan tags, a deleted code
or test path, CI workflows, credential, state-root and the paths below).
**Runtime lines** are the changed lines of code-class paths, minus outputs the base
commit's `.gstack/generated.json` registers. Docs, tests and release metadata are never
runtime; a `.tmpl` edit plus its regenerated `SKILL.md` counts the template once.
| Depth | When | Pass 1 dispatches |
|---|---|---|
| **docs** | markdown no agent or gate reads: README, CHANGELOG, TODOS, how-to and explanation docs | the parent's core checklist only |
| **tests** | test files and fixtures only | core plus the testing specialist |
| **code** | everything else, including every `SKILL.md` and template, any markdown in a skill directory, `*/sections/**`, `scripts/resolvers/**`, `docs/*-policy.md`, `docs/captain.md`, `docs/unattended.md`, `docs/pregate.md`, checklists, specialists and bins | specialists by scope (as before), the native adversarial pass, the outside voice; the Red Team past 200 lines or after a CRITICAL |
| **gate** | code plus auth or migration scope, a hunk the gate scan tags (an RH rule: a relaxed threshold, a skipped test), a deleted code file, CI workflows, credential or state-root paths | code's set, plus the Red Team always, at full effort, and no hit-rate gating |
| **light**, runtime 0 | docs and tests only, or only registered outputs | the core checklist only; the outside voice when the diff publishes numbers |
| **light**, runtime 1–50 | `SMALL_DIFF_LINES` or fewer runtime lines | core, one **batched** specialist (testing, maintainability and security checklists, the specialists scope selects and the Red Team prompt in one context), the native adversarial pass (kept: backtest inconclusive, GSTA-147 D5) and the outside voice on numbers |
| **standard** | more than 50 runtime lines | the code set with every threshold (specialists at 50, simplification at 100, Red Team at 200) read on runtime lines; specialists leave docs and registered outputs out of their diff, and every specialist but Testing leaves tests out too; adversarial and outside voice read everything |
| **full** | gate class, or any classifier error | the gate set, unchanged: every reviewer on the whole diff, the Red Team always, no hit-rate gating |
**Numbers keep the outside voice.** On a docs or tests diff the outside voice is
skipped unless the diff publishes numeric claims: `gstack-review-plan` scans the
added lines of every path for percentages, money, multipliers, durations, token
or credit counts and score, rate or speed claims (`NUMBERS: yes`), and a
claim always dispatches it. The claims' fingerprint (`fingerprint=`) is passed
back as `--reviewed-numbers`; when a later fix changes a claim, only the
outside voice reruns, on the numbers.
A fixed blocking finding, a CRITICAL or an open blocker raises a light diff to
standard for the next step (`step=standard`): its specialists read at least 50 lines
and the Red Team runs, so a raise never dispatches less than light did. A fix-check
is the same at every depth.
**Fail closed.** Each of these makes the depth full and is listed in `errors[]` with
the input it could not read, a sanitized cause and the repair: a `gstack-diff-scope`
error, a gate scan that is unavailable, a base registry that cannot be read or is
invalid (`REGEN_REGISTRY_INVALID` or `REGEN_CONTAINMENT` in `codes=`), a numstat git
could not produce, and a binary code-class path (it is never counted as 0 lines).
**The registry rule.** Only the base commit's registry counts (the merge base's
`origin/<base>` commit for live /ship and the current base ref's commit for the queue: `origin/<base>` in `ready`, `<remote>/<base>` in `train build`), and only
entries with a `command`, which pregate's regen check proves for that tree. An entry
added on the branch never excludes anything; `command: null` entries, vendored code
and lockfiles stay runtime. To adopt it, commit an entry like this on the base branch:
```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", "*/sections/*.tmpl"] }
]
}
```
**Gate paths for the machinery itself.** An edit to `lib/review-depth.ts`,
`lib/review-collect.ts`, `bin/gstack-review-plan`, `lib/review-scope.ts`,
`lib/ship-queue*.ts`, `lib/regen.ts`, `.gstack/generated.json`,
`ship/sections/review-army.md.tmpl`, `ship/sections/adversarial.md.tmpl` or
`scripts/resolvers/review-army.ts` is gate class: changing how much review runs gets
the most review. `rules=` (`DEPTH_RULES`) hashes the deciding constants and patterns,
not file bytes, so a comment edit keeps it.
**`--depth-floor standard|full`** (a /ship prompt flag, passed to every plan run)
raises the depth and never lowers it.
**Numbers keep the outside voice.** At light depth the outside voice runs only when
the diff publishes numeric claims: `gstack-review-plan` scans the added lines of
prose and data files for percentages, money, multipliers, durations, token or credit
counts and score, rate or speed claims (`NUMBERS: yes`). The claims' fingerprint
(`fingerprint=`) is passed back as `--reviewed-numbers`; when a later fix changes a
claim, only the outside voice reruns, on the numbers. It always reads the whole diff.
**The adversarial and outside voices run once per invocation** (Step 11, on the
final tree), as the last full-pass plan lists them; after they ran, decision 2
@@ -166,6 +215,15 @@ default. `gstack-config set review_effort_tiers off` (or `--no-tiers`) runs
the audits and watchers at the host default too
([model-policy.md](model-policy.md#reasoning-effort)).
### Behavior changes
- A tests-only diff (runtime 0) no longer dispatches the Testing specialist: the
core checklist and the gate scan review it.
- Light no longer runs a separate Red Team: the batched specialist carries its
prompt. At runtime 0 light runs no adversarial pass either.
- Standard reads thresholds on runtime lines, not the whole diff, so a test-heavy
diff with few runtime lines dispatches fewer specialists.
## What the receipt records
`gstack-review-scope summary --since <REVIEW_SINCE>` reads this invocation's
@@ -190,6 +248,13 @@ specialist stats they record). The summary line appends `fix_checks=`,
`dispatches=` and `est_input_tokens=` only when they were recorded, so an old
loop prints the old line.
Beside the counters, `write` records `review_depth` (the depth, runtime lines,
rules hash, base and registry commits, and the gate scan bound to the head) and
the queue refuses a receipt below the depth its head needs
([ship-policy.md#ship-receipt](ship-policy.md#ship-receipt)). A stop the owner
accepted after a fix-check edit goes in `review_loop_exception`, never in prose
under `REVIEW_LOOP:`.
The receipt's summary line prints `passes=<n> blocking_found=<k> deferred=<m>`,
which is what a scorecard tracks (target: a median of 1 pass per PR). A
receipt carried across a base merge or a test-only fix
@@ -268,9 +333,10 @@ trailers, appended }`.
## Where this is enforced
- `scripts/resolvers/review-army.ts`: the floor and `NO FINDINGS` in every specialist and Red Team prompt (rendered into `review/sections/review-army.md` and `ship/sections/review-army.md`), and the lead judgment's "On record" line that turns a [ruled class](#rulings-below-the-floor) into a note; in `/ship` every specialist and Red Team prompt it renders carries its diff base as a literal sha the parent writes in (`DIFF_BASE=<diff-base>`: the merge base on pass 1, `RANGE_BASE` from pass 2 on), because a reviewer is its own process and never sees the parent's shell variables; the fan-out batches above.
- `ship/sections/review-army.md` Step 9: passes, `RANGE_BASE`, classify, defer, render, the cap check before any edit, the Step 11 late-defer row, scratch files under `.gstack/tmp/`, the cap decisions; `ship/sections/adversarial.md` Step 11: the outside voice reads the same range and scopes queued fixes the same way.
- `lib/review-depth.ts`, `bin/gstack-review-plan`: the diff class, the transition table, the numeric-claim scan and each role's effort tier (`test/review-depth.test.ts`, `test/review-plan-cli.test.ts`).
- `scripts/resolvers/review-army.ts`: the floor and `NO FINDINGS` in every specialist and Red Team prompt (rendered into `review/sections/review-army.md` and `ship/sections/review-army.md`), and the lead judgment's "On record" line that turns a [ruled class](#rulings-below-the-floor) into a note; in `/ship` every specialist and Red Team prompt it renders carries its diff base as a literal sha the parent writes in (`DIFF_BASE=<diff-base>`: the merge base on pass 1, `RANGE_BASE` from pass 2 on) beside its plan's `DIFF_CMD` excludes, because a reviewer is its own process and never sees the parent's shell variables; the fan-out batches above.
- `ship/sections/review-army.md` Step 9: the plan's `REVIEW_DEPTH`, `GATE_SCAN` and `--depth-floor` on every run, the depth fields of the review-log row, passes, `RANGE_BASE`, classify, defer, render, the cap check before any edit, the Step 11 late-defer row, scratch files under `.gstack/tmp/`, the cap decisions; `ship/sections/adversarial.md` Step 11: the outside voice reads the same range and scopes queued fixes the same way.
- `lib/review-depth.ts`, `bin/gstack-review-plan`: the diff class, the depth (`depthFor`, `DEPTH_RULES`, `depthOfCommitInput`), each dispatch's `exclude` pathspecs and `DIFF_CMD`, the transition table, the numeric-claim scan and each role's effort tier (`test/review-depth.test.ts`, `test/review-plan-cli.test.ts`).
- `lib/review-collect.ts`: `collectCommit` (git objects only, from any checkout) and `collectLive` (the working tree), per-path lines and the base commit's registry (`test/review-collect.test.ts`).
- `lib/review-scope.ts`, `bin/gstack-review-scope`: `classify`, `defer`, `render`, `summary` (with `fix_checks`, `dispatches`, `est_input_tokens`); refusals are `REVIEW_SCOPE_INPUT` and `REVIEW_SCOPE_RANGE` ([troubleshooting.md](troubleshooting.md#review-scope-input)).
- `lib/ship-policy.ts`: the `review_scope` key and its `ship: review_scope …` consequence line.
- `review/checklist.md`: `prereg-with-results` under Gate Integrity and `test-keyboard-count-loop` under Pass 2; `bin/gstack-coauthor-trailers` for the contributor-credit trailers of a port.
+159
View File
@@ -0,0 +1,159 @@
# /ship token budget and fresh-context handoffs
`/ship --token-cap 25M` (or `GSTACK_SHIP_TOKEN_CAP=25M`) bounds what one /ship run spends, and
long runs hand their heavy segments to fresh-context children. The helper is
`bin/gstack-ship-budget`; the skill's procedure is `ship/sections/budget-handoff.md`.
There is no default cap: without one, /ship meters usage when it can and prints it, and
nothing else changes.
## What the cap guarantees
It is an **admission cap**, not a hard ceiling. Before every step and every dispatch batch,
`check` admits the work only when
used + open reservations + unknown charges + projection + tail reserve + stop overhead <= cap
Exactly at the cap is admitted; one token over is not. A parallel review batch is reserved as
one sum before any of it starts. Admitted work can still cost more than its projection, and
gstack cannot stop a host mid-turn from bash, so a run can finish above the cap by the
misprojection of work already admitted. The tail reserve (default 2M, `--reserve <n>`) keeps
room for the receipt steps, and the stop overhead (calibrated) keeps room to stop cleanly.
## Quickstart
**Capy.** Read the root counter in one execute program that returns only that number:
```js
return (await tools.thread.get({ thread_id: "this" })).cost.tokens;
```
then
```bash
gstack-ship-budget start --record <record> --token-cap 25M --reading <n>
gstack-ship-budget check --run <id> --step 6 --next 7 --reading <n> [--prior-reading <n from your previous call>]
```
The counter rolls up every subagent of the thread, outside voices included; Capy exposes no
per-subagent counter, so other work in the same thread counts too (it only stops earlier).
**Claude Code.** `gstack-ship-budget start --record <record> --token-cap 25M --session <id>`. Without the
session id, `start --meter claude` prints a nonce: echo it once, then rerun with `--nonce <value>`.
The helper reads the session journal, its subagent journals and the journals of headless
children it launched; you never pass `--reading`.
**Anywhere else.** `--meter estimate` projects with the calibration and prints `meter=estimate`.
With a cap set and no readable meter, start refuses
([BUDGET_METER_UNAVAILABLE](troubleshooting.md#budget-meter-unavailable)).
## The BUDGET line
BUDGET: used=<n> reserved=<n> unknown=<n> admission_cap=<n|none> tail=<n> unit=<u> meter=<m> epoch=<n> call=<n|unknown> next=<id> projected=<n> verdict=continue|delegate|stop
| Field | Meaning |
|---|---|
| `used` | Metered usage since start (all epochs), or settled estimates under `meter=estimate` |
| `reserved` | Open reservations: admitted steps, started dispatches, and each open child's sub-cap counted as max(sub_cap, child_used), never both |
| `unknown` | Charges the meter cannot see (an external-CLI outside voice on Claude Code), counted at their reservation |
| `admission_cap` | The cap, or `none` |
| `tail` | Tail reserve plus stop overhead, held back from every admission |
| `unit`, `meter`, `epoch` | The meter contract: `capy-tokens` (root counter), `claude-usage-tokens` (journal usage), `estimate-tokens`; a meter change opens a new epoch |
| `call` | The per-call charge: two Capy readings in consecutive calls, or the last Claude Code call's usage |
| `next`, `projected` | What the check was for and its calibrated projection |
| `verdict` | `continue` (exit 0, prints `RESERVATION:` under a cap), `delegate` (exit 0), `stop` (exit 3) |
Any exit other than 0 means the next step does not start. Exit 2 is a usage or validation
error on stderr with the code, the value and the repair. GSTACK_RESULT appears only on a stop,
an incomplete close and a child's return; /ship's real terminal line stays Step 22's.
## Dispatches
Under a cap, `gstack-review-log --start` refuses a dispatch without an unspent, one-shot
reservation ([BUDGET_CHECK_SKIPPED](troubleshooting.md#budget-check-skipped)):
```bash
gstack-ship-budget check --run <id> --step 9 --next dispatch:specialist,red-team --reading <n>
gstack-review-log --start review --reservation <id>
```
Roles are review-depth's: `specialist`, `red-team`, `adversarial-native`, `outside-voice`,
`fix-check`. `--finish <token>` settles the reservation at the dispatch's completion receipt.
`close` audits after the fact: each review-log row of the run with no reservation gets one
BUDGET_CHECK_SKIPPED line and the run ends status=incomplete for the owner to accept
(`close --approved-by <who> --words "<their words>"`); a reservation is never created after the fact.
## Delegation
At start, a per-call charge of 100k or more delegates the whole /ship. Before Step 7 and before
Step 12, 150k or more (or an unknown charge) delegates the next segment (7–11.5, 12–22):
1. `gstack-ship-budget handoff --run <id> --from 12 --to 22 --base origin/<base> --reading <n>` writes a
brief (mode 0600, under the run directory) with the pins (HEAD, merge base, working tree,
reviewed tree), the record with its six headings and start tokens, the decisions in scope
with the owner's words, the decision trail, the budget and the return contract. It reserves
the child's sub-cap: min(cap − used − reserved − tail, the segment estimate), or `--sub-cap`.
2. The child is a Capy subagent on the shared machine, or on Claude Code
`gstack-ship-budget launch --in <brief>`: a background `claude -p` (resolved through lib/claude-bin.ts)
with explicit permission flags and the return file named.
3. The child runs `verify-handoff` first; a moved pin refuses
([HANDOFF_PIN_MOVED](troubleshooting.md#handoff-pin-moved)). It admits against its sub-cap
with child_used = reading − reading at handoff, is the only writer of the record, and never
delegates again: a delegate rule stops it. It ends with `return` (at most 40 lines).
4. `gstack-ship-budget reconcile --run <id> --return <file> --reading <n>`; idempotent per handoff hash,
so a duplicate return charges nothing twice. A refused, incomplete or interrupted child
exits 3: regenerate the handoff or stop.
`--delegate never` turns delegate into stop for hosts without children.
## Stop and resume
A stop prints
STOP: reason=budget_exhausted spent=<n> reserved=<n> unknown=<n> admission_cap=<n> next=<id> projected=<n>
RECOVER: /ship --resume <record> --token-cap <used + reserved + unknown + projected tail>
GSTACK_RESULT: skill=ship status=incomplete run=<dir>
Raising the cap is the owner's call; the resume journals who raised it and their words:
```bash
gstack-ship-budget resume --record <record> --token-cap <n> --approved-by Garry --words "raise it to finish" --reading <n>
```
Every command takes `--run` or `--record`; runs are never found by branch or directory, and a
/ship without `--resume` or `--continue` always starts a new run. The ledger and record are
mirrored to a private Drive checkpoint (`GSTACK_SHIP_BUDGET_CHECKPOINT_DIR`, else the
project-user Drive folder on Capy) bound to the run id and their hashes, so after a machine
change `resume --run <id>` restores both. A corrupt ledger
([BUDGET_LEDGER_INVALID](troubleshooting.md#budget-ledger-invalid)) is rebuilt from the
record's block with `--rebuild-from-record`; open reservations are then charged as unknown.
## Calibration
`lib/ship-budget-calibration.json` holds per-meter constants (per call, per dispatch, stop
overhead, calls and dispatches per step) fitted from metered runs: each constant is the
largest observation times 1.5, rounded up to 10,000 tokens. The leave-one-out test
(test/ship-budget-calibration.test.ts) covers the per-call constant only, the one observed
across three runs; per-dispatch has two observations and stop overhead one. Only the Capy
meter has observations; the Claude Code and estimate units are seeded from it until a
Claude-metered /ship completes.
## Token cap and USD spend cap
A lane's `--spend-cap` (USD) bounds paid gates and evals through lib/spend-ledger.ts; `--token-cap`
bounds the /ship run's model tokens through its own ledger. They are independent: set both
when a lane runs paid gates inside /ship, and neither converts into the other.
## Claiming savings
Savings count completed ships only. Preregister the comparison with `/eval-plan` (arms, bars,
stop rules) before claiming a saving from the cap or from delegation.
## Result codes
[BUDGET_CAP_REACHED](troubleshooting.md#budget-cap-reached) ·
[BUDGET_READING_MISSING](troubleshooting.md#budget-reading-missing) ·
[BUDGET_READING_REJECTED](troubleshooting.md#budget-reading-rejected) ·
[BUDGET_METER_UNAVAILABLE](troubleshooting.md#budget-meter-unavailable) ·
[BUDGET_LEDGER_INVALID](troubleshooting.md#budget-ledger-invalid) ·
[BUDGET_CHECK_SKIPPED](troubleshooting.md#budget-check-skipped) ·
[HANDOFF_PIN_MOVED](troubleshooting.md#handoff-pin-moved)
+119 -14
View File
@@ -221,8 +221,11 @@ It refuses (`QUEUE_NOT_READY`) when the working tree has tracked edits, HEAD
is not the PR's pushed head, CI at that exact head is not green, or the pregate
result is not a current pass, and (`QUEUE_RECEIPT_MISSING`,
`QUEUE_RECEIPT_STALE`) when the PR body carries no /ship receipt for that
head: /ship runs before every READY ([details](#ship-before-ready)). Send the
block as printed.
head: /ship runs before every READY ([details](#ship-before-ready)). It also
refuses `QUEUE_RECEIPT_DEPTH_RAISED` when the receipt's review depth is below
what the head needs and `QUEUE_RECEIPT_EXCEPTION_INVALID` when an owner
exception the receipt records does not stand ([the ship receipt](#ship-receipt)).
Send the block as printed.
**2. Lane behind another PR: gate ahead.** Gate the tree the queue will have
once the predecessor lands, while it is still open:
@@ -370,12 +373,17 @@ slot. The queue enforces this at every step from the PR body alone:
- `ready` refuses with `QUEUE_RECEIPT_MISSING` when the body has no valid
receipt and with `QUEUE_RECEIPT_STALE` when the receipt's `head` is not the
head being READY'd (or its `pr` is another PR); the informational
`SHIP_RECEIPT:` line still prints. There is no flag around it.
`SHIP_RECEIPT:` line still prints. It refuses `QUEUE_RECEIPT_DEPTH_RAISED`
(with a `REVIEW_DEPTH:` line) when the receipt records a review depth below
what the head needs, and `QUEUE_RECEIPT_EXCEPTION_INVALID` when an owner
exception it records does not stand ([the ship receipt](#ship-receipt)).
There is no flag around it.
- `train build` refuses a pin whose PR has no current receipt with the same
codes, naming the PR, before anything is written, pushed or dispatched,
`--dry-run` included.
- `verify --train` refuses the slot with the same codes before printing
`MERGE_CONDITION`.
- `verify --train` refuses the slot with `QUEUE_RECEIPT_MISSING` or
`QUEUE_RECEIPT_STALE` before printing `MERGE_CONDITION`; it does not
recompute the review depth.
**The stamp keeps the pin's receipt.** The slot's stamp commit changes only
stamp files (and, from slot 2 on, merges main into the pin), so no /ship runs
@@ -478,6 +486,9 @@ its record. Refusals, all before any spend: `QUEUE_TRAIN_UNTRUSTED` (a fork
head), `QUEUE_TRAIN_MOVED` (a head moved from its pin),
`QUEUE_RECEIPT_MISSING` and `QUEUE_RECEIPT_STALE` (the PR body has no /ship
receipt for the pin; see [/ship before READY](#ship-before-ready)),
`QUEUE_RECEIPT_DEPTH_RAISED` and `QUEUE_RECEIPT_EXCEPTION_INVALID` (the
receipt's review depth is below what the pin needs, or an owner exception it
records does not stand; see [the ship receipt](#ship-receipt)),
`QUEUE_NOT_READY` (no READY receipt at that head), `QUEUE_TRAIN_CONFLICT` (the printed
`QUEUE_TRAIN_CONFLICT: train <id> stopped at #<pr> (conflicts: <files>) after
merging #<a>,#<b>; nothing dispatched, no branch pushed; fix: …` line names
@@ -777,7 +788,9 @@ written by the pr-body section of `/ship` after the body is composed.
The ```` ```gstack-ship-receipt ```` block is the handoff record: identifiers
first (`pr`, `head`, `version`, `base`), then the evidence (`gate`, `ci`,
`pregate`, `issues`, `approval`, `docs`, `review_loop`, `carry`), validated
`head_tree`, `product_patch_id`, `gstack_version`, `review_depth`, `pregate`,
`issues`, `approval`, `docs`, `review_loop`, `review_loop_exception`, `carry`),
validated
against the `ship-receipt` schema (`gstack-artifact schema ship-receipt`) and
read back by `gstack-ship-receipt read`, `result`, `carry` and the queue.
@@ -812,6 +825,46 @@ unreadable base policy is `POLICY_SOURCE_UNAVAILABLE`); one
(`absent/<max> over` for a path the tree lacks), exit 1 on any `over`, whose
repair is trimming the file, never raising the cap in the same PR.
**`review_depth` and `gstack_version`.** `write` records the gstack that
wrote the receipt and the review depth at the checked-out head
([review-policy.md#review-depth](review-policy.md#review-depth)):
`{ depth, class, runtime_lines, rules, base, registry, gate_scan: { tagged, head }, reasons }`,
computed from git objects for the range against `origin/<base>`
(`gstack-diff-scope <base> --head <sha>` for the scope signals), with the gate
scan run on that head. `--head` must be the checked-out HEAD
(`RECEIPT_INVALID` otherwise), so the record and `head_tree` describe the
tree the review read; tracked edits in the checkout make the scan unavailable,
which records `full`. `gstack-ship-queue ready` and `train build` recompute
the depth the head needs from the same range and scope and print `REVIEW_DEPTH: receipt=<d> required=<d>
runtime_lines=<n> class=<c> reasons=<…> inputs=base@<sha>,registry@<sha>,gate_scan@<sha|missing>`;
a recorded gate scan counts only when its `head` is the full bound commit,
otherwise the scan is unavailable and the head needs `full`. A receipt below the required
depth, or one whose writer records `gstack_version` but no `review_depth`, is
[`QUEUE_RECEIPT_DEPTH_RAISED`](troubleshooting.md#queue-receipt-depth-raised):
the receipt is left unchanged and the fix is /ship on that head. A receipt with
neither field predates depth receipts and is accepted as legacy. A carry
recomputes the depth at the new head, rebinds the gate scan, and refuses when
the new head needs a deeper review than the receipt records (a test change that
skips a test raises the diff to gate class), when `--head` is not the
checked-out HEAD, when the receipt records `gstack_version` without
`review_depth`, or when an owner exception no longer stands, each as
[`RECEIPT_CARRY_REFUSED`](troubleshooting.md#receipt-carry-refused).
**`review_loop_exception`.** The owner's acceptance of a stop after a
fix-check edit under frozen scope (#3559), from `write --review-loop-exception
<json|@file>`: `{ accepted_by, date, via, reason, fix_check: { head, findings[, files] } }`.
It needs `--review-loop` with `fix_checks` of 1 or more, a `fix_check.head`
that is the receipt head or its ancestor, and, with `--body`, the line
`Review-loop exception: accepted by <owner> <date> via <ref>: <reason> (fix-check at <sha12>, <n> findings)`
word for word; otherwise
[`REVIEW_LOOP_EXCEPTION_INVALID`](troubleshooting.md#review-loop-exception-invalid)
and nothing is written. The queue checks both owner exceptions the same way
before READY and train build: the record must parse, the body must still
carry its line, and the fix-check head must be the bound head or its ancestor
(after a carry across rewritten history the old fix-check commit is off the
branch, so that check is skipped), or the receipt is
[`QUEUE_RECEIPT_EXCEPTION_INVALID`](troubleshooting.md#queue-receipt-exception-invalid).
**Prose against the receipt.** `gstack-ship-receipt read --pr <n> --check-prose`
(or `--body <file>`) compares every counter the body quotes with the receipt's
integers under `review_loop`, `gate` and `ci`: `passes=2`, `passes: 2` and
@@ -891,6 +944,32 @@ line, and the codes a refusal ends with are
<a id="receipt-carry"></a>
## Carrying a receipt across a base merge or a test-only fix
**Merge main into a reviewed branch; never rebase it.** What happens to the
receipt when the base moves:
| What changed since /ship | The receipt |
|---|---|
| a merge of the base, product diff unchanged (tests, release metadata, registered outputs the pregate's regen check proved) | carries; no review reruns |
| a merge of the base whose resolution changed product code | a fresh /ship (`RECEIPT_CARRY_PRODUCT_CHANGED`) |
| a rebase or force-push onto a tree the receipt covers, product diff against the new base unchanged | carries (`carry.ancestry: rewritten`) |
| any other rebase or force-push | a fresh /ship (`RECEIPT_CARRY_TREE_MISMATCH`) |
| a stacked PR whose parent landed (squash or merge), retargeted to the base with the base merged in, HEAD's product diff exactly the one reviewed against the parent branch | carries (reason `stack parent <branch> landed`) |
**Stacked PRs (#3570).** A child PR's receipt is written against its parent
branch, so its `base` names that branch and its `product_patch_id` is the
child's own range. After the parent lands, the PR is retargeted to the base, and
/ship's carry check runs with `--base <base>`. When that base differs from the
receipt's, the carry compares the reviewed range (the recorded
`product_patch_id`, or for an older receipt the range against
`origin/<parent>` while that branch still resolves) with HEAD's whole product
diff against the base, computed the way `write` records it, so a parent's
release-metadata change that never reached the base counts too. Equal means
the parent fully landed and the child is unchanged, so the receipt carries
and from then on names the base it was carried to. A parent that has not landed, a child edited
since, or a parent that landed with changes the child never merged all leave a
different diff and refuse with `RECEIPT_CARRY_PRODUCT_CHANGED`. An older
receipt whose parent branch is gone refuses with `RECEIPT_CARRY_REFUSED`.
Since the release that added `review_scope`. A `/ship` receipt names the head it
reviewed, so a conflict-only merge of the base or a test-only fix used to
invalidate it and cost a full re-run on identical product code.
@@ -921,11 +1000,22 @@ newer tentative one the stamp moved (a `*_KEY_VERSION` written as the next
minor, #3512; a version older than the old one or of another width is a hand
edit). A logic change in a
stamped source file is product. Lockfiles are product unless the policy names
them. A base merge contributes no product
change of its own. A receipt carried twice keeps the reviewed head as
them. An output of an entry with a `command` in origin/<base>'s
`.gstack/generated.json` is `generated`, not product, only when the
`--pregate` result passed for HEAD lists it under the `regen` check's
`proven` paths (the check deleted the touched output in its scratch worktree
and the command recreated it byte-identically) and that check ran the same
registry, origin/<base> at the commit the carry reads. A file that matches an
output glob but that the command never writes is not proven. Without that
evidence, or for an entry with no command or one only the branch registers,
the output is product (U1 on gstack-evals paid a
full rerun because a merge changed its generated index's diff). A base merge
contributes no product change of its own. A receipt carried twice keeps the reviewed head as
`carry.from` and adds up the merges and paths. The carried receipt records
`carry: { from, reason, product_patch_id, changed_paths, merge_commits,
range_diff, covers, reruns }` with the `git range-diff` evidence, prints
range_diff, covers, reruns, ancestry, trees }` (`ancestry` is `linear`
along the receipt's own history, `rewritten` once any hop was a rebase;
`trees` lists the earlier covered trees) with the `git range-diff` evidence, prints
`CARRY: carried from <old> to <new> (<reason>)` and the `GSTACK_RESULT:
skill=ship status=complete` line, and its summary line ends with
`carried_from=<old12>`. `CARRY: current` means the receipt already names HEAD;
@@ -933,10 +1023,25 @@ skill=ship status=complete` line, and its summary line ends with
A changed product path, or a product patch-id that differs, is
[`RECEIPT_CARRY_PRODUCT_CHANGED`](troubleshooting.md#receipt-carry-product-changed)
(exit 3; the refusal names the paths), and a receipt whose head is not in HEAD's
history (a rebase or force-push) or a working tree with uncommitted changes is
[`RECEIPT_CARRY_REFUSED`](troubleshooting.md#receipt-carry-refused): run `/ship`
on the new head. The queue's `ready`, `train build` and `verify --train` read the
(exit 3; the refusal names the paths), and a working tree with uncommitted
changes is [`RECEIPT_CARRY_REFUSED`](troubleshooting.md#receipt-carry-refused).
**Rewritten history.** `write` records `head_tree` (the reviewed head's tree)
and `product_patch_id` (its product patch against the base), and every carry
rebinds both to the new head and keeps the earlier trees in `carry.trees`.
When the receipt's head is no longer in HEAD's history, the carry follows only
when HEAD's tree is one of those trees and HEAD's product patch-id against the
current base equals the recorded one; it records `carry.ancestry: rewritten`
and `range_diff: tree-equal`, and needs neither the old commit nor its
history, so a fresh clone carries too. The tree alone is not enough: a stacked
branch rebased onto main after its base closed unmerged keeps its tree while
its diff grows by the base's code, and that is `RECEIPT_CARRY_PRODUCT_CHANGED`.
Any other tree is
[`RECEIPT_CARRY_TREE_MISMATCH`](troubleshooting.md#receipt-carry-tree-mismatch),
which names HEAD's tree and the covered trees. A receipt from before
`head_tree` existed, whose head is not in this clone, is
`RECEIPT_CARRY_REFUSED`: run `/ship` on the new head. A refusal never
rewrites the body. The queue's `ready`, `train build` and `verify --train` read the
carried receipt like any other.
**Delta /ship: eligibility, then checks, then publication.** `/ship` Step 1 runs
@@ -995,7 +1100,7 @@ or the order moved, the result is `QUEUE_STALE` (exit 3) with the exact
Stages, each journaled in `.gstack/tmp/restamp-journal.json`:
0. **restamp_command** (only when the policy sets it): the repo's own restamp, see [restamp_command](#restamp-command).
1. **version**: `gstack-version-bump write --version <X>` in the staging worktree (VERSION, the manifest, npm lockfiles); skipped when `restamp_command` owns the version.
1. **version**: `gstack-version-bump write --version <X>` in the staging worktree (VERSION, the manifest, npm lockfiles); skipped when `restamp_command` owns the version. The manifest gets the npm-valid three-part form, except when the policy lists it in `stamp_paths` or `stamp_files` and its copy at the base holds four parts (gbrain's convention): then it keeps the full version (`--manifest-form full`) and the stage detail says so.
2. **release_tool**: the policy command, under the contract below.
3. **changelog**: the PR's `[Unreleased]` or old-version section moves to the top under `<X>`, above its predecessor's entry. A duplicate heading the PR introduces (`CHANGELOG_DUPLICATE_HEADING`: the headings are counted against the base's CHANGELOG at `origin/<base>` and at the fork point, so a version the base already heads twice passes as long as the PR adds no further copy) and a top entry that is not `<X>` (`CHANGELOG_TOP_MISMATCH`) are errors; the section being re-headed itself may exist only once.
4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites, plus every `stamp_patterns` template, within `stamp_paths` and `stamp_files`. A quote around X in the first two forms is part of the form and is kept as written: `since: 'X'`, `since: "X"`, `(was 'X')` and `(was "X")` move like the bare ones (the shape of a quoted source row such as gbrain's `{ since: '0.60.468.0', … }`), an opened quote must close right after X (`since: '1.2.3-beta'` and `since: '1.2.3"` are not the form, in the writer and in the tree receipt alike), and `vX` already accepts a quote before the `v`. Which X moves is decided by provenance. The branch's own tentative stamp (its VERSION when it moved past base) and `--was <X>`, when given, are each rewritten on every line of every stamp file. Beyond that, only lines the branch itself added are read, by their position in the file (`git diff --unified=0` from the fork point), so a base line with the same text, or a CRLF checkout whose diff is LF-normalized, is told apart: a stamp-form mention there of a version newer than the version at the branch's fork point is a tentative stamp and moves (this is what finds a quoted row after a lost slot and a merge of the base, since the base's release is newer than the fork point), and when the branch has no stamp of its own because its VERSION equals the base's and its fork point is the base itself (a branch rebased onto the base after it reached X, or one never stamped) a mention of the base version on a line it added is its stale stamp and moves too, while the same version on a line the base already carries is the base's release and stays. The stage detail names both counts per file (`VERSION equals base X, so X moved only on lines this branch added and stayed on the base's own lines (<file>: N moved, M kept)`), so the stamp commit shows which lines moved and why. A line the base added since the fork point is the base's even when that diff shows it (a merge of the base on the branch, committed or still staged, brings every line the base added since the fork into the diff from the fork point, main's own release keys included): it is left out, the detail says `N line(s) the base added since the fork point are the base's, not this branch's, and stay (<file>: N)`, and a base diff git cannot run refuses the stage (`POLICY_SOURCE_UNAVAILABLE`, `cannot diff <since>..<base> for the stamp_paths stage`) rather than reading as an empty set that would move the base's lines again. The rule reads line ownership, not meaning: a branch-added line inside `stamp_paths` that means the base's own release in a stamp form (`Requires vX or later` written on a branch forked at X) moves with it, which the `--dry-run` diff and the per-file counts show before anything is written; write such a reference outside a stamp form, or keep that file out of `stamp_paths`. `--was X` is the opposite tool: it rewrites X everywhere, the base's own lines included, and re-heads the CHANGELOG section `## [X]`. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate.
+4 -2
View File
@@ -906,8 +906,10 @@ gstack's own templates and for any skill folder you own.
- **Drift.** `test/skill-bin-drift.test.ts` fails when a template or rendered skill calls a gstack bin or flag that
does not exist (`bun run scripts/skill-bin-drift.ts --skill <name>` for one skill). Its allowlist only shrinks.
- **Full diet.** Report first, write the eval tasks before any cut
(`test/fixtures/skill-evals/<skill>/tasks.json`), cut, then prove before vs after vs no skill with the shared skill
eval (`docs/designs/SKILL_EVAL_CONTRACT.md`). A diet ships only when no task that passed before fails after.
(`test/fixtures/skill-evals/<skill>/tasks.json`), lock them (`gstack-evals skill-eval --lock`, then commit
`tasks.lock.json`; the shared eval refuses a paid run without it), cut, then prove before vs after vs no skill with
the shared skill eval (`docs/designs/SKILL_EVAL_CONTRACT.md`). A diet ships only when no task that passed before
fails after.
`/write-skill` is not `/skillify`: `/skillify` codifies a browser scrape into a browser-skill. Adapted from
[nicobailon/skill-diet](https://github.com/nicobailon/skill-diet) (MIT). The first diet, /learn, cut its template from
+312 -26
View File
@@ -1566,11 +1566,19 @@ process has exited never blocks, and neither does a stale one: each lease
records its process's start identity, so a pid reused after a reboot or a
machine wake (GBRA-105's lease named pid 379, which had become dbus-daemon)
is reclaimed instead of blocking. The upgrader's own session (the pid chain it
runs under) never counts.
runs under) never counts for `bin/gstack-upgrade` run by hand. A Capy install
in install mode (`bin/gstack-capy-session`) counts it while that lease is under
120 minutes old: an install run from inside a workflow that holds the lease
(/ship's docs step, #3571) prints `SELF_UPGRADE: skipped (a gstack run in this
session holds a workflow lease on this installation: pid <p> (<age>m
<command>), so the upgrade waits until the lease is 120 minutes old; fix:
<checkout>/bin/gstack-upgrade once the run ends (UPGRADE_BUSY))` and leaves the
checkout where the run started. Each `gstack-skill-start` renews the lease.
**Fix.** Wait for those sessions to finish (`bin/gstack-lease list` shows
every lease with its age and `alive|dead|stale`; a holder is named as
`pid <p> (<age>m <command>)`), then re-run `bin/gstack-upgrade`.
`pid <p> (<age>m <command>)`), then re-run `bin/gstack-upgrade`. For the
session's own run, run the printed `bin/gstack-upgrade` after the run ends.
When you know they are idle, `GSTACK_UPGRADE_FORCE=1 bin/gstack-upgrade`
proceeds. At a Capy session start the same refusal appears as
`SELF_UPGRADE: skipped (live gstack sessions hold this installation: ...)`,
@@ -1729,8 +1737,11 @@ restate them. Rotate the secret if it was real.
**Meaning.** The record is kept per branch (one Capy thread ships one branch), and
HEAD is detached or the working directory is not a git repository. Exit 3.
From `import --autoplan <dir> --stack <b1,b2,...>` it means a listed branch exists
neither locally nor on origin; the line names it, and no branch's record was written.
**Fix.** Check out the branch the work ships from, or pass `--branch <name>`.
**Fix.** Check out the branch the work ships from, or pass `--branch <name>`. For
`--stack`, correct or fetch the named branch and import again.
<a id="approval-record-unavailable"></a>
### `gstack-approvals: ... (APPROVAL_RECORD_UNAVAILABLE)`
@@ -1769,7 +1780,9 @@ import again. Imports are idempotent, so a retry never records an answer twice.
### `gstack-owner-brief render --captain: ... (CAPTAIN_BRIEF_INCOMPLETE)`
**Meaning.** The plan is missing at least one section the brief needs: `## Before → after`,
`## Why it matters`, `## What caused this work`, `## Big decisions`. The brief still prints,
`## Why it matters`, `## What caused this work`, `## Big decisions`, and `## ELI5` (alias
`## In plain words`) on a run whose `gate.json` carries an `eli5` field (gstack 1.91.305 and
later; older runs render as before). The brief still prints,
but it opens with "Not ready for approval" and has no reply invitation. Exit 1.
**Fix.** Add the named sections to the plan (`gstack-autoplan revise --plan <file>` if
@@ -1810,6 +1823,92 @@ all the command prints `SHOT_PNG: unavailable (...)` and exits 0 instead.
**Fix.** Screenshot `shot.html` with your own browser at 880 px wide, or use `text.md`.
## /ship token budget and fresh-context handoffs (gstack-ship-budget)
The flow, the BUDGET line field by field and the meters are in [ship-budget.md](ship-budget.md).
Exit 2 lines go to stderr; exit 3 lines (a stop) go to stdout with the STOP, RECOVER and
GSTACK_RESULT lines.
<a id="budget-cap-reached"></a>
### `gstack-ship-budget: ... (BUDGET_CAP_REACHED)`
**Meaning.** The next step or dispatch batch would push used + open reservations + unknown
charges + its projection + the tail and stop reserve past the admission cap, so it did not
start. Exit 3. `gstack-review-log --start` prints the same code (exit 1) for a dispatch
after the run stopped. The cap admits work; it cannot stop a turn already running, so the
overshoot is bounded by the misprojection of work admitted before the stop.
**Fix.** The run ended status=incomplete with a resumable record. Raising the cap is the
owner's call: run the printed `RECOVER:` line; its resume needs
`--approved-by <who> --words "<their words>"`.
<a id="budget-reading-missing"></a>
### `gstack-ship-budget: ... (BUDGET_READING_MISSING)`
**Meaning.** The run's meter is the Capy root counter, a cap is set, and the command got no
`--reading`. Exit 2. No gstack binary can call Capy's API, so the agent reads it.
**Fix.** In one execute program return only
`(await tools.thread.get({ thread_id: "this" })).cost.tokens` and pass it as `--reading <n>`.
<a id="budget-reading-rejected"></a>
### `gstack-ship-budget: ... (BUDGET_READING_REJECTED)`
**Meaning.** The reading breaks the run's meter contract: it is lower than the last reading
of the same epoch, `--prior-reading` is above `--reading`, or `--reading` went to a meter that
reads its own source (Claude Code journals, the estimate). Exit 2; nothing changed.
**Fix.** Pass the counter the run started with (the BUDGET line names meter, unit and epoch).
A different meter opens a new epoch that keeps prior usage:
`gstack-ship-budget resume --run <id> --meter <m>`.
<a id="budget-meter-unavailable"></a>
### `gstack-ship-budget: ... (BUDGET_METER_UNAVAILABLE)`
**Meaning.** A cap is set and no meter is readable: no `--reading`, no bound Claude Code
session, no `--meter estimate`; or a `--session`/`--nonce` matched zero or several journals.
The line names what it tried. Exit 2.
**Fix.** Capy: `--reading <n>`. Claude Code: `--session <id>`, or echo the nonce the line prints
once and rerun with `--nonce <value>`. Elsewhere, with the owner's consent, `--meter estimate`.
<a id="budget-ledger-invalid"></a>
### `gstack-ship-budget: ... (BUDGET_LEDGER_INVALID)`
**Meaning.** The ledger (`<state root>/ship-runs/<run>/budget.json`), the record's
gstack-budget block, a handoff brief's block or a checkpoint is missing, unparseable, from
another schema version, or not bound to the run (identity or sha256 mismatch). The line names
the path, the schema version and the parse location. Exit 2.
**Fix.** After a machine change, `gstack-ship-budget resume --run <id>` restores the ledger and
record from the Drive checkpoint. Otherwise rebuild:
`resume --record <path> --rebuild-from-record --reading <n>`; open reservations are charged as unknown.
<a id="budget-check-skipped"></a>
### `gstack-review-log: ... (BUDGET_CHECK_SKIPPED)` / `gstack-ship-budget: ... role=<r> step=<s> started=<t> spend=unknown (BUDGET_CHECK_SKIPPED)`
**Meaning.** Under a cap, `gstack-review-log --start` got no reservation id, an id from another run
or a step reservation (mismatched), or an id already used (one-shot); it refused before the
dispatch (exit 1, nothing captured). From `close`: a review-log row of this run had no
reservation before it started; one line per dispatch, and the run ends status=incomplete (exit 3).
**Fix.** Run `gstack-ship-budget check --run <id> --step <s> --next dispatch:<roles>` before the
batch and pass the printed id once with `--reservation`. A reservation is never created after the
fact: the owner accepts the unreserved spend with `close --approved-by <who> --words "<their words>"`
or the dispatches rerun under reservations.
<a id="handoff-pin-moved"></a>
### `gstack-ship-budget: ... (HANDOFF_PIN_MOVED)`
**Meaning.** `verify-handoff` found the brief no longer matches its tree: HEAD, the merge base
with the base ref, the working-tree fingerprint (`gstack-wtree`) or the reviewed tree moved, or the
brief's sha256 is not the one the parent recorded. Each line names the pin with its expected
and actual value. Main moving alone keeps the merge base and never refuses. Exit 3; the child
wrote its return with status=refused.
**Fix.** The parent reconciles the refusal and regenerates the handoff at the current tree
(`gstack-ship-budget handoff ...`) or stops.
## Browser
<a id="browse-runtime-version-skew"></a>
@@ -2388,6 +2487,14 @@ and an E.164 `+14155551234` still report, so the finding means the id stands
without a run word (`the id is 40269992077`, `40269992077 failed`) or follows
a word that is not one (`see 40323989742`).
Separately, the scanner skips a bare number that is exactly a power of two
from 2^30 through 2^49, one less than such a power, or one of eleven 32-bit
hash and PRNG constants (Knuth's multiplicative hash, the golden-ratio
constant, the glibc and Numerical Recipes LCGs, FNV-1a, MurmurHash2 and 3, the
MT19937 seeder and the largest 32-bit prime; `isNumericConstant` in
`lib/redact-patterns.ts`). Only the exact digits with no separator are exempt:
the same digits with a `+`, spaces, dashes, dots or parentheses still report.
**Fix.** Write the id next to a run word (`run 40269992077`) or as an
`/actions/runs/` link. For anything else, rerun with `--confirm <token>`
using the `TOKEN:` line the refusal printed; MEDIUM findings do not block
@@ -2409,11 +2516,20 @@ holding only its first section therefore no longer wipes the rest of the PR.
The redaction scan runs on the merged bytes, so a MEDIUM or HIGH finding can
come from a kept section; the `RULE:` line's `LINE:` is in the merged text.
When the current body cannot be read (`gh pr view --json body` fails) nothing
is posted, exit 3.
is posted, exit 3. When the current body holds a title the file rewrites more
than once (an earlier description pasted below a `---` under the new one, as
on gstack-evals#19), the file's section matches no single copy, so nothing is
posted, exit 64: `gstack-post: the PR's current body holds "What changed" more
than once, so the file's copy matches no single section; nothing was posted.
Rerun with --replace and the whole body`. Merging would replace one copy and
keep the stale other beside it.
**Fix.** Nothing, when the kept sections are wanted. To overwrite the whole
body with the file, rerun with `--replace`. To drop a section, post the body
with `--replace` or edit it on the host.
with `--replace` or edit it on the host. For the duplicate-title refusal, write
the whole body you want (keep the earlier description under a title of its own,
such as `## Original description`, if it should stay) and post it with
`--replace`.
<a id="redact-prepush-email"></a>
### `MEDIUM pii.email <file>:<line>` from the pre-push hook
@@ -2428,6 +2544,15 @@ An address only another remote knows still reports. If the hook also printed
`existing-email suppression was limited for this push`, it could not read the
history in 5 seconds, so known authors may be listed too.
A filename with an `@` in the package@version style is not an address and
never reports: the part after the `@` is a version or tag (one dotless label
or a dotted version, never a domain name) followed by file extensions no
top-level domain uses, or a dotted all-digit version followed by
an extension that doubles as a top-level domain (`looksLikeAtFilename` in
`lib/redact-patterns.ts`). A file named after a real address, an address at an
all-digit domain and an address whose top-level domain doubles as a file
extension still report.
**Fix.** Remove the address, or allow it for this repo:
`git config --add gstack.redact.allowEmail <address>`. If it is your own
address, check `git config user.email`. The allowlist only covers `pii.email`;
@@ -2900,10 +3025,12 @@ for an older PR, write it once by hand with the same command.
### `RECEIPT_INVALID`
**Meaning.** The block exists but does not validate against the `ship-receipt`
schema (`gstack-artifact schema ship-receipt`).
schema (`gstack-artifact schema ship-receipt`). `gstack-ship-receipt write`
also refuses with this code, and writes nothing, when `--head` is not the
checked-out HEAD: the receipt binds the tree the review read.
**Fix.** Regenerate it with `gstack-ship-receipt write`; never hand-edit the
block.
block. For the `--head` refusal, check out that commit and write there.
<a id="execution-unknown"></a>
### `EXECUTION_UNKNOWN`
@@ -3150,7 +3277,10 @@ cannot be downgraded; every other check id in `warn` is honoured only from the
base branch's copy or the user-owned file. For the user-owned file, edit it
directly at the path the message names. A user file inside the repository, or
reached through a symlink into it, is ignored rather than read (`--explain`
names it).
names it). An unknown `sealed` key that a newer gstack defines (such as
`allow_paths` or `case_insensitive`) means this machine's gstack is older than
the policy: upgrade gstack here, and raise `min_gstack_version` in the
repository's `.gstack/ship-policy.json` before landing such a key.
<a id="pregate-secrets"></a>
### `PREGATE_SECRETS`
@@ -3240,6 +3370,27 @@ specifiers in test files that resolved to no repo file (for example a
a test does cover the file, declare it under `dependencies`; if the resolver
should have found it, report the specifier as a resolver gap.
<a id="pregate-no-lane-own-policy"></a>
**The branch adds the lane itself.** A branch that edits `.gstack/pregate.json`
to give its own new files a lane still gets this error: pregate reads the policy
from `origin/<base>` (or the user-owned file), never from the branch, so a
branch cannot loosen its own gate ([trust boundary](pregate.md#trust-boundary)).
That is intended, and pregate never switches to the branch's copy on its own.
The fix line then opens `this branch edits .gstack/pregate.json, but the policy
comes from origin/<base>@<sha>:.gstack/pregate.json so a branch cannot loosen
its own gate, and a lane it adds counts only once it is on <base>` and names the
two routes:
1. Land the policy change in its own PR first (the reviewed route), then merge
the base into this branch and rerun.
2. Rerun with `--policy-from head --allow-repo-commands`. The run reads the
branch's copy and its record says `policy head (unreviewed)`, so a
reviewer sees that the gate ran under a policy nobody has reviewed yet;
`gstack-ship-queue` never accepts a head-policy run as a remote mirror.
An allowlist entry the branch adds to its own `pregate.json` is read the same
way: it clears a finding only once it is on the base.
<a id="pregate-lane-failed"></a>
### `PREGATE_LANE_FAILED`
@@ -3656,7 +3807,11 @@ finding: rule, `file[:line]`, commit. They never print the matched text.
**Fix.** Remove the output from every published commit before the branch
reaches the public repository (a later commit that deletes it still publishes
it). A reviewed false positive gets one line in the base branch's
it). A publishable file kept next to sealed data (a `MANIFEST.json` of hashes
and counts) can be exempted from the path rules with a `sealed.allow_paths`
glob in the base branch's `.gstack/pregate.json`; the text rules still read it
([docs/pregate.md#sealed](pregate.md#sealed)). A reviewed false positive gets
one line in the base branch's
`.gstack/sealed-allow.tsv`: `rule<TAB>path<TAB>sha<TAB>reason`. A private
destination sets `"visibility": "private"` under `sealed` in the base branch's
`.gstack/pregate.json`. Capy's pull-request tool may not run local hooks, so
@@ -4434,6 +4589,39 @@ written for the pinned head is the binding one and `verify --train` accepts it.
`gstack-ship-queue ready --pr <n> --screen|--no-screen`. A lane never stamps
to fix this: the stamp comes only when the slot is called.
<a id="queue-receipt-depth-raised"></a>
### `QUEUE_RECEIPT_DEPTH_RAISED`
**Meaning.** `ready` or `train build` recomputed the review depth the head
needs from git (the range against the base ref, `origin/<base>` in `ready`
and `<remote>/<base>` in `train build`, the base commit's
generated-output registry, and the receipt's gate scan when it names this
head) and the receipt's `review_depth.depth` is below it, or the receipt comes
from a gstack that records `gstack_version` but carries no `review_depth`. The
`REVIEW_DEPTH:` line prints the receipt's depth, the required depth, runtime
lines, the class, the reasons and the inputs (`base@`, `registry@`,
`gate_scan@` or `missing`). A gate scan recorded for another head counts as
missing, which makes the head need `full`. The receipt is unchanged.
**Fix.** Run /ship on the head the READY names; it reviews at the depth
`gstack-review-plan` plans for that head and writes a receipt that records it.
Never edit `review_depth` by hand ([review-policy.md](review-policy.md#review-depth)).
<a id="queue-receipt-exception-invalid"></a>
### `QUEUE_RECEIPT_EXCEPTION_INVALID`
**Meaning.** The receipt records an owner exception (`docs.exception` or
`review_loop_exception`) that the queue cannot accept: the record does not
parse, a review-loop exception names no fix-check (`review_loop.fix_checks`
is 0 or missing), its `fix_check.head` is neither the bound head nor an
ancestor of it (not checked after a carry across rewritten history), or the PR
body no longer carries the exception's line word for word
(`Documentation exception: …` or `Review-loop exception: …`).
**Fix.** Restore the line in the PR body exactly as `write` printed it, or run
/ship again and record the owner's acceptance with `--docs` or
`--review-loop-exception` ([ship-policy.md](ship-policy.md#ship-receipt)).
<a id="queue-screen-stale-ref"></a>
### `QUEUE_SCREEN_STALE_REF`
@@ -4694,29 +4882,78 @@ rebased; /ship's own rule is to merge the base in, never rebase a reviewed head.
### `RECEIPT_CARRY_REFUSED`
**Meaning.** `gstack-ship-receipt carry` could not carry the PR's /ship receipt
to HEAD: the PR body has no valid receipt, the receipt's head is not an ancestor
of HEAD (a rebase or force-push dropped it), the working tree has uncommitted
or untracked changes (a carry certifies a commit, never a tree that differs from
it), or git could not compare the two product diffs or read the working tree's
status. The receipt in the body is unchanged and still names the old head, so
`gstack-ship-queue ready` refuses `QUEUE_RECEIPT_STALE`.
to HEAD: the PR body has no valid receipt, the working tree has uncommitted or
untracked changes (a carry certifies a commit, never a tree that differs from
it), the receipt's head left HEAD's history and the receipt records no tree to
compare (a receipt written before `head_tree`, whose head is not in this
clone), the PR was retargeted to the base after its stack parent landed but
the receipt records no `product_patch_id` and `origin/<parent>` no longer
resolves, so the reviewed range cannot be recomputed, or git could not
compare the two product diffs or read the working tree's status. It also refuses when an owner exception the receipt records
no longer stands, and, for a receipt from a gstack that records review
depth, when HEAD needs a deeper review than the receipt records, `--head` is
not the checked-out HEAD, or the receipt records `gstack_version` without
`review_depth` ([ship-policy.md](ship-policy.md#ship-receipt)). The receipt in
the body is unchanged and still names the old head, so `gstack-ship-queue
ready` refuses `QUEUE_RECEIPT_STALE`.
**Fix.** Keep the reviewed head in HEAD's history (merge the base in instead of
rebasing), commit or stash every working-tree change, then rerun
**Fix.** Commit or stash every working-tree change, then rerun
`gstack-ship-receipt carry --pr <n>` at the new head. When the history is
already rewritten, run /ship on the new head.
already rewritten and the receipt predates `head_tree`, run /ship on the new
head; next time merge the base in instead of rebasing. For a retargeted stacked
PR whose older receipt lost its parent branch, or a depth or exception
refusal, run /ship on the new head. When `--head` is not the checked-out HEAD,
check it out and carry there.
<a id="review-loop-exception-invalid"></a>
### `REVIEW_LOOP_EXCEPTION_INVALID`
**Meaning.** `gstack-ship-receipt write --review-loop-exception` refused the
record and wrote nothing: it is not `{ accepted_by, date, via, reason,
fix_check: { head, findings[, files] } }`, `--review-loop` is missing or counts
no fix-check, `fix_check.head` is neither the receipt head nor its ancestor,
or the `--body` file lacks the `Review-loop exception: …` line.
**Fix.** Record the owner's exact words and the fix-check the stop followed,
pass the `gstack-review-scope summary` file as `--review-loop`, put the printed
line in the PR body, and write again
([ship-policy.md](ship-policy.md#ship-receipt)).
<a id="receipt-carry-tree-mismatch"></a>
### `RECEIPT_CARRY_TREE_MISMATCH`
**Meaning.** The branch was rebased or force-pushed after /ship, so the
receipt's head is no longer in HEAD's history, and HEAD's tree is not a tree
the receipt covers (its `head_tree` or a tree in `carry.trees`). No review saw
this code. The refusal names HEAD's tree and the covered trees; the body is
unchanged.
**Fix.** Run /ship on the new head. A rebase carries only onto a reviewed tree
whose product diff against the new base is unchanged; merging the base in
instead keeps every later carry available
([ship-policy.md](ship-policy.md#receipt-carry)).
<a id="receipt-carry-product-changed"></a>
### `RECEIPT_CARRY_PRODUCT_CHANGED`
**Meaning.** Between the receipt's head and HEAD the product diff against the
base changed: at least one path that is neither a test, release metadata nor
part of the base merge has a different patch than the reviewed head had. The
reviewed verdict does not cover HEAD, so the receipt is not carried. The
refusal names the changed product paths.
base changed: at least one path that is neither a test, release metadata, a
registered output the pregate's regen check proved (listed under `proven`,
checked against the same base registry), nor part of the base merge has
a different patch than the reviewed head had. After a rebase it means HEAD has
the reviewed tree but the base moved under it, so the diff a reviewer would see
is not the reviewed one. For a stacked PR retargeted to the base after its
parent landed, it means HEAD's product diff against the base is not the range
reviewed against the parent branch: the parent has not fully landed, or the
branch changed since. The reviewed verdict does not cover HEAD, so the
receipt is not carried. The refusal names the changed product paths, or for a
retargeted stacked PR the parent branch and the base.
**Fix.** Run /ship on the new head. The receipt carries only across a
conflict-only base merge, a test-only change or release metadata, which the
conflict-only base merge, a test-only change, release metadata, a registered
output the pregate's regen check proved, a rebase onto a covered tree with
the recorded product diff, or a stacked PR's retarget once its parent fully
landed on the base, which the
`carry` object in the carried receipt records with the product patch-id and
the range-diff evidence ([unattended.md](unattended.md#the-terminal-line)).
@@ -5097,7 +5334,7 @@ The name is not one of openclaw, hermes, dots, generic, qm, muse or grok. For a
<a id="harness-mode-unsupported"></a>
### `gstack-harness: Hermes does not support --skills router` (HARNESS_MODE_UNSUPPORTED)
Hermes opens only skills it has indexed, so a router that points at unlisted files would not load. Drop `--skills` to take the harness's default (Hermes: all; OpenClaw, dots and generic: router).
Hermes lists in its skills index only the skills registered as folders in its skills tree, so a router would leave every other gstack skill out of the listing the model chooses from (Hermes's `skill_view` can still load an unlisted skill by name, but nothing tells the model it exists). Drop `--skills` to take the harness's default (Hermes: all; OpenClaw: hybrid; dots and generic: router). OpenClaw takes router, hybrid or all.
<a id="harness-unsafe-destination"></a>
### `gstack-harness: refusing to write into <dir>: <why>` (HARNESS_UNSAFE_DESTINATION)
@@ -5109,7 +5346,12 @@ The destination is your home folder, the filesystem root, inside the gstack chec
gstack only replaces files its ledger says it wrote. A folder or a skill with the same name is yours (or another tool's), and harnesses resolve skills by name, so gstack stops instead of shadowing it. Rename or move that entry, then register again.
The same code stops `register generic --context <file>` when the file holds a `<!-- gstack:begin harness-routing -->` or `<!-- gstack:end harness-routing -->` line without exactly one partner, or a gstack block someone edited. Delete those marker lines (and the lines between them, once you have kept what you want), then register again.
The same code stops `register <harness> --context <file>` when the file holds a `<!-- gstack:begin harness-routing -->` or `<!-- gstack:end harness-routing -->` line without exactly one partner, or a gstack block someone edited. Delete those marker lines (and the lines between them, once you have kept what you want), then register again.
<a id="harness-context-refused"></a>
### `gstack-harness: refusing to write the gstack block into <file>: <why>` (HARNESS_CONTEXT_REFUSED)
`--context` writes the routing block only into a file you name, and two kinds of file are refused before anything is written. `SOUL.md` (in any letter case) is the harness's identity file (Hermes and OpenClaw both load it every session), and gstack never edits it. For Hermes, a new `.hermes.md`, `HERMES.md`, `AGENTS.override.md`, `AGENTS.md` or `CLAUDE.md` that would outrank a rules file Hermes loads now is refused too: Hermes loads one project context type, first match wins (`.hermes.md`/`HERMES.md`, then the `AGENTS.md` chain, then `CLAUDE.md`, then `.cursorrules`), takes the first name it finds in a folder (`.hermes.md` before `HERMES.md`, `AGENTS.override.md` before `AGENTS.md`), and takes the nearest `.hermes.md`/`HERMES.md` walking up to the git root, so creating it would hide those rules. Name the file Hermes already loads instead (the error names it), or a file in `$HERMES_HOME` for every session.
<a id="harness-render-failed"></a>
### `gstack-harness: could not render <harness> skills: ...` (HARNESS_RENDER_FAILED)
@@ -5141,3 +5383,47 @@ QM's admin API refused or did not answer. gstack lists packs before it creates o
QM registration does not work yet. QM's skill-pack import clones the whole repository and reads every file before it picks the skills, and by default it refuses a pack over 32 MiB or 5,000 files. The gstack repository is larger (about 73 MiB in 4,300 files), so gstack checks the size first and sends nothing. When QM itself refuses (a QM with other limits), the pack it created stays in the org without skills: `gstack-harness unregister qm --remote` archives it. Meanwhile, give a QM agent gstack by pasting the output of `gstack-harness register generic --print` into its instructions.
## Module loadouts (gstack-modules)
These codes come from `bin/gstack-modules` and `gen-skill-docs --loadout/--modules` (guide: [loadouts.md](loadouts.md)). Each one exits 2 before anything is written, and none falls back to the `full` loadout: an eval arm that silently rendered `full` would measure the control.
<a id="loadouts-invalid"></a>
### `LOADOUTS_INVALID: modules/loadouts.json …`
**Meaning.** `modules/loadouts.json` is unreadable, or names a module, base loadout, rung or default that does not exist, excludes a pinned module, or maps one model id to two rungs. **Fix.** Correct the named entries, then run `bin/gstack-modules validate` until it prints `LOADOUTS: ok`; `git checkout -- modules/loadouts.json` restores the committed file.
<a id="loadout-unknown"></a>
### `LOADOUT_UNKNOWN: "<name>" is not a loadout (valid: …)`
**Meaning.** `--loadout` or the first token of `--modules` names no loadout in `modules/loadouts.json`. **Fix.** Use a name from the `valid:` list (`bin/gstack-modules list` prints them); `full` is always present.
<a id="module-spec-invalid"></a>
### `MODULE_SPEC_INVALID: …`
**Meaning.** A `--modules` token is not `-<module id>` or `+<module id>`, both `--loadout` and `--modules` were given, `gen:skill-docs` got a selection with exclusions but no `--out-dir`, or a render flag is missing or malformed. **Fix.** Write the spec as `<loadout>,-<id>,+<id>` (for example `full,-section:office-hours/second-opinion`) and pass the flags `bin/gstack-modules --help` lists.
<a id="module-unknown"></a>
### `MODULE_UNKNOWN: "<id>" is not a module`
**Meaning.** The id names no skill, section, overlay or preamble piece. **Fix.** Copy the id from `bin/gstack-modules list`.
<a id="module-pinned"></a>
### `MODULE_PINNED: <id> is pinned and can never be excluded`
**Meaning.** The spec removes a module the safety contract needs: the start command (`preamble:bash`), plan-mode rules, the question format, completion status, the unattended profile, /ship's review gates, the router or `/gstack-upgrade`. **Fix.** Drop `-<id>` from the spec; `bin/gstack-modules list --pinned` prints every pinned id.
<a id="module-exclusion-unsafe"></a>
### `MODULE_EXCLUSION_UNSAFE: <id>: referenced by <file>:<line>`
**Meaning.** The rendered tree still points at a module the loadout left out, so an agent following it would read a file that is not there. **Fix.** Exclude the referencing module as well, or wrap the referencing skeleton text in `{{IF_SECTION:<id>}}…{{/IF_SECTION}}` in its template (the text renders unchanged when the section loads). Nothing was published; rerun after the change.
<a id="render-failed"></a>
### `RENDER_FAILED: gen-skill-docs failed: …`
**Meaning.** Generation itself failed for this loadout (an unresolved placeholder, an oversized skill, a missing section). **Fix.** Fix the named template or module and rerun. The staging directory was removed and `--out-dir` is unchanged.
<a id="render-dest-not-owned"></a>
### `RENDER_DEST_NOT_OWNED: <dir> exists and holds no loadout.lock.json`
**Meaning.** `--out-dir` is a non-empty directory that an earlier `gstack-modules render` did not write, so replacing it could delete someone's files. **Fix.** Pass a new or empty directory, or the directory of an earlier render.
+60 -3
View File
@@ -187,6 +187,15 @@ most 300 words; a subagent brief names the files to read instead of pasting
them. Evidence goes in files the report points to. Waits go on operations,
never on polling wakes.
### Token caps and fresh-context handoffs
A parent bounds a /ship run with `--token-cap <n>` (or `GSTACK_SHIP_TOKEN_CAP`).
It is an admission cap: /ship meters usage, reserves every step and dispatch
batch before it starts, and stops `status=incomplete` with a `RECOVER:` line
instead of starting work that would cross the cap. Long runs hand Steps 7–11.5
and 12–22 to fresh-context children. The flow, the BUDGET line and the result
codes are in [ship-budget.md](ship-budget.md).
### The outside voice on Capy
Step 11's outside adversarial review prefers the Codex CLI. On a Capy machine
@@ -225,9 +234,10 @@ and reruns carry with `--pregate .gstack/tmp/pregate.json`. `CARRY: carried from
now names HEAD with a `carry` object holding the `git range-diff` evidence, and
`ship-run.json` gains `carried_from` and `carry_reason`; the `review_loop`
counters are kept, because the reviewed product diff is unchanged. `CARRY:
current` means nothing moved. `CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)`
or `(RECEIPT_CARRY_REFUSED)` with the refused result line means the product
diff changed or the reviewed head left HEAD's history, so /ship runs again;
current` means nothing moved. `CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)`,
`(RECEIPT_CARRY_TREE_MISMATCH)` or `(RECEIPT_CARRY_REFUSED)` with the refused
result line means the product diff changed, or history was rewritten onto a
tree no review saw, so /ship runs again;
`(RECEIPT_GIT_TOO_OLD)` means the machine's git predates 2.39 and cannot run
`patch-id --verbatim` ([ship-policy.md](ship-policy.md#receipt-carry)).
@@ -494,6 +504,53 @@ by its runner. Result codes: `EXECUTION_UNKNOWN`, `RUN_LOCKED`,
decision ids are renamed and journaled); one written by a newer gstack is
refused.
### Carry-forward
A gate revision after the first (`gate_rev` 2 and later, written after a
`revise`) is answered with the owner's recorded words from the revision before
it when nothing new needs their decision. `gstack-autoplan` prints
`CARRIED: gate_rev=<n> <- gate_rev=<m> words="<their words>" by=<relayer> reason=<why>`,
then the usual `APPROVED:` lines, and journals `gate_carried`. The carry needs
all of: the earlier gate approved (`p1a` or `p1b`) with `rec` or "do it"
(not item tokens), no item there left pending, and the owner's words
recorded; that gate was not itself carried; every accepted change incorporated
or settled; the plan file's bytes equal those recorded at the gate or at the
`revise` that took the changes (any later edit refuses, an ELI5-only one
included); no urgent or
outside-plan finding open; and every new gate item one of the cases below.
Anything else prints `CARRY: no (<why>)` (journal `carry_refused`) and the gate
goes to the owner as before. `--no-carry-forward` on the first `next` turns it
off for the run.
- **"do it".** When the owner's words were "do it" (their words, not a
`captain:` relay of them), a new `auto` item that changes no scope, cost or
direction, whose recommended option changes no plan text, and is no round
or budget grant takes its recommendation. The
decision row gets `carried_under: do it`, the output prints
`CARRIED_UNDER_DO_IT:` per item, and the approval record lists them as
"taken under "do it", not seen by the owner". `rec` carries no new item.
- **Unreviewed post-cap delta.** A "plan changed after <phase> cap" item
recommending `a` (accept) is read by one reviewer before it carries. `CARRY_CHECK:` names the attempt and
the prompt (the delta against `plan-at-gate.r<m>.md`, the accepted changes,
the full plan); the run reserves the estimate in `spend.json` and refuses
when it is over `--carry-check-cap <usd>` (default 5) or the run's cap.
Dispatch it to a model family other than the plan reviewers', then
`check --out <dir> --attempt <id> --result <file> --model <id>`
(`--same-family "<why>"` when no other family is available). A High or
Critical finding, or `--unavailable "<reason>"`, leaves the gate for the
owner. The verdict is saved as `carry_check` on `gate.json`, on the
decision rows (`carry_check`) and in `<dir>/decisions.tsv`. A pending
check is void, its reservation released and the gate left for the owner,
when the owner answers the gate, the plan is revised, a new gate is
written, or the delta changed after dispatch.
Carried rows keep `answered_by: agent:gstack-autoplan` and name their source
(`carried_gate_from`, `carried_gate_rev`, `carried_answered_by`);
`gstack-approvals import --autoplan` writes the carried gate as its own row,
and `gstack-owner-brief render --captain` opens with "Nothing pending.
Carried forward from gate_rev <m>", followed by any items taken under "do it"
without being shown to the owner.
## Owner brief
`gstack-owner-brief render --out <dir>` is what the owner sees at the final