Merge remote-tracking branch 'origin/main' into gsta232-autoplan-wave

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

# Conflicts:
#	test/fixtures/skill-md-size-allowlist.json
This commit is contained in:
garrytan committed 2026-11-01 08:17:35 +00:00
commit 2a55f70e67
138 files changed
+6661 -756

No files matched your search

+1 -1
View File
@@ -50,7 +50,7 @@ send off the machine MUST write a hash-chained receipt to
polarity is per-class: fail-closed for sensitive sinks (brain-sync, memory-ingest,
gbrain-sync, telemetry, ngrok tunnels, mcp-verify, supabase-provision, and the
Memorable bridge's per-prompt memorable-recall hand-off), fail-open
+ stderr warning for user-facing ones (design OpenAI calls, update-check,
+ stderr warning for user-facing ones (design OpenAI and Gemini calls, update-check,
dashboards, git-class ops). The new-sink scanner in
`test/egress-receipt-wiring.test.ts` fails CI on an unreceipted `curl` /
`git push` / `fetch` to a non-loopback host unless the file carries a reasoned
+12
View File
@@ -300,6 +300,18 @@ anything else goes to a repair lane. An infra-only census earns one re-dispatch.
backstop gate runs on every push to the base branch and freezes the queue when it finds
a tree no green train covered; one repair PR at its exact head clears the freeze.
**Wave PRs when the queue is long.** Every slot pays its own
stamp, its own round of required checks and its own version, one after another. When more
than 6 PRs are READY, `gstack-ship-queue wave plan` prints `WAVE_ADVISED` and suggested
area groups of 3 to 6 (`train build` prints the same line when handed more than 6 pins). The
controller asks the owner of each group's largest PR to integrate: `wave build` makes the
wave branch with every constituent merged unchanged, the integrator runs /ship once and
READYs the wave PR, which then takes one slot and one version. Constituent heads are frozen
once in; fixes go through the integrator, and a constituent that must change is dropped
(`wave build --wave <nonce> --drop <pr>`) and goes back to its owner. After the wave lands,
`wave settle` closes each constituent with a link. While the queue stays long, start new
work as area waves. The full flow is in [ship-policy.md](ship-policy.md#wave-prs).
**Slot 2 and later.** The base branch already carries the earlier slots' stamps, so a
stamp on the bare pin leaves the PR conflicting on the CHANGELOG. The lane merges the base
into its pin first (expect a CHANGELOG-only conflict; resolve it with `mergeChangelogs`
+6 -1
View File
@@ -241,7 +241,12 @@ keeps the lease. `bin/gstack-lease list` shows `alive|dead|stale`, and
clone too, through `GSTACK_CAPY_CLONED`).
2. Preflight: `git`, `curl`, `node`, `jq`, then Bun. The project's pins
(`--project <path>`, default the git toplevel of the current directory) are
read with the doctor's own parser (`bin/gstack-runtime-pins.ts`: engines,
read (a `--project` inside a repository resolves to that repository's git
toplevel, so `--project <repo>/<subdir>` means `<repo>`; outside a repository
it resolves to the directory's real path, and a value that is not a
directory, such as a repo slug, exits 2 with a usage error before anything
runs; the pins, the doctor and the install verdict all use the resolved
directory) with the doctor's own parser (`bin/gstack-runtime-pins.ts`: engines,
packageManager, `.tool-versions`, `.bun-version` and CI `bun-version:` lanes
for this platform). CI exact pins for this platform are alternatives: a
repository may test several Bun versions (gstack's own CI pins 1.4.2 and
+29
View File
@@ -207,6 +207,35 @@ receipts record the gateway host. On Codex, `/design-shotgun` generates mockups
with Codex's built-in `$imagegen` skill instead, so it needs no
`OPENAI_API_KEY`; `$D` still builds and serves the comparison board.
### Gemini when there's no OpenAI key
With no OpenAI key, `$D` uses Google's Gemini instead, so people without an
OpenAI key (or with an OpenAI organization that hasn't passed ID verification)
get real mockups instead of wireframes. A Gemini key from
[Google AI Studio](https://aistudio.google.com/apikey) is free to create:
`export GEMINI_API_KEY=...` or `$D setup --provider gemini`.
| | OpenAI | Gemini |
|---|---|---|
| Picked when | an OpenAI key is set (always wins) | no OpenAI key, or `GSTACK_DESIGN_PROVIDER=gemini` |
| Key | `~/.gstack/openai.json`, then `OPENAI_API_KEY` | `~/.gstack/gemini.json`, then `GEMINI_API_KEY` |
| Images | `gpt-5.5` driving `gpt-image-2` | `gemini-nano-banana-2.1` (`GSTACK_DESIGN_IMAGE_MODEL`) |
| Screenshot reading | `gpt-5.5` | `gemini-3.8-flash` (`GSTACK_DESIGN_MODEL`) |
| Egress ledger sink | `design-openai` | `design-gemini` |
`GSTACK_DESIGN_PROVIDER=openai|gemini` picks one explicitly; when an OpenAI key
is also set, `$D setup --provider gemini` saves the key and prints that export
as its last line, because OpenAI still comes first otherwise. A failure on the
picked provider is never retried on the other one; an OpenAI
organization-verification error ends with the line that switches to Gemini.
On Gemini, model overrides must be `gemini-…` names, `--size` must be
1536x1024, 1024x1024 or 1024x1536 (sent as an aspect ratio), `--quality` is
ignored, and `OPENAI_BASE_URL` does not apply. On Google's free tier, Gemini
API prompts and images may be used to improve Google's products; `$D` says so
once per run. `bun run design/scripts/live-gemini-check.ts` checks a Gemini
key against the defaults (free model lookups, then one image and one vision
call).
## Adding a host
**Want to add support for another agent?** See [docs/ADDING_A_HOST.md](ADDING_A_HOST.md).
+2 -2
View File
@@ -98,8 +98,8 @@ keeps `.gstack/` out of git (a shared repo where gstack is one contributor's
tool) can hold the same file at `<state root>/projects/<slug>/pregate.json`. The
state root is `GSTACK_STATE_ROOT`, else `GSTACK_HOME`, else `GSTACK_STATE_DIR`,
else a gstack plugin's data directory, else `~/.gstack` ([docs/state-root.md](state-root.md)); `<slug>` is what
`~/.claude/skills/gstack/bin/gstack-slug` prints as `SLUG=` in the repository
(`GSTACK_PROJECT_SLUG` overrides it). It is read only when the base carries none
`~/.claude/skills/gstack/bin/gstack-slug --get REPO_SLUG` prints in the repository
(`GSTACK_PROJECT_SLUG` overrides it; a `.gstack-project` marker does not). It is read only when the base carries none
(under `--policy-from head` it is the trusted policy the head file is compared with), it lives outside the repository so a PR still
cannot reach it, and the record names it (`policy user <path>`) and fingerprints
its bytes (`policy_digest`, and each check's `inputs.policy`); `verify` and the
+2 -1
View File
@@ -376,7 +376,8 @@ 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
([ship-policy.md#receipt-carry](ship-policy.md#receipt-carry)) keeps these
counters, because the reviewed product diff is unchanged.
counters, because the reviewed product diff is unchanged or its only
difference passed a scoped review.
## The scope knob
+88 -2
View File
@@ -551,6 +551,88 @@ a command gate cap below what a full train reserves, a merge method GitHub has
turned off, and `rebase` as the merge method (`QUEUE_TRAIN_DOCTOR_FINDINGS`).
Neither command writes anything.
<a id="wave-prs"></a>
### Wave PRs: several PRs in one slot
When the queue is long, PRs that touch the same part of the code travel
together as one **wave PR**: one integrator branch, one /ship, one READY, one
train slot and one version (Garry's rule R46, 2026-10-31: "consolidating
functional areas into larger wave PRs ... so we can land them faster with fewer
test passes"). Each slot otherwise costs its own stamp commit and a full round
of required checks before it merges, one slot after another, so a wave of
three saves two of those rounds and two versions.
```bash
gstack-ship-queue wave plan # read-only: QUEUE, WAVE_ADVISED, WAVE_GROUP lines
gstack-ship-queue wave build --area ship --prs 41@<sha>,42@<sha>,43@<sha>
gstack-ship-queue wave build --wave <nonce> --continue # after resolving a conflict in the printed worktree
gstack-ship-queue wave build --wave <nonce> --drop 42 # rebuild without #42 as a new wave
gstack-ship-queue wave build --wave <nonce> --release # end the wave unlanded; its PRs ride alone
gstack-ship-queue wave status [--wave <nonce>]
gstack-ship-queue wave settle --wave <nonce> [--dry-run] # after the wave PR merged
```
- **When.** `wave plan` counts the live READY PRs (open, head equal to their
READY receipt). Over the trigger (6) it prints `WAVE_ADVISED` and suggested
groups of 3 to 6: PRs that change a common file always share a group (their
conflicts are resolved once), each group takes its largest PR's functional
area (a skill directory, the first word of a `lib/`, `bin/` or `test/`
module name, or `docs`), smaller groups ride solo, and the integrator is
the PR with the most changed lines. `train build` handed more than 6 pins
prints the same `WAVE_ADVISED` line. It is advice: urgent and repair PRs
still ride alone. While the queue stays long, plan new work as area waves
from the start, so each piece skips its own /ship.
- **Build.** `wave build` checks every pin is an open, same-repository PR at
that head (`QUEUE_WAVE_MOVED`, `QUEUE_TRAIN_UNTRUSTED`), not already in a
live wave (`QUEUE_WAVE_DUPLICATE`), 3 to 6 of them (`QUEUE_WAVE_SIZE`;
`--min`/`--max` change it). It merges the heads in order onto main with the
train's merge rules, each as a merge commit whose second parent is the pin,
so no constituent is rebased or squashed. The wave record (kind `wave`) is
written locally and to `refs/notes/gstack-trains` before `wave/<nonce>` is
pushed, and each constituent gets one "frozen in wave" comment. Then the
integrator opens the wave PR (the printed `gh pr create` and body file, or
the host's PR tool), runs /ship on it and READYs it.
- **Conflicts.** A merge that conflicts outside the CHANGELOG and the stamp
files stops with `QUEUE_WAVE_CONFLICT` and leaves the merge in progress in a
worktree under the state root. The integrator resolves and commits it there,
then `--continue` checks that commit merges exactly that pin onto the
recorded commit and finishes the chain.
- **Frozen heads.** Once in, a constituent's head does not move. Fixes go on
the wave branch as integrator commits, which the chain check allows. A
constituent that has to change goes back to its owner: `--drop <pr>` builds
a new wave on a new branch (never a force-push; the integrator closes the
old wave PR first, and the old record is marked superseded), refused with
`QUEUE_WAVE_DEPENDENT` when another pin contains the dropped one. A drop
that would leave fewer PRs than the minimum releases the wave instead, and
`--release` does the same for a wave PR closed without merging: the record
is marked released and every constituent can be READY'd on its own again.
Both wait until the wave PR is closed, since a train that already admitted
it would still land it. A constituent must target the base branch; a
stacked PR is refused, because its pin carries its parent's commits.
- **READY and the train.** `ready --pr <wave pr>` and `train build` read the
wave record for a `wave/<nonce>` head branch (rebuilt from the notes ref on a
fresh machine; `QUEUE_WAVE_UNKNOWN` when there is none). They walk the
branch's first parents back to the recorded base: every pin must be merged
unchanged, in order, and any other merge is refused. Every constituent must
still be open at its pin. READY gains `WAVE:` and `CONSTITUENTS:` lines.
`ready` and `train build` refuse a constituent of a live wave on its own
(`QUEUE_WAVE_DUPLICATE`), so it cannot land twice. A wave head may carry
train land's stamp commits after its last constituent, so a stamped wave
is re-READY'd like any stamped PR. From there the wave PR is an ordinary slot: `train
land`, `verify`, the stamp and the backstop treat it like any PR, and under
`changelog_fragments` its one stamp folds every constituent's fragment into
one version section.
- **After the merge.** `wave settle` finds the wave PR merged into the base
branch, checks that its merge commit is on that branch and carries its head
(the head's tree, or the head merged onto the merge's parent when the train
merged the PR directly), and walks its chain (train land's stamp or update
commits on top are allowed). A
constituent still at its pin gets a "Landed in wave #W as vX (sha)" comment
naming any integrator commits or hand-resolved merges that touched its
files, and is closed. One whose head moved after the wave froze stays open
with a comment saying its newer work did not land. Comments carry a marker, so a
rerun posts nothing twice; `--dry-run` shows the plan.
<a id="ship-before-ready"></a>
**0. Lane: /ship before READY.** Every READY follows a /ship run on the branch
at the head being READY'd, including a re-READY after a push (Garry,
@@ -1217,7 +1299,8 @@ 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 merge of the base that edited lines next to the branch's unchanged hunks (the branch's +/- lines identical and in the same places) | carries after one fix-check reviewer reads the hunks and comes back clean (`RECEIPT_CARRY_SCOPED_REVIEW`, then `carry --scoped-review <record.json>`) |
| a merge of the base whose resolution changed product code, or moved the branch's lines elsewhere | 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`) |
@@ -1293,7 +1376,10 @@ 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 working tree with uncommitted
(exit 3; the refusal names the paths), except when the only difference is
base context next to the branch's unchanged hunks, which is
[`RECEIPT_CARRY_SCOPED_REVIEW`](troubleshooting.md#receipt-carry-scoped-review)
until one fix-check reviewer clears the packet; 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)
+72
View File
@@ -137,6 +137,78 @@ yourself, for example in the project's `.claude/settings.local.json`
remote, or for a vendored sub-repo that should be its own project. The pin is
applied per run and never cached.
### Nested repos and umbrella folders
By default, a repository inside another repository files under the **outer**
repository's slug, even when the inner one has its own remote. To make a folder
a project of its own, create an empty marker file in it:
```bash
touch path/to/folder/.gstack-project
```
- **A nested repo** (`~/projects/app-one/` with its own remote inside a
workspace repo): put the marker at the nested repo's top folder. It and every
folder under it file under its own remote's slug. The outer repo is unchanged.
- **An umbrella folder** (a plain folder over `backend/` and `frontend/`
repos): put the marker in the umbrella. The umbrella and both repos share
one memory under the umbrella folder's name, so a plan made in the umbrella
carries into the repo where you build it. A repo under the umbrella that should
keep its own memory gets its own marker; the innermost marker wins.
The marker's contents are ignored. It counts only when it is a regular file
you own, in a folder whose name has a letter or digit, and either at a
repository's top folder or in a folder that isn't inside any repository. When
you work under your home folder, a marker in your home folder itself or above
it is ignored; outside your home folder (a CI or cloud checkout under
`/workspace`, say) every folder up to `/` is checked, so keep stray markers out
of shared parents. A marker in a subfolder of a repository, a link or a
directory is ignored, so a repository you clone can't choose another project's
memory. An umbrella without a remote of its own files under its folder name, so
two umbrellas with the same name (`~/clientA/work` and `~/clientB/work`) share
one memory: rename one, or pin each with `GSTACK_PROJECT_SLUG`. Commit
it in a nested repo you share, if you like; it changes nothing for someone who
clones that repo on its own.
Checkpoints, learnings, decisions, the timeline and design docs follow the
marker. Records that belong to one repository never do: pregate policy and run
scratch, approvals, `/verify` results, the per-branch review log, the `/ship`
spend-cap pointer, the cached solo or collaborative repo mode and the Greptile
early-push answer stay
under the slug the repository has without a marker (`gstack-slug --get
REPO_SLUG`), so a branch that commits a marker can't change which policy
applies.
| To | Use | Lasts |
|---|---|---|
| Make a folder its own project | `.gstack-project` | until you delete it |
| Pick an exact bucket name | `GSTACK_PROJECT_SLUG` in the project's `.claude/settings.local.json` | per Claude Code project |
| Pick a name for one shell | `export GSTACK_PROJECT_SLUG=<name>` | that shell |
`GSTACK_PROJECT_SLUG` wins over a marker. Check which applies:
```bash
~/.claude/skills/gstack/bin/gstack-slug --identity
```
`PROJECT_MARKER=` names the marker in use; each `MARKER_IGNORED=` line names
a marker that didn't count and why. Deleting the marker restores the slug the
folder had before.
**Bringing earlier checkpoints across.** Checkpoints saved before the marker
stay in the old bucket, and the first run after you add the marker names it.
Preview the ones saved inside the marked folder, then copy them:
```bash
~/.claude/skills/gstack/bin/gstack-slug --adopt-legacy --from <old slug> --under-root
~/.claude/skills/gstack/bin/gstack-slug --adopt-legacy --from <old slug> --under-root --apply
```
Only checkpoints stamped with a folder at or below the marked one are copied,
and they are restamped so `/context-restore` finds them. Checkpoints from
before identity stamps, and other projects' files, stay where they are. The old
bucket is never changed.
## Ephemeral machines
A Capy cloud machine, a CI runner or a throwaway VM loses its disk when the
+220 -7
View File
@@ -767,13 +767,67 @@ If the message names a missing timeout tool, install coreutils (`gtimeout`) or p
**Expected result.** The next design skill prints `DESIGN_READY`.
<a id="design-image-model-invalid"></a>
### `GSTACK_DESIGN_IMAGE_MODEL="<value>" is not a gpt-image model name (expected something like gpt-image-2); fix it or unset it to use gpt-image-2`
### `GSTACK_DESIGN_IMAGE_MODEL="<value>" is not a gpt-image model name ...` (or `... is not a Gemini model name, and the design provider is gemini`)
**Meaning.** The design binary refuses an image tool model override that is not
a `gpt-image-*` model name, before sending any request.
**Meaning.** The design binary refuses a model override that does not fit the
picked provider, before sending any request. On OpenAI the image tool model
must be a `gpt-image-*` name; on Gemini, `GSTACK_DESIGN_IMAGE_MODEL` and
`GSTACK_DESIGN_MODEL` must be `gemini-*` names. A Gemini 404 for a model also
points here.
**Fix.** `export GSTACK_DESIGN_IMAGE_MODEL=gpt-image-2` (or another gpt-image
model your key can use), or `unset GSTACK_DESIGN_IMAGE_MODEL`.
**Fix.** `unset GSTACK_DESIGN_IMAGE_MODEL GSTACK_DESIGN_MODEL` to use the
defaults, or set models the picked provider and your key can use.
<a id="design-no-image-key"></a>
### `No image API key found (OpenAI or Gemini).`
**Meaning.** `$D` found neither an OpenAI key (`~/.gstack/openai.json`,
`OPENAI_API_KEY`) nor a Gemini key (`~/.gstack/gemini.json`,
`GEMINI_API_KEY`). Design skills fall back to HTML wireframes.
**What is kept.** Nothing was sent or spent.
**Fix.** Get a free Gemini key at https://aistudio.google.com/apikey, then
`export GEMINI_API_KEY=<key>` or `printf %s "$KEY" | $D setup --provider gemini --key-stdin`.
**Expected result.** The next design command prints `Using Gemini key from ...`.
<a id="design-provider-invalid"></a>
### `GSTACK_DESIGN_PROVIDER="<value>" is not a design provider (expected openai or gemini).`
**Fix.** `export GSTACK_DESIGN_PROVIDER=gemini` (or `openai`), or
`unset GSTACK_DESIGN_PROVIDER` to let `$D` pick (OpenAI when its key is set,
else Gemini).
<a id="design-provider-no-key"></a>
### `GSTACK_DESIGN_PROVIDER=<provider> but no <Provider> key was found`
**Meaning.** You picked a provider explicitly and it has no key, so `$D`
stopped before any request instead of quietly using the other one.
**Fix.** Set that provider's key (`GEMINI_API_KEY` or `OPENAI_API_KEY`), run
`$D setup --provider <provider>`, or unset `GSTACK_DESIGN_PROVIDER`.
<a id="design-gemini-key-rejected"></a>
### `Gemini API error (400|403, model <model>, key from <source>): ...`
**Meaning.** Google rejected the key (`API_KEY_INVALID`), or the key's Google
Cloud project has the Generative Language API turned off. The message names
where the key came from, never the key.
**Fix.** Create a key at https://aistudio.google.com/apikey and run
`$D setup --provider gemini`, or enable the Generative Language API for the
key's project. To use OpenAI instead: `export GSTACK_DESIGN_PROVIDER=openai`.
<a id="design-gemini-quota"></a>
### `Gemini API error (429 ...): ... no image quota left today`
**Meaning.** The key's daily quota for the model is used up, or the key is on a
free tier with no quota for image models. `$D` does not retry this; a
short-term 429 is retried after Google's suggested delay.
**Fix.** Enable billing for the key in Google AI Studio, wait for the daily
reset, or `export GSTACK_DESIGN_PROVIDER=openai`.
<a id="design-taste-profile-unavailable"></a>
### `TASTE_PROFILE_UNAVAILABLE: could not resolve the project slug (gstack-slug failed). Fix: run ./setup.`
@@ -3265,6 +3319,54 @@ makes the row a check rather than a claim.
`REALITY: already-done pass 0 hits since 2026-10-09 for bin/gstack-doctor 91bbd9e`
or `REALITY: surface-check finding bun upgrade --version absent on 1.3.14 bin/gstack-capy-install:52`.
<a id="office-hours-probe-refused"></a>
### `OFFICE_HOURS_PROBE_REFUSED`
**Meaning.** The office-hours spec reviewer may check a cited flag with
`gstack-office-hours-review probe --help <command> [<subcommand>]`, and nothing
else runs. The command must be a gstack bin or one of the listed read-only CLIs
(git, gh, bun, node, npm, pnpm, yarn, jq, rg, python3, uv, go, cargo, make,
docker, claude, codex, gemini), found on PATH outside the repository; a gstack
bin must handle `--help`. Subcommand words are allowed for git, gh,
docker, cargo, go, npm and uv (one word for git, cargo and npm; up to two for gh, docker, go and uv), each from that CLI's list of built-in names in
`lib/office-hours-probe.ts`; words that run a named program, script, image or
extension (`uv run`, `docker run`, `go test`, `npm exec`, a gh extension) are
refused. The probe never runs a shell or a repository-local program (PATH
entries inside the repository are dropped), closes stdin, runs in an empty
temporary directory, passes no keys or tokens in its environment and stops after
10 seconds.
**Fix.** Nothing to repair. The claim stays unverified, not disproved; check it
from the command's source or docs in the repository instead.
<a id="tasks-supersede-refused"></a>
### `TASKS_SUPERSEDE_REFUSED`
**Meaning.** `gstack-tasks` refused a task list. Either `add` found a line in
the rows file (`.gstack/tmp/tasks-<phase>-rows.jsonl`) that is not a valid
task or disposition, or it leaves a task from the branch's earlier list neither
carried nor closed (the error names each `<run_id>/<id>`), so nothing was written; or `supersede` was asked to retire the earlier lists
but the new list (`--keep`) is missing, outside the tasks directory, has a
malformed line, or holds a row for another phase or branch, so nothing was
moved and both lists stay live.
**Fix.** Fix the line the error names in the rows file (`gstack-tasks --help`
shows both row shapes), and rerun `gstack-tasks add --phase <phase>`.
<a id="reality-receipt-unresolved"></a>
### `REALITY_RECEIPT_UNRESOLVED`
**Meaning.** `gstack-plan-reality check` opened a receipt and it is false in
the checkout: the file is missing, it is a directory or a dangling link, the
line is 0 or the range runs backward, or the cited line is past its end. The `RECEIPT_UNRESOLVED: <row> <receipt> <reason>` line names each
one. A receipt the checkout cannot judge (a path outside every root, a bare
file name no root has, a sha a shallow clone lacks) prints
`RECEIPT_UNVERIFIABLE:` instead and never blocks.
**Fix.** Open the file and cite the line that holds the evidence. When the
evidence lives in another checkout, run `check` with `--root <dir>` for it
(repeatable; the default is the git top level of the working directory).
## Pre-gate: generated-file registry, gstack-pregate, issue links
Both tools read their pin file (`.gstack/generated.json`, `.gstack/pregate.json`)
@@ -4890,6 +4992,91 @@ trains and other trains' merges are refused until a repair lands.
<n>@<sha>`). The freeze clears when verify finds main's tree after that merge
in a green record; `NEXT:` then builds a train of the PRs still unmerged.
<a id="queue-wave-conflict"></a>
### `QUEUE_WAVE_CONFLICT`
**Meaning.** `wave build` merged the constituents in order with the train's
merge rules, and one conflicts with the wave so far outside the CHANGELOG and
the stamp files. Nothing was pushed. The record (state `conflict`) names the
PR, the files and a worktree under the state root where the merge is in
progress. `--continue` refuses until that worktree holds one committed merge
of the constituent's pin onto the recorded commit with a clean tree.
**Fix.** Resolve the files in the printed worktree, `git -C <worktree> commit
--no-edit`, then `gstack-ship-queue wave build --wave <nonce> --continue`. Or
build the wave without that PR.
<a id="queue-wave-moved"></a>
### `QUEUE_WAVE_MOVED`
**Meaning.** A wave no longer lands what its record names: a constituent was
pushed, closed or merged after it joined (heads freeze once in), the wave
branch carries a merge that is not a constituent's pin (main merged in, for
example), a pin is missing from the branch, or the wave was rebuilt and this
PR is the old one. The same code refuses a constituent that targets a branch
other than the base (a stacked PR's pin carries its parent's commits), a
wave that was released, and a `--drop` or `--release` while the wave PR is
still open, since a train that already admitted it would still land it.
**Fix.** Close the wave PR if one is open, then rebuild without the moved PR,
`wave build --wave <nonce> --drop <pr>`, which makes a new wave branch and PR (never a
force-push); run /ship and READY the new wave PR. The moved PR goes back to
its owner and rides on its own or in a later wave. A stacked PR rides on its
own; after a release, each constituent is READY'd on its own.
<a id="queue-wave-duplicate"></a>
### `QUEUE_WAVE_DUPLICATE`
**Meaning.** A PR would land twice or waves would nest: it is already a
constituent of another live wave, `train build` was handed both a wave PR and
one of its constituents (or two waves sharing one), a wave PR was named as a
constituent, or a constituent of a live wave was READY'd or handed to `train
build` on its own.
**Fix.** Keep each PR in one place: drop the separate pin from `train build`,
or rebuild one wave without the shared PR. A constituent of a live wave lands
through the wave PR.
<a id="queue-wave-size"></a>
### `QUEUE_WAVE_SIZE`
**Meaning.** A wave carries 3 to 6 PRs by default (R46). `--min` and `--max`
change the bounds for one command.
**Fix.** Add or remove PRs; `wave plan` suggests area groups. Fewer PRs than
the minimum ride as single train slots.
<a id="queue-wave-unknown"></a>
### `QUEUE_WAVE_UNKNOWN`
**Meaning.** A PR's head branch is `wave/<nonce>`, or a command named a
nonce, but no wave record exists in local state or on
`refs/notes/gstack-trains`. `ready` and `train build` refuse rather than
treat the PR as an ordinary one, since its constituents cannot be checked.
**Fix.** `gstack-ship-queue wave status` lists the known waves. When the
record is gone, rebuild the wave with `wave build --prs`.
<a id="queue-wave-dependent"></a>
### `QUEUE_WAVE_DEPENDENT`
**Meaning.** `wave build --drop <pr>` was asked to drop a PR whose pin is an
ancestor of another constituent's pin (a stacked PR), so the rebuilt wave would
still land the dropped code.
**Fix.** Drop the containing PR too, or keep the PR in the wave.
<a id="queue-wave-not-merged"></a>
### `QUEUE_WAVE_NOT_MERGED`
**Meaning.** `wave settle` found no PR merged into the base branch whose head
branch is the wave branch, its merge commit is not on the base branch, or that
merge's tree is neither the PR's head tree nor that head merged onto the
merge's parent, so it cannot say the constituents landed. Nothing was commented or closed.
**Fix.** Land the wave PR through its train (`train land`), then rerun
`gstack-ship-queue wave settle --wave <nonce>`.
<a id="queue-receipt-missing"></a>
### `QUEUE_RECEIPT_MISSING`
@@ -5354,11 +5541,37 @@ 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, 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
the recorded product diff, a stacked PR's retarget once its parent fully
landed on the base, or base context next to unchanged hunks after a clean
scoped review ([`RECEIPT_CARRY_SCOPED_REVIEW`](#receipt-carry-scoped-review)), 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)).
<a id="receipt-carry-scoped-review"></a>
### `RECEIPT_CARRY_SCOPED_REVIEW`
**Meaning.** Every product path the branch touches has the reviewed +/- lines
in the same places, but the base edited context lines next to some of them, so
the 3-line patches differ (GST-384: main reworded the line above the PR's
hunk). The merge may have spliced the two, as in a JSON entry where the branch
changed one key and the base the next, so the receipt does not carry silently.
The refusal names the paths and a packet file with the hunks that differ and
the base's own change. The same lines moved somewhere else are
`RECEIPT_CARRY_PRODUCT_CHANGED`, not this code.
**Fix.** Dispatch one fix-check reviewer
([review-policy.md](review-policy.md)) on the packet. When it comes back clean,
write `{"head":"<HEAD>","paths":[...],"reviewer":"<model>","verdict":"clean"}`
and rerun `gstack-ship-receipt carry ... --scoped-review <record.json>`; the
carry reason records the reviewer and the paths. A blocking finding means /ship
on the new head. `gstack-ship-release finish` stops the same way with
`context=N` and takes `--context-reviewed <record.json>` bound to the tree;
write that record beside the packet, outside the worktree, so it does not change
the tree it is bound to. Finish also stops this way for test code changed since
review with no gate edit (`tests=N`): the packet holds those test hunks, and the
reviewer checks that each test still tests what it did. A packet that cannot be
written (an unwritable `.git`) is a STOP or `RECEIPT_CARRY_REFUSED` naming the path.
<a id="receipt-prose-mismatch"></a>
### `RECEIPT_PROSE_MISMATCH`
+6 -2
View File
@@ -237,8 +237,12 @@ and reruns carry with `--pregate .gstack/tmp/pregate.json`. `CARRY: carried from
`GSTACK_RESULT: skill=ship status=complete run=<dir>` line means the receipt
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)`,
counters are kept, because the reviewed product diff is unchanged or its only
difference passed a scoped review. `CARRY: current` means nothing moved.
`CARRY: refused (RECEIPT_CARRY_SCOPED_REVIEW)` means the base edited context
next to reviewed hunks: one fix-check reviewer reads the packet the refusal
names, and a clean record reruns carry with `--scoped-review <record.json>`
([troubleshooting](troubleshooting.md#receipt-carry-scoped-review)). `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;