mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-15 12:57:22 +01:00
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:
commit
2a55f70e67
138 files changed
+6661
-756
No files matched your search
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user