mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-14 12:27:18 +01:00
v1.92.16.0 stamp: train gate.train-222fed7efa48-8695938e7464-b5f2d4 S1 (#3684)
This commit is contained in:
commit
e4a6bf6d3b
244 files changed
+14961
-1569
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
|
||||
|
||||
+82
-16
@@ -199,7 +199,7 @@ one duration-packed `--ci-plan`, 20 ordinary `--ci-run` shards plus a separate
|
||||
exclusive-fixture shard, and a `--ci-verify` aggregate. CI shards still run on
|
||||
independent machines without ordering unrelated jobs. Two workflow-level
|
||||
constants in `free-tests.yml` set that size for every event: `FREE_SUITE_RUNNER`
|
||||
(`ubicloud-standard-2`) and `FREE_SUITE_SHARDS` (`'20'`), which `free-plan`
|
||||
(`ubicloud-standard-4`) and `FREE_SUITE_SHARDS` (`'20'`), which `free-plan`
|
||||
resolves into the shards' runner and shard count. A `workflow_dispatch` run may
|
||||
override them with `free_runner` (`ubicloud-standard-2`, `-4` or `-8`) and
|
||||
`free_shards` (1–40) for runner-sizing experiments only; its job summary says
|
||||
@@ -1034,6 +1034,22 @@ never forwards `UBICLOUD_*`. All of this is pinned offline in
|
||||
`test/ubicloud-runner.test.ts` against a fake API and a fake `ssh`, including
|
||||
a fixture whose fake API creates, so `run` and `verify` execute end to end.
|
||||
|
||||
`run` and `verify` start their setup, command and tier phases detached on the
|
||||
VM (output, leader pid and exit status in `~/.ubirun-<phase>.*`) and key each
|
||||
run locally before any VM exists (content tree, command, setup content, env,
|
||||
pass-value hashes, size, location, image, storage, origin/HEAD, the runner's
|
||||
own sha256). A rerun of the same command after the runner died reattaches to
|
||||
the live VM and continues from the first phase without an exit file
|
||||
(`UBI_ATTACH_BUSY` while another runner owns it, exit 125 `UBI_ATTACH_LOST`
|
||||
when the session left no status); HUP detaches (`UBI_DETACHED`, exit 129), INT
|
||||
and TERM still tear down. A `.gstack/ubicloud.json` with `tiers` records
|
||||
start/pass/fail per tier under `UBI_CHECKPOINT_DIR/<run key>/`, so a resume on
|
||||
a fresh VM reruns setup and only the tiers without a pass; `pregate-remote.sh`
|
||||
adds its tree and planned-set digest to the key, so a rerun of
|
||||
`gstack-pregate --stage tests --remote` reattaches and bills from the original
|
||||
`created_at`. The fake VM in `test/ubicloud-runner.test.ts` runs these phases
|
||||
for real in a directory.
|
||||
|
||||
Under 8 cores (`gstack-doctor`'s `cores` row says so), run the full unit and
|
||||
E2E tiers on a remote gate: `gstack-config set test_backend ubicloud` records
|
||||
the choice and the row names the runner. Resume checklist after a wake or a
|
||||
@@ -1524,7 +1540,7 @@ refresh to a maintainer.
|
||||
`scripts/ubicloud/setup-free-suite.sh` and the Windows seed through
|
||||
`windows-free-tests.yml`'s `record_durations` path. The recorder stays on
|
||||
`ubicloud-standard-8` while the free-suite shards run on `FREE_SUITE_RUNNER`
|
||||
(`ubicloud-standard-2`); both are Ubicloud standard runners. When either recording adds
|
||||
(`ubicloud-standard-4`); both are Ubicloud standard runners. When either recording adds
|
||||
or drops a test file, it force-updates the `bot/free-duration-seeds` branch,
|
||||
opens or refreshes one seed PR whose body states the ratchet result, and
|
||||
dispatches Free Tests, Windows Free Tests and Quality gate on it. Those
|
||||
@@ -1629,9 +1645,11 @@ word list: `test/fixtures/arm-benchmark/` and its four children, `browse-judge/`
|
||||
names under `paid-report-census/` and the `test/` subdirectories of
|
||||
`state-matrix-6792/`. All of these are runner-side: no candidate's working
|
||||
directory is one of them. Candidate-visible gaps are in the temporary roots
|
||||
the harnesses create: `test/helpers/arm-benchmark-harness.ts` copies each fixture
|
||||
into `os.tmpdir()/arm-<fixture>-<arm>-*`, so the candidate's working directory is
|
||||
named after the arm rather than after a project; `test/skill-e2e-deploy.test.ts`
|
||||
the harnesses create: `test/helpers/arm-benchmark-harness.ts` copied each fixture
|
||||
into `os.tmpdir()/arm-<fixture>-<arm>-*`, so the candidate's working directory was
|
||||
named after the arm rather than after a project (since GST-469 it is named after
|
||||
the task's project: `hike-booking-*`, `notes-api-*`, `receipts-*`);
|
||||
`test/skill-e2e-deploy.test.ts`
|
||||
runs the /benchmark case in `skill-e2e-benchmark-*`, a root that carries a listed
|
||||
word; `test/skill-e2e-test-value.test.ts` creates `skill-e2e-<id>-*` roots as the
|
||||
candidate's working directory, and two of its ids (`review-test-value`,
|
||||
@@ -1676,18 +1694,66 @@ own attempts. The arm gets:
|
||||
|
||||
**Reporting.** The no-gstack result is one row beside the variants in every
|
||||
results table and every verdict, including when it wins. A comparison that
|
||||
drops the row has not reported its result. The prereg check cannot see the
|
||||
report, so the `report:` line states where the row goes and the reviewer of
|
||||
the result holds the author to it.
|
||||
drops the row has not reported its result. `gstack-eval-plan results --check
|
||||
<prereg> <results.md>` enforces it: one table in the results file must hold a
|
||||
filled row for every Arms identity (`EVAL_REPORT_NO_GSTACK_ROW` otherwise), and
|
||||
no number is published before it passes.
|
||||
|
||||
**Precedent and its gaps.** `test/helpers/arm-benchmark-harness.ts` already has
|
||||
a `without-skill` arm beside `with-skill`, and it is the closest existing
|
||||
control. It falls short of this rule in three ways: each arm and task runs
|
||||
once rather than as a panel; the judge (`armJudge` in
|
||||
`test/helpers/llm-judge.ts`) scores each diff on its own, without `A`/`B`
|
||||
labels; and the candidate's temporary folder is named after the arm
|
||||
(`arm-<fixture>-<arm>-*`). Existing paid cases are not retrofitted by the
|
||||
change that added this rule; an audit of them is tracked separately.
|
||||
**Safety-rule and router evals: a descriptive third arm.** The W1 safety-rule
|
||||
evals compare `GSTACK_SAFETY_ARM=rule` (the shipped rule) with `removed` (the
|
||||
rule paragraph deleted), and the router failure-routing evals compare
|
||||
`GSTACK_ROUTER_ARM=current` with `before` (the base branch's router). Both
|
||||
variables also take `none` (GST-470): the same fixture, tools and user request,
|
||||
with no skill excerpt, rule text, step file or installed skill. Any other value
|
||||
throws, so a typo cannot run one arm under another's label. The `rule`,
|
||||
`removed`, `current` and `before` prompts are byte for byte what they were
|
||||
before `none` existed (`test/safety-none-arm.test.ts`,
|
||||
`test/failure-routing-arm.test.ts`). To run the three arms as a comparison, run
|
||||
the same case once per arm, each as its CI panel:
|
||||
|
||||
```bash
|
||||
for arm in rule removed none; do
|
||||
GSTACK_SAFETY_ARM=$arm bun run scripts/test-paid-shards.ts --tier gate --case safety-ship-stale-evidence
|
||||
done
|
||||
for arm in current before none; do
|
||||
GSTACK_ROUTER_ARM=$arm bun run scripts/test-paid-shards.ts --tier gate --case router-failure-contradicted-cause
|
||||
done
|
||||
```
|
||||
|
||||
The arm variable is part of the shard-reuse environment, so one arm's result is
|
||||
never reused for another. Report the three rows side by side in one results
|
||||
table and pass `gstack-eval-plan results --check` on it.
|
||||
|
||||
**The descriptive exception.** Under `none` every behavioral check, the plain
|
||||
`expect`s on model actions included, is recorded as an observation in
|
||||
`none-arm-observations.jsonl` in the eval directory and on stderr
|
||||
(`test/helpers/none-arm.ts`). It never fails the case and never writes
|
||||
`contract-violations.jsonl`; harness preconditions (the fixture built, the CLI
|
||||
ran, the session ended normally) still assert. This departs from "the same
|
||||
panel" above because these contracts are gstack's own rules: a plain model was
|
||||
never asked to keep them, so a failing `none` trial is the measurement, not a
|
||||
regression, and a red verdict would block the gate on behavior nobody
|
||||
specified. The router's `none` arm has no "routed to /investigate" metric,
|
||||
since no skill is installed to route to; it records plain metrics instead:
|
||||
whether the suite is green, whether an edit came before the first read, and
|
||||
how many read-only calls came before the first edit. The first paid comparison
|
||||
with these arms gets its own preregistration.
|
||||
|
||||
**Cases without a `none` arm.** `safety-pair-agent-block` and
|
||||
`safety-ios-demo-ui-only` refuse `GSTACK_SAFETY_ARM=none` with the reason
|
||||
(`NO_NONE_ARM` in `test/helpers/safety-rules.ts`). Their task exists only in
|
||||
the skill: relaying /pair-agent's remote instruction block, and /ios-qa's demo
|
||||
mode with its device routes. A plain prompt has nothing to relay and no device
|
||||
interface to drive, so the arm would measure the prompt, not gstack.
|
||||
|
||||
**Precedent.** `test/skill-e2e-arm-benchmark.test.ts` runs a `with-skill` and a
|
||||
`without-skill` arm (named `<model>#no-gstack` in its preregistration) as 3
|
||||
pairs per task under a spend ledger and cap. A paired judge reads both diffs as
|
||||
`A` and `B` in both orders, counts a win only when the orders agree, and
|
||||
decides only after it passes calibration; the per-diff `armJudge` stays beside
|
||||
it as the diagnostic. Candidate folders and git identity name a project, never
|
||||
the arm. The model panel (`test/skill-e2e-model-panel.test.ts`) runs a
|
||||
`<model>#no-gstack` arm beside each model (GST-468, GST-469).
|
||||
|
||||
## Running evals as an agent: detach
|
||||
|
||||
|
||||
+42
-1
@@ -248,12 +248,20 @@ a push included. The sequence that works unattended on Capy:
|
||||
([base_red](ship-policy.md#base-red)): the lane is waiting, not failed. It
|
||||
registers `gstack-base-red wait --pause <file>` as a background operation,
|
||||
tells the controller which cases hold it, and on `BASE_GREEN` runs
|
||||
`/ship --resume <record>`; no READY goes out on the conditional receipt.
|
||||
`/ship --resume <record>`; no READY goes out on the conditional receipt,
|
||||
except on a train with `gates` ([Merge trains for other repos](#merge-trains-for-other-repos)).
|
||||
9. READY: `gstack-ship-queue ready --pr <n> --pregate <pregate.json> --screen|--no-screen`;
|
||||
send the printed block.
|
||||
10. After the slot call, the stamp commit keeps the pin's receipt; re-READY with the same
|
||||
receipt, no second /ship.
|
||||
|
||||
When /ship runs in a subagent, the parent subscribes to the pull request's check and
|
||||
review notices as soon as the subagent opens it, and stops the /ship subagent only once
|
||||
that subscription is in place and its READY arrives. From then on the parent owns the pull
|
||||
request: red checks, human review, the slot stamp and re-READY of step 10, and any receipt
|
||||
carry. It does that work itself or sends it to the subagent as an explicit instruction. On
|
||||
Capy the mechanics are in [the gstack skill](../capy/skill/gstack/SKILL.md#plan-approve-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
|
||||
@@ -292,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`
|
||||
@@ -323,6 +343,27 @@ head while i merges); the severe race (several severe lanes told at once that th
|
||||
urgent train is theirs, first complete READY takes it; never a race for a slot inside a
|
||||
built train); whether a READY also carries a gate run id and tree hash.
|
||||
|
||||
### Merge trains for other repos
|
||||
|
||||
A repository without gstack's eval workflow declares `gates` in its train block
|
||||
([ship-policy.md](ship-policy.md#train-gates)): its own CI workflows, a test
|
||||
command on a Ubicloud VM, or both. Every prefix is gated, so a red train names
|
||||
its culprit with no bisect and no rebuild. To turn one on, run `train init` in
|
||||
a checkout, commit the printed block (and the workflow snippet) on the base
|
||||
branch through a PR, then run `train doctor` until it reports no findings.
|
||||
|
||||
The controller's loop shortens to `train build --prs <pins>`, then
|
||||
`train watch --train <nonce> --land`, which waits for every prefix run, reads
|
||||
the verdict and lands each landable slot in order. Lanes READY with
|
||||
`ready --pr <n> --no-screen`; there is no screen and no slot call. A lane whose
|
||||
/ship paused on a red base READYs with `--base-red --fixer <pr>`, its fixer with
|
||||
`--fixes-base-red <cases>`, and the train seats the fixer at slot 1. The `update` commit `train land` pushes to a PR
|
||||
branch (the gated tree on [pin, main]) is the one exception to CREW rule 14's "never merge
|
||||
the base into a PR": it is tool-made, recorded before the push, and pushed under the pin's
|
||||
lease, like a stamped train's stamp commit. A peer crew
|
||||
adopting this starts from
|
||||
[captain/templates/merge-train-peer.md](../captain/templates/merge-train-peer.md).
|
||||
|
||||
## Approvals: three shapes
|
||||
|
||||
**A. The record.** Every lane keeps an `## Approvals` table in its report and PR body,
|
||||
|
||||
@@ -145,8 +145,11 @@ archive bytes, not a verified response URL.
|
||||
Image publication starts with the committed
|
||||
[`build-inputs.json`](../lib/cso/images/build-inputs.json). Its reviewed revision
|
||||
pins distinct native manifests for every stack and the SBOM generator. Each
|
||||
profile records the inspected source tag and immutable index for provenance,
|
||||
then `baseImages` keyed by `linux/amd64` and `linux/arm64`; Python additionally
|
||||
profile records the inspected source tag and immutable index for provenance.
|
||||
Every source tag (each base, Python's uv and the SBOM generator) must name an exact release (`<major>.<minor>.<patch>`, or
|
||||
`<major>.<minor>` for PostgreSQL); a floating major or minor tag, `latest` or
|
||||
another alias fails matrix generation with `FLOATING_<FAMILY>_SOURCE`. Each profile then records
|
||||
`baseImages` keyed by `linux/amd64` and `linux/arm64`; Python additionally
|
||||
records native uv manifests. CI fails if a source tag no longer resolves to the
|
||||
reviewed index, a platform manifest is absent from that index, its image
|
||||
configuration names another architecture, or an executable reports a different
|
||||
|
||||
@@ -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).
|
||||
|
||||
+155
-7
@@ -88,6 +88,8 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`).
|
||||
| `stall_timeout_s` | seconds a test command may print nothing before its process group is killed and the check is `incomplete` (`PREGATE_STALLED`); default 300, `0` disables; `--stall-timeout` overrides; the `--remote` gate is exempt (see [supervision](#supervision)) |
|
||||
| `timeout_s` | seconds each runner process may take (the unit-file group, each serial file, each guard without its own `timeout_s`, and the remote gate's deadline) when `--timeout` is not passed; 1 to 3600, default 600. `gstack-ship-preflight` and the `finish` fallback run pregate without `--timeout`, so this is how a repo with long lanes sets theirs |
|
||||
| `remote_gate` | the repo's remote test gate command; under 8 cores the local test tier becomes a `requires-remote` obligation naming it, and `--remote` runs it (see [remote gate](#remote-gate)) |
|
||||
| `run_profiles` | name → `{ workflow, events, jobs, artifact, max_age_hours?, os? }`: the GitHub Actions runs `clear --run` verifies as evidence on the identical tree; a profile named after a lane (`free-tests.yml/macos-named-regressions`) clears that lane (see [run evidence](#run-evidence)) |
|
||||
| `remote_gate_runs` | `{ profiles, inventory }`: the run profiles that together clear `pregate.json/remote_gate`, and the command that prints the remote gate's test files, one per line, which their runs must cover (see [run evidence](#run-evidence)) |
|
||||
| `allowlist` | `[{ finding, paths?, pattern?, reason }]`: benign findings the `secrets` check accepts and names as allowlisted, by gstack-redact finding id or non-secret category, path globs and a pattern anchored at the finding; never `env.value` (see [allowlist](#allowlist)) |
|
||||
|
||||
Only the `origin/<base>` copy or the user-owned file below is honoured, so a
|
||||
@@ -96,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
|
||||
@@ -112,9 +114,10 @@ the caller's environment, never the checkout's `.env`: the bin's shebang passes
|
||||
`--no-env-file`, and a bare `bun gstack-pregate` launched beside any `.env` file
|
||||
refuses (`POLICY_SOURCE_UNAVAILABLE`). `verify` reads both paths from the
|
||||
record, not from its own environment.
|
||||
It covers `gstack-pregate run` only: `sealed` and `price_file` are read from the
|
||||
base branch's copy, so the user file refuses them (`PREGATE_CONFIG_INVALID`)
|
||||
rather than holding a policy nothing reads. A user file that exists but cannot
|
||||
It covers `gstack-pregate run` only: `sealed`, `price_file`, `run_profiles` and
|
||||
`remote_gate_runs` are read from the base branch's copy, so the user file refuses
|
||||
them (`PREGATE_CONFIG_INVALID`; an empty `run_profiles` is accepted) rather than
|
||||
holding a policy nothing reads. A user file that exists but cannot
|
||||
be read, or a symlink on its path that points nowhere, refuses the same way
|
||||
instead of falling back to defaults. A base copy git cannot read (and
|
||||
`git ls-tree` cannot confirm is absent) refuses with `POLICY_SOURCE_UNAVAILABLE`
|
||||
@@ -175,7 +178,11 @@ Two stages with separate clocks, both printed:
|
||||
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.
|
||||
category, fails; other MEDIUM findings warn. Cannot be downgraded. A
|
||||
changed Python (`.py`, `.pyi`) or Dart file is scanned whole and only
|
||||
findings on its added lines count, so a line added inside an existing
|
||||
`"""` or `'''` string is still read as a literal; its rows give the file's
|
||||
line number (`app.py, line 10`).
|
||||
- `strays`: a new file at the repository root, a new top-level directory, or a
|
||||
scratch name (`zz-*`, `tmp*`, `scratch*`) anywhere.
|
||||
- `literals`: for every changed exported constant or named timeout/retry
|
||||
@@ -435,7 +442,7 @@ the selected local files as a `requires-remote` obligation with the command to
|
||||
run, `pregate.json/remote_gate` (the declared command) or
|
||||
`test_backend/<backend>` (gstack's `scripts/ubicloud/ubi-runner.sh run --src
|
||||
<repo> -- '<runner> <files>'`). Without either, the tier runs locally under
|
||||
the supervision above. Close the obligation one of two ways:
|
||||
the supervision above. Close the obligation one of three ways:
|
||||
|
||||
- after running it, `gstack-pregate clear --remote pregate.json/remote_gate
|
||||
--receipt <file>` with the gate's output, validated as `--remote` validates it
|
||||
@@ -444,6 +451,10 @@ the supervision above. Close the obligation one of two ways:
|
||||
accepted and recorded, see [below](#remote-gate-stamp-only)), or
|
||||
`gstack-pregate clear --remote test_backend/<backend> --run-url <url>`, which
|
||||
has no machine-checkable receipt and is recorded as `attested:<url>`; or
|
||||
- when CI already ran the full suite on this tree, `gstack-pregate clear --remote
|
||||
pregate.json/remote_gate --run <id|url> [--run …] --allow-repo-commands`, one
|
||||
verified GitHub Actions run per `remote_gate_runs` profile (see
|
||||
[run evidence](#run-evidence)); or
|
||||
- `gstack-pregate --stage tests --remote --spend-cap <usd> --allow-repo-commands`,
|
||||
which runs the declared `remote_gate` now (under `--policy-from head`, still the trusted copy's gate;
|
||||
a `test_backend` without `remote_gate` cannot be run this way). The gate runs
|
||||
@@ -611,6 +622,129 @@ Every refusal line starts with a reason code:
|
||||
| `PREGATE_REMOTE_INFRA` | 4 | create, setup, SSH or the stop deadline failed, or the runner's `UBICLOUD: left=` line is not `0`; the VM, its teardown line and `spent=` follow |
|
||||
| `PREGATE_REMOTE_INTERRUPTED` | 130 | TERM or INT; ubi-runner tears the VM down on its own |
|
||||
|
||||
<a id="run-evidence"></a>
|
||||
## Run evidence: one full suite per tree (`clear --run`)
|
||||
|
||||
A repository that runs its full suite in CI and again as its remote gate on the
|
||||
same tree paid twice for one result (gbrain: a 30–45 min Ubicloud gate, then an
|
||||
18–20 min full-ci dispatch, every serial slot). `clear --run` lets a passed
|
||||
GitHub Actions run stand in for the gate, on the runner's own record and never
|
||||
on prose: a pasted URL is only a pointer the verifier resolves.
|
||||
|
||||
**Declare it on the base branch.** In `.gstack/pregate.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"run_profiles": {
|
||||
"test": { "workflow": "test.yml", "events": ["workflow_dispatch", "push"], "jobs": ["full-suite"], "artifact": "full-suite-tree" },
|
||||
"e2e": { "workflow": "e2e.yml", "events": ["workflow_dispatch", "push"], "jobs": ["e2e-full-suite"], "artifact": "e2e-full-suite-tree" }
|
||||
},
|
||||
"remote_gate_runs": { "profiles": ["test", "e2e"], "inventory": "bash scripts/pregate-remote-gate.sh --list" }
|
||||
}
|
||||
```
|
||||
|
||||
Profiles are read only from `origin/<base>`, so a pull request can tighten
|
||||
them but never relax them. That covers the profile, not the suite it names: the
|
||||
workflow file a run executed and the inventory command are the branch's own, as
|
||||
the remote gate's script already is, so a diff to either is reviewed like any
|
||||
other gate change. Each profile names the workflow file, the events it
|
||||
accepts, every job that must have passed (each exactly once), and the artifact
|
||||
holding the run's `tree.json`. `max_age_hours` (1–168, default 24) bounds how
|
||||
old the run may be, and `os` (`Linux`, `macOS`, `Windows`) requires
|
||||
`tree.json` to record that runner OS. Each workflow uploads `tree.json` from
|
||||
its own job: `run_id`, `run_attempt`, `event`, `sha` (or `head_sha`),
|
||||
`tree` (`git rev-parse HEAD^{tree}` as tested), `files` (every test file the
|
||||
run executed), `tests` (the count) and, with `os`, the runner OS.
|
||||
|
||||
**What `clear --run` checks** (`lib/run-evidence.ts`), for every run, online,
|
||||
once:
|
||||
|
||||
- The run belongs to this repository and ran for a same-repository head. Its
|
||||
workflow file is the profile's, its event is `push` or `workflow_dispatch`
|
||||
(a `pull_request` run tests the merge commit, not the head tree, so no
|
||||
profile may accept it), and it completed with success. The API answers with
|
||||
the newest attempt, so an earlier green attempt under a newer red one is
|
||||
refused, and the run is read again after the evidence is collected: a new
|
||||
attempt meanwhile is `run_changed`.
|
||||
- Every job the profile names appears exactly once in that attempt and
|
||||
succeeded. Jobs and runs are read across every page.
|
||||
- `tree.json` (an artifact of at most 8 MiB) names this run, attempt, event and
|
||||
head (`sha` and `head_sha`, each checked when present). It is not a reuse
|
||||
record (a reuse is never evidence). Its tree equals the tree git computes for
|
||||
the run's head commit. `files` is a list of paths and `tests` a
|
||||
non-negative integer when present; without `tests` the file count stands in.
|
||||
- No run of the same workflow on that head, or on pregate.json's own head,
|
||||
whose newest attempt started at or after the second this run was created
|
||||
failed, was cancelled or is still running (`run_started_at`, so a red re-run
|
||||
of an older run counts, a run started in the same second counts, and
|
||||
re-running this run does not hide a failure in between).
|
||||
- The run finished within `max_age_hours` (its jobs' latest `completed_at`,
|
||||
else the run's `updated_at`), and on the profile's OS when it names one.
|
||||
Re-running only the failed jobs refuses (`artifact_mismatch`) unless the job
|
||||
that uploads `tree.json` re-runs too, because that file must name the
|
||||
newest attempt. The 8 MiB cap is read from the artifact's zipped size before
|
||||
download, so a workflow on the branch itself can still unpack a larger file
|
||||
into the temporary directory.
|
||||
|
||||
Then, for the lane:
|
||||
|
||||
- pregate.json was recorded against this repository's base, whose profiles
|
||||
apply (`base_mismatch` otherwise).
|
||||
- Each profile gets its own run (a run id given twice is a usage error), and
|
||||
every run verifies under its profile. The assignment is searched, so a run
|
||||
valid for two profiles cannot starve the other. A lane with a profile needs
|
||||
one run; `pregate.json/remote_gate` needs one per `remote_gate_runs`
|
||||
profile.
|
||||
- All runs tested one tree: pregate.json's tree, or a stamp-only predecessor of
|
||||
it under exactly the rule [above](#remote-gate-stamp-only) (a policy whose
|
||||
`stamp_sensitive_gates` names `tests` refuses that substitution).
|
||||
- Every run executed something (`no_tests` names one that did not), and the
|
||||
runs' `files` together cover the obligation's selected test files, plus, for
|
||||
the remote gate, every file the `inventory` command prints. The inventory is
|
||||
a pin-file command, so it runs only with `--allow-repo-commands`, and only
|
||||
while the working tree is pregate.json's tree (checked before and after it
|
||||
runs); an empty or failing inventory refuses, and it runs under `pipefail`,
|
||||
so a pipeline whose first command fails refuses too.
|
||||
- Every run is read once more after the inventory: a run that moved to a new
|
||||
attempt or stopped being a success meanwhile refuses (`run_changed`).
|
||||
|
||||
Any API or git error refuses. Each refusal prints `REUSE: refused
|
||||
reason=<code> — …` as `PREGATE_RECEIPT_INVALID`, and a run that fails
|
||||
verification is named with its own reason (`fork_run`, `event_mismatch`,
|
||||
`run_not_success`, `job_missing`, `artifact_mismatch`, `artifact_reused`,
|
||||
`tree_mismatch`, `newer_run_vetoes`, `run_changed`, `run_stale`,
|
||||
`os_mismatch`, `api_error`, …); the clear-level codes are `base_mismatch`,
|
||||
`no_verified_run`, `tree_mismatch` (runs on different trees),
|
||||
`tree_not_stamp_only`, `no_tests`, `inventory_failed`, `inventory_empty`,
|
||||
`inventory_uncovered` (the remote gate) and `tests_uncovered` (a lane's own
|
||||
selected tests). A wrong run count, a repeated run or a lane with no
|
||||
profile is a usage error (exit 2), not a REUSE refusal.
|
||||
|
||||
Accepted, `clear` prints `REUSE: accepted <lane> <receipt>`, and the receipt
|
||||
reads `remote-gate tree=<t12> tests=<n> runs=<id>/<attempt>,… profiles=<p>,…
|
||||
heads=<h12> verified=<time>` (with `accepted-for=` and the substitution for a
|
||||
stamp-only predecessor). A lane with its own profile gets `run tree=… runs=…`
|
||||
the same way; for such a lane `--run-url` is verified as `--run` is, and
|
||||
`--bundle-lane` is refused. A lane with no profile keeps today's `--run-url`
|
||||
recording. When the base config or the GitHub origin cannot be read, `--run`
|
||||
fails, and so do `--run-url` and `--bundle-lane` unless the base was read and
|
||||
shows the lane has no profile; then the lane is recorded as before, with a
|
||||
warning naming the cause. `gstack-ship-queue ready --dispatch-ci` still
|
||||
records its dispatched runs by URL; routing it through the verifier is the next
|
||||
change (P3b), so a repository that declares lane profiles clears those lanes
|
||||
with `gstack-pregate clear --run`.
|
||||
|
||||
**`verify` stays offline.** The check ran at `clear`. `gstack-pregate verify`,
|
||||
and `gstack-ship-queue ready` through it, refuse a run receipt whose `tree=`
|
||||
is not pregate.json's tree unless a substitution for it is recorded. They make
|
||||
no network call, so GitHub's availability and the run's age never sit on the
|
||||
merge path.
|
||||
|
||||
**What is never reusable.** Security and advisory scans (osv, audit,
|
||||
gitleaks) depend on the day, not on the tree, so they are not run profiles:
|
||||
they run on every head as before. Pull-request required checks always run
|
||||
fresh.
|
||||
|
||||
<a id="guards"></a>
|
||||
## Guards and the lane-done rule
|
||||
|
||||
@@ -672,6 +806,20 @@ those, each with its scope and its reason:
|
||||
the entry is inert for that finding and the row says so (`allowlist[0]
|
||||
env.kv not tested on env.kv (<file>): the scanner gave no position for its
|
||||
pattern; the finding is kept`) rather than silently keeping it.
|
||||
- The position is where the flagged value starts, not the name before it.
|
||||
For `session: HttpSession` in a YAML file the pattern is `HttpSession`;
|
||||
`session: HttpSession` never matches there. An entry that names a finding
|
||||
by id and path, and whose pattern matches the line but not at the value,
|
||||
prints `allowlist[0] env.kv pattern did not match env.kv (<file>, added
|
||||
line N, col C): …; the finding is kept`.
|
||||
- In Python and Dart source, a parameter or field annotation
|
||||
(`session: HttpSession,`) and a dict entry or Dart named argument that reads
|
||||
a name (`"password": request.password`) are code, not credentials, as they
|
||||
are in TypeScript (#3630). Python's keyword form (`password=request.password`)
|
||||
still reports. The same line inside a `"""` docstring is text and still
|
||||
reports; allowlist it by its value. The pre-push hook sees only added lines,
|
||||
so it scans Python and Dart lines without this exemption and may still warn
|
||||
(MEDIUM, non-blocking) on them.
|
||||
|
||||
The `secrets` row says `N allowlisted (<ids>)` and lists each allowlisted
|
||||
finding with the entry (`allowlisted env.kv (evals/receipts/t1.json, added line
|
||||
|
||||
+16
-14
@@ -34,21 +34,23 @@ needed, because every skill reads `PROACTIVE` from `gstack-skill-start` at the
|
||||
start of each run and the router (`/gstack`) applies it to its routing rules. A
|
||||
value gstack does not know (a hand-edited `proactive: maybe`) reads as `core`.
|
||||
|
||||
Two surfaces follow the setting only in part. The first is the `## Skill routing`
|
||||
section gstack offers to append to your project's CLAUDE.md. Under `off` the offer is
|
||||
not made, because those rules are the auto-invocation you turned off (a section
|
||||
already in CLAUDE.md stays; delete it yourself). Under `core` the offered
|
||||
section still lists every routing rule and tells the host to invoke any
|
||||
matching skill. If you accepted it before the `core` default, trim that section
|
||||
to the four core rules (or delete it and let gstack offer it again); making the
|
||||
offered section follow `core` is tracked in #3446.
|
||||
The `## Skill routing` section gstack offers to append to your project's
|
||||
CLAUDE.md follows the setting too. Under `core` it lists only the four core
|
||||
rules and says the other skills run when you type them or a running skill
|
||||
hands off to them; under `all` it lists
|
||||
every routing rule; under `off` it is not offered, because those rules are the
|
||||
auto-invocation you turned off. gstack offers the section once per project and
|
||||
never edits it afterwards. So a section you accepted earlier, or under another
|
||||
setting, stays as it is: trim it to match (the core four are /office-hours,
|
||||
/review, /qa and /ship), or delete it and let gstack offer it again.
|
||||
|
||||
The second is the full skill descriptions, which a `--catalog-mode=full` build
|
||||
and the non-Claude hosts keep. Non-core skills still say "Proactively suggest
|
||||
when ..." there, and that text does not change with the setting, so a host
|
||||
that picks skills from those descriptions can still reach them under `core`.
|
||||
The default Claude build uses one-line descriptions without that sentence.
|
||||
#3446 tracks that too.
|
||||
Skill descriptions cannot change with the setting, so they are written for the
|
||||
`core` default. Only the four core skills say "Proactively suggest ..." in the
|
||||
full descriptions that a `--catalog-mode=full` build and the non-Claude hosts
|
||||
keep. The other skills keep their "Use when asked to ..." sentences, which
|
||||
describe asks. On those hosts, `all` therefore loses the proactive hint the
|
||||
non-core descriptions used to carry; the router's routing rules still apply when
|
||||
`/gstack` runs.
|
||||
|
||||
gstack asks once, after the telemetry question, which of the three you want;
|
||||
if you stored `off` before that, it takes the answer as given and does not ask.
|
||||
|
||||
+14
-2
@@ -271,8 +271,9 @@ the specialists, audits and watchers at the host default too
|
||||
### Behavior changes
|
||||
|
||||
- Pass 2 after a blocking fix on a diff below full depth, with a fix range of at
|
||||
most 150 runtime lines, is a [scoped pass](#scoped-pass): a fix-check, the Red
|
||||
Team and the native adversarial pass on the range instead of the specialist army.
|
||||
most 150 runtime lines, is a [scoped pass](#scoped-pass): a fix-check and the Red
|
||||
Team on the range, plus the native adversarial pass (on the range once Step 11 has
|
||||
run, otherwise on the whole branch), instead of the specialist army.
|
||||
- A tests-only diff (runtime 0) no longer dispatches the Testing specialist: the
|
||||
core checklist and the gate scan review it.
|
||||
- Every depth dispatches one batched specialist instead of one reader per
|
||||
@@ -515,6 +516,17 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
missing or doubled title stops the pass. On Capy the pass runs as one call to the
|
||||
checked-in program `lib/capy-review-pass.js`, served by `program` and loaded
|
||||
unchanged by the gstack Capy skill.
|
||||
7. Every brief, audit readers' included, ends with a workspace paragraph naming the
|
||||
checkout the bins run in (GST-482): a reader on a shared machine starts in the
|
||||
machine's working directory, so it is told to `cd` there on every command, to give
|
||||
every file tool an absolute path under it, and to start its reply with
|
||||
`REVIEWED_TREE: <HEAD^{tree}> <toplevel>` measured there. The paragraph follows the
|
||||
section's render, which stays byte for byte. `collect` and `mark --state collected`
|
||||
compare that line with the fingerprint recorded at render: a missing or foreign line is
|
||||
`COLLECT_WRONG_TREE`, a later commit in the same checkout `COLLECT_TREE_MOVED`; nothing
|
||||
is merged, the reader becomes `failed`, and `verify` reports `NOT_COLLECTED`. An
|
||||
outside voice's tree failure is recorded `unavailable` only when its reply holds no finding. The line catches a reader that
|
||||
worked elsewhere by accident; it is not proof of reading, since diff text can predict it.
|
||||
|
||||
## Where this is enforced
|
||||
|
||||
|
||||
+207
-8
@@ -60,7 +60,7 @@ refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
|
||||
| `review_scope` | `frozen` \| `diff-plus-blocking` \| `all` | `frozen` | The finding scope of `/ship`'s review loop; see [review_scope](#review-scope). `frozen` fixes the diff on pass 1, files blocking findings in unchanged code as issues, and later passes read only the range since the previous pass. `diff-plus-blocking` is the same but fixes a blocking finding in unchanged code in this PR instead of filing it. `all` is the earlier loop: every finding is fixed on every pass. Needs `min_gstack_version` when not `frozen`. |
|
||||
| `min_gstack_version` | version or `null` | `null` | The oldest gstack that may read the file; see the compatibility rule above. |
|
||||
| `docs` | `{ caps: [{ path, max_bytes }] }` or `null` | `null` | Documentation files with a byte cap. `/ship` Step 14.5 runs `gstack-ship-receipt docs-caps --base <base>` (the caps come from `origin/<base>`'s policy, never the branch's copy; `--policy <file>` is for a repository bootstrapping its policy) before the verification gate and prints one `DOCS_CAP: <path> <bytes>/<max> ok\|over` line per cap; any `over` (an absent file counts) exits 1 and blocks the ship until the file is trimmed or the cap raised on the base branch (`DOCS_AUDIT_BLOCKED`). At most 64 caps, each path contained. See [the ship receipt](#ship-receipt). Needs `min_gstack_version`. |
|
||||
| `train` | `{ gate_workflow, free_workflow, full_gate_check, record_artifact, max_size, fill_hours, screen_selection, trial_reservation_usd }` or `null` | `null` | Turns on [merge trains](#run-a-merge-train), read from the base revision only. Every field is required and an unknown field is `POLICY_INVALID`: `max_size` 1 to 6, `fill_hours` above 0 and at most 24, `screen_selection` `gstack-paid` \| `none`, `trial_reservation_usd` above 0 and at most 100. `null` keeps the per-PR queue exactly as it is. Needs `min_gstack_version`. |
|
||||
| `train` | `{ gate_workflow, free_workflow, full_gate_check, record_artifact, max_size, fill_hours, screen_selection, trial_reservation_usd, land? }`, `{ gates, land, max_size, fill_hours }` or `null` | `null` | Turns on [merge trains](#run-a-merge-train), read from the base revision only. Every field is required and an unknown field is `POLICY_INVALID`: `max_size` 1 to 6, `fill_hours` above 0 and at most 24, `screen_selection` `gstack-paid` \| `none`, `trial_reservation_usd` above 0 and at most 100. A block with `gates` ([any repository](#train-gates)) takes `max_size` 1 to 5 and needs `min_gstack_version` at least `TRAIN_GATES_KEY_VERSION`. `null` keeps the per-PR queue exactly as it is. Needs `min_gstack_version`. |
|
||||
| `base_red` | `{ setup, select, timeout_sec }` or `null` | `null` | How `/ship` Step 5 proves a failure is the base branch's own before pausing on it; see [base_red](#base-red). Read from the base revision only. Needs `min_gstack_version`. |
|
||||
| `changelog_fragments` | `{ dir, required_checks? }` or `null` | `null` | PRs carry unversioned [CHANGELOG fragments](#changelog-fragments) in `dir` (one flat, repo-relative directory, no `..`) and the merge step ([`land`](#land)) assigns the version. `required_checks` (up to 50 distinct names) are the checks [`train land`](#stamped-trains-under-changelog_fragments-train-land) requires present and green on a stamp commit. `null` keeps `[Unreleased]` sections and per-PR stamps. Needs `min_gstack_version`. |
|
||||
|
||||
@@ -345,13 +345,14 @@ The surface is off unless the base revision's `.gstack/ship-policy.json`
|
||||
declares a `train` block. Without one, `train …` and `verify --train` refuse
|
||||
with `QUEUE_TRAIN_OFF`, and `ready --pr <n>` prints exactly the per-PR READY
|
||||
block above (`--screen`, `--no-screen` and `--broad` are ignored with a
|
||||
warning on stderr). A block for gstack itself would read:
|
||||
warning on stderr). A repository without gstack's eval workflow declares
|
||||
[`gates`](#train-gates) instead. A block for gstack itself would read:
|
||||
|
||||
```json
|
||||
"train": {
|
||||
"gate_workflow": "evals.yml", "free_workflow": "free-tests.yml",
|
||||
"full_gate_check": "gstack/full-gate", "record_artifact": "gstack-train-record",
|
||||
"max_size": 3, "fill_hours": 1, "screen_selection": "gstack-paid",
|
||||
"max_size": 5, "fill_hours": 1, "screen_selection": "gstack-paid",
|
||||
"trial_reservation_usd": 1.5
|
||||
},
|
||||
"min_gstack_version": "<the version that introduced train>"
|
||||
@@ -439,6 +440,199 @@ covers main only when its exact tree is some S_i of a green record (plain
|
||||
T_i records still cover modulo stamps). A dropped slot changes every later
|
||||
version, so the train is rebuilt, as today.
|
||||
|
||||
<a id="train-gates"></a>
|
||||
### Trains for any repository: `gates`
|
||||
|
||||
A repository without gstack's eval workflow declares its own gates instead of
|
||||
`gate_workflow` and the other gstack-evals keys. Such a train gates every
|
||||
prefix, not just its tip, so a red train names its culprit without a bisect:
|
||||
|
||||
```json
|
||||
"train": {
|
||||
"gates": [
|
||||
{ "kind": "workflows", "workflows": ["ci.yml"], "required_jobs": ["ci.yml/test", "ci.yml/lint"] },
|
||||
{ "kind": "command", "command": "bun test", "timeout_sec": 1800, "spend_cap_usd": 3 }
|
||||
],
|
||||
"land": { "merge_method": "squash", "required_checks": ["test", "lint"] },
|
||||
"max_size": 5, "fill_hours": 3
|
||||
},
|
||||
"min_gstack_version": "1.92.0.0"
|
||||
```
|
||||
|
||||
A block with `gates` holds exactly these four keys; a gstack-evals key beside
|
||||
them is refused, and a block without `gates` is the gstack-evals kind and
|
||||
behaves exactly as above. `min_gstack_version` must be at least
|
||||
`TRAIN_GATES_KEY_VERSION` (lib/ship-policy.ts), so an older gstack refuses the
|
||||
policy by name. `max_size` is at most 5, because each gate runs on k+1
|
||||
prefixes.
|
||||
|
||||
- **`workflows` gates.** Each workflow is dispatched on a pushed
|
||||
`train-prefix/<nonce>/t<i>` ref with a `nonce` input; the train records the
|
||||
nonce before it sends the dispatch and adopts only the workflow_dispatch run
|
||||
whose run-name carries `[eval-bg <nonce>]`, never a run found by commit.
|
||||
A prefix is GREEN only when that run tested the prefix head exactly, every
|
||||
`required_jobs` entry (`<workflow file>/<job name as GitHub reports it>`)
|
||||
concluded success and the run concluded success. With `record` set (an
|
||||
artifact name), the run must also upload a JUnit XML or `{ "tests": [...] }`
|
||||
JSON file reporting at least one executed test, and failed test ids become
|
||||
the red cases. A red run gets one `gh run rerun --failed`: green on the
|
||||
rerun is GREEN with a `FLAKY:` line, red again is real, and both attempts'
|
||||
jobs stay in the record. `post_merge_checks` names checks that must succeed
|
||||
on main after each land before the next slot lands (`verify --post-merge
|
||||
<sha> --checks <names>`; `train land` checks the previous slot's merge and
|
||||
refuses the next slot until they are green, and `train watch --land` runs it
|
||||
between slots). A workflow whose concurrency
|
||||
group is not keyed by `github.ref` (or the nonce) gets one prefix at a
|
||||
time.
|
||||
- **`command` gates.** Each prefix is checked out clean and detached and runs
|
||||
`command` on a Ubicloud VM through scripts/ubicloud/ubi-runner.sh
|
||||
(standard-16, `UBI_OWNER` required). The VM's `git write-tree` must equal
|
||||
the prefix tree, and exit 0 is GREEN. Only names in `pass_env` reach the VM
|
||||
(`UBICLOUD_*` never). Each run reserves its timeout plus 10 minutes at the
|
||||
list price, and each command gate's `spend_cap_usd` covers every run of that
|
||||
gate in the train, reruns included (3 dollars covers six prefixes and one rerun at 1800 s; `train init`
|
||||
prints that default and `train doctor` flags a cap below a full train). The
|
||||
next run past the cap refuses (`QUEUE_TRAIN_SPEND_CAP`); raising the cap is a
|
||||
policy commit that moves main, so it is followed by a new train.
|
||||
|
||||
**The verdict.** T0 red is red on main (`QUEUE_TRAIN_RED_ON_MAIN`, fix
|
||||
forward). Otherwise the first non-green prefix decides: incomplete stops the
|
||||
train, red with a later prefix green is unattributed (repair or a new order,
|
||||
never an unchanged rebuild), and red with every later prefix red names its
|
||||
slot as the culprit (`QUEUE_TRAIN_RED`); the slots before it may land.
|
||||
`train bisect` and `train rebuild` refuse such trains, since every prefix
|
||||
already has its verdict. A green T0 run of the same gate at main's exact
|
||||
commit in another train is reused instead of run again.
|
||||
|
||||
**Base-red PRs.** A lane whose /ship paused on a red base (its receipt carries
|
||||
`base_red`) READYs with `ready --pr <n> --no-screen --base-red --fixer <pr>`:
|
||||
the condition must have been classified on today's base and not have expired.
|
||||
The fixer READYs with `--fixes-base-red <case,...>`. The fixer rides at slot 1
|
||||
(moved there unless `--order` says otherwise, which then refuses), T0 may be
|
||||
red on exactly the fixer's cases and nothing else, and every conditional PR
|
||||
behind it is gated on its own prefix with nothing excluded. A conditional PR
|
||||
without its fixer at slot 1, or on a gstack-evals train, refuses
|
||||
(`QUEUE_TRAIN_BASE_RED`, `QUEUE_BASE_RED_NO_TRAIN`).
|
||||
|
||||
**Landing.** `train land` publishes T_i, or S_i when the train stamps
|
||||
(`changelog_fragments`, or `gates` with `queue_mode: stamp-at-merge`), with
|
||||
main's tree equal to the prefix before it. It records the mode, and the update
|
||||
commit when there is one, before its first remote effect:
|
||||
|
||||
1. `direct`: `gh pr merge --<merge_method> --match-head-commit <pin>`, used
|
||||
only when branch protection is readable and not strict, the pin merges onto
|
||||
main as the gated tree exactly, and the pin's own checks (every
|
||||
`required_checks` name) are green.
|
||||
2. `update`: the gated tree committed on [pin, main], pushed under the pin's
|
||||
lease, its checks waited for, then merged at that commit. It is forced for
|
||||
a stamped train, strict or unreadable protection, a conditional PR, a merge
|
||||
that resolved release files, and a pin whose checks are red. `rebase`
|
||||
always refuses (`QUEUE_TRAIN_LAND_METHOD`): it would replay commits, and
|
||||
the replayed tree is not the gated one.
|
||||
|
||||
Required checks are `changelog_fragments.required_checks`, else
|
||||
`train.land.required_checks`. Fork heads refuse. Either way main's tree must
|
||||
be the prefix afterwards, or the queue freezes as above; the backstop
|
||||
dispatches each gate workflow on main by nonce. `train watch --train <nonce>
|
||||
--land` runs build, verdict and every landable slot's land in order, and stops
|
||||
at the first refusal.
|
||||
|
||||
**Setting one up.** `train init [--command "<test command>"]` reads branch
|
||||
protection, the base's workflows and the open PRs, and prints a block mapping
|
||||
each required check to its workflow job, the `nonce` input, run-name and
|
||||
concurrency snippet each gate workflow needs, and the land mode slots will
|
||||
use. It recommends `changelog_fragments` when open PRs bump the version, and
|
||||
refuses (`QUEUE_TRAIN_INIT_REFUSED`) only when they do and the policy neither
|
||||
declares fragments nor stamps at merge. After the block lands on the base,
|
||||
`train doctor` re-checks it: a missing workflow, nonce input or run-name, a
|
||||
required job no workflow defines, a concurrency group not keyed by ref, a push
|
||||
trigger that matches `train-prefix/` refs, a workflow never green on the base,
|
||||
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,
|
||||
@@ -568,8 +762,8 @@ forked (the chain's merge onto the pin's own prefix is the verdict), refuses
|
||||
`QUEUE_TRAIN_CONFLICT` for a pin that does not merge, and otherwise prints
|
||||
`MERGE_CHECK: T1..T<k> merge clean onto <base>@<sha>`, so a
|
||||
`QUEUE_TRAIN_WAITING` result is also a clean dry-run merge of the pool (the
|
||||
crew template's sibling trial merge, done by the tool). gstack's own block fills at 3 PRs or one hour (it was 5 PRs or
|
||||
three hours until 2026-10-26, which left READY PRs waiting most of a day). The same pins on the same base return the live train
|
||||
crew template's sibling trial merge, done by the tool). gstack's own block fills at 5 PRs or one hour (it was 5 PRs or
|
||||
three hours until 2026-10-26, which left READY PRs waiting most of a day, then 3 PRs or one hour until 2026-10-31). The same pins on the same base return the live train
|
||||
instead of a second one; `train build --train <nonce>` resumes a train from
|
||||
its record. Refusals, all before any spend: `QUEUE_TRAIN_UNTRUSTED` (a fork
|
||||
head), `QUEUE_TRAIN_MOVED` (a head moved from its pin),
|
||||
@@ -774,7 +968,7 @@ repair. When verify confirms the repair merged, it prints `FREEZE: cleared`
|
||||
and `NEXT:` builds a train of the PRs still unmerged.
|
||||
|
||||
**5b. Coordinator: after the merge.** `gstack-ship-queue verify --post-merge
|
||||
<sha> [--timeout <s>]` (the merged commit's full 40-character SHA) watches
|
||||
<sha> [--timeout <s>] [--checks <a,b>]` (the merged commit's full 40-character SHA) watches
|
||||
every check run attached to it (the push workflows: free tests, the Windows
|
||||
subset, the main backstop) and every commit status until none is still
|
||||
running, prints one `CHECK: <name> <conclusion>` per check and
|
||||
@@ -783,7 +977,10 @@ Green needs at least one completed check and no failure; `neutral` and
|
||||
`skipped` (the backstop superseding itself) count as skipped; a pending
|
||||
commit status keeps the commit pending and a failed one makes it red. A
|
||||
failure is `QUEUE_POST_MERGE_RED` naming the checks; a timeout (default 2700 s) is
|
||||
`QUEUE_TRAIN_PENDING` with the same command as `NEXT:`. It keeps no process
|
||||
`QUEUE_TRAIN_PENDING` with the same command as `NEXT:`. With `--checks` (a
|
||||
workflows gate's `post_merge_checks`), each named check must also run and
|
||||
succeed: one that did not succeed is `QUEUE_POST_MERGE_RED`, and one that
|
||||
never ran is `QUEUE_TRAIN_PENDING` at the timeout. It keeps no process
|
||||
state, so rerunning it after a machine restart continues the watch.
|
||||
|
||||
**6. Clean up.** `gstack-ship-queue train prune [--train <nonce>] [--dry-run]`
|
||||
@@ -1080,7 +1277,9 @@ expiry), finishes every other step, and writes its receipt with `--base-red
|
||||
`expires_at`) makes it **conditional**. `gstack-ship-receipt result` prints
|
||||
`SHIP_PAUSED: base-red …` and `status=incomplete`; `ready`, `train build`,
|
||||
`verify --train`, `carry` and every other receipt consumer refuse a conditional
|
||||
receipt with `QUEUE_RECEIPT_CONDITIONAL`.
|
||||
receipt with `QUEUE_RECEIPT_CONDITIONAL`. The one exception is a train with
|
||||
`gates`: `ready --base-red --fixer <pr>` READYs the PR to ride behind its fixer
|
||||
([base-red PRs](#train-gates)).
|
||||
|
||||
To resume, a parent runs `gstack-base-red wait --pause <file>` as a background
|
||||
operation. On each new base head (`git ls-remote`) it reruns `setup` and
|
||||
|
||||
@@ -964,6 +964,9 @@ runs every row through the stub model for $0 and flags malformed rows
|
||||
(`EVAL_DRY_RUN_FAILED`); `pilot start|settle|release|report` runs three priced
|
||||
items, records actual spend, extrapolates to the full set against the cap
|
||||
(`EVAL_BUDGET_EXHAUSTED`) and prints the spend table the approval question shows.
|
||||
`results --check <prereg> <results.md>` refuses a finished results file unless one
|
||||
table holds a filled row for every arm, each `<model>#no-gstack` row beside its
|
||||
variants (`EVAL_REPORT_NO_GSTACK_ROW`); no number leaves the run before it passes.
|
||||
The skill's closing section lists the tier-3 items (instrument audit, steady
|
||||
state, sealed paths, off-machine persistence, memory preflight, power tables, QA
|
||||
soak) as `tier 3, after P7`; none of them run yet.
|
||||
|
||||
@@ -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
|
||||
|
||||
+360
-14
@@ -755,13 +755,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.`
|
||||
@@ -1109,6 +1163,28 @@ conflict yourself instead, finish or abort your merge (`git -C '<dir>' merge
|
||||
--abort`, `git -C '<dir>' checkout -- <file>`); gstack retries at the next
|
||||
check.
|
||||
|
||||
<a id="auto-update-tracked-by-project"></a>
|
||||
### `gstack auto-update: update held (tracked-by-project: ...)` / `(version-reverted: ...)` / `gstack-upgrade: tracked-by-project: ... (UPGRADE_REFUSED)` / `gstack-upgrade: version-reverted: ... (UPGRADE_REFUSED)`
|
||||
|
||||
**Meaning.** Your gstack install sits inside another git project, and that
|
||||
project has committed gstack's files as ordinary files. Every time you switch
|
||||
branches or check out files in that project, git puts its old copies of
|
||||
gstack's files back, so VERSION and other files fall behind the install's own
|
||||
HEAD while updates seem to succeed (#1022). `version-reverted` is the same
|
||||
symptom seen from the install: its VERSION disagrees with HEAD. Neither
|
||||
`gstack-upgrade`, `/gstack-upgrade` nor the auto-updater moves the install
|
||||
until it is fixed. A project that holds gstack as a git submodule is fine.
|
||||
`version-reverted` with no enclosing project means VERSION was edited by
|
||||
hand; only the `stash push` step below applies.
|
||||
|
||||
**Fix.** Print the exact commands with
|
||||
`bash <install>/bin/gstack-ff-update --preflight <install>`. They do three
|
||||
things: untrack the install in the project (`git rm -r --cached`), add it to
|
||||
the project's `.gitignore`, and commit both on every branch you check out
|
||||
(or vendor gstack as a submodule instead). Then
|
||||
`git -C <install> stash push` puts the install's own files back and keeps
|
||||
any edit you made in the stash, and the next update proceeds.
|
||||
|
||||
<a id="auto-update-autostash-unrecoverable"></a>
|
||||
### `gstack auto-update: update held (autostash-unrecoverable: <reason>); your local edits are where they were and nothing was installed. Look first: git -C '<dir>' status --short. Fix now (...)` / `autostash-unrecoverable: <reason>`
|
||||
|
||||
@@ -1551,6 +1627,8 @@ directory is not a gstack checkout, HEAD is detached or on another branch (a
|
||||
pin), a tracked file is modified, a local commit is not on the remote branch,
|
||||
the incoming release's Bun floor is above the Bun on PATH, or another updater
|
||||
holds `<state root>/.setup-lock`. It never stashes, resets or discards.
|
||||
A `tracked-by-project` or `version-reverted` cause has its own section:
|
||||
[an enclosing project tracks the install](#auto-update-tracked-by-project).
|
||||
|
||||
**Fix.** Resolve the named cause with the printed command (commit or move the
|
||||
listed changes, check out the default branch, move local commits to another
|
||||
@@ -3229,6 +3307,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`)
|
||||
@@ -3293,8 +3419,9 @@ repository's lockfile), then rerun `gstack-regen check` or the pre-gate.
|
||||
**Meaning.** `.gstack/pregate.json` has an unknown key, a wrong type, a glob
|
||||
that escapes the repository, a runner without `{files}`, or `secrets` in `warn`.
|
||||
The user-owned `<state root>/projects/<slug>/pregate.json` also refuses when it
|
||||
sets `sealed` or `price_file` (both are read only from the base branch's copy)
|
||||
or exists but cannot be read (a dangling symlink on its path counts).
|
||||
sets `sealed`, `price_file`, a non-empty `run_profiles` or `remote_gate_runs`
|
||||
(all read only from the base branch's copy) or exists but cannot be read (a
|
||||
dangling symlink on its path counts).
|
||||
|
||||
**Fix.** `gstack-pregate validate`. `secrets` and required-lane obligations
|
||||
cannot be downgraded; every other check id in `warn` is honoured only from the
|
||||
@@ -3490,6 +3617,12 @@ reports failures. The obligation stays uncleared. A run URL alone never clears
|
||||
this obligation; a `test_backend/<backend>` obligation takes a run URL, recorded
|
||||
as attested rather than validated.
|
||||
|
||||
`gstack-pregate clear --run <id|url>` refusals use this code too: the line
|
||||
reads `gstack-pregate clear: receipt refused — REUSE: refused reason=<code> —
|
||||
…`, and the GitHub Actions runs offered did not verify against the base
|
||||
branch's `run_profiles`. Each reason code and
|
||||
what it checks is listed in [run evidence](pregate.md#run-evidence).
|
||||
|
||||
Another tree is accepted in exactly one case: it differs from `pregate.json`'s
|
||||
tree only in the repo's stamp rule (`.gstack/ship-policy.json` on the base
|
||||
branch: `stamp_files`, `stamp_patterns`, the built-in version, manifest,
|
||||
@@ -3585,7 +3718,9 @@ run, named by `workflow.yml/job`, or the small-machine `pregate.json/remote_gate
|
||||
tier) has no receipt; or a `remote_gate` receipt accepted from a stamp-only
|
||||
predecessor tree records `accepted_for` as a tree other than `pregate.json`'s,
|
||||
or a `gated_tree` other than its receipt string's `tree=` (the file was edited
|
||||
after `clear`), so it clears nothing here.
|
||||
after `clear`), so it clears nothing here; or a `clear --run` receipt names
|
||||
another tree than `pregate.json`'s with no substitution recorded (`run
|
||||
receipt(s) verified for another tree`).
|
||||
|
||||
**Fix.** Dispatch that job, then
|
||||
`gstack-pregate clear --remote <workflow.yml/job> --run-url <url>` (or
|
||||
@@ -3593,7 +3728,11 @@ after `clear`), so it clears nothing here.
|
||||
`clear --remote pregate.json/remote_gate --receipt <gate output>`: the gate's
|
||||
own run on this tree, or a run on a predecessor tree that differs only by the
|
||||
repo's stamp rule ([gate ahead of the stamp](pregate.md#remote-gate-stamp-only));
|
||||
`verify` then prints the accepted substitution on its ok line. The ship receipt
|
||||
`verify` then prints the accepted substitution on its ok line. When the base
|
||||
branch declares run profiles, `clear --remote <lane> --run <id|url>` clears a
|
||||
lane from verified GitHub Actions runs on this tree (for the remote gate, one
|
||||
run per `remote_gate_runs` profile, with `--allow-repo-commands`); a profiled
|
||||
lane refuses `--bundle-lane` ([run evidence](pregate.md#run-evidence)). The ship receipt
|
||||
carries each obligation with its receipt.
|
||||
|
||||
<a id="issue-unreadable"></a>
|
||||
@@ -3672,6 +3811,21 @@ goes beside the variants. When no arm loads a skill stack, write
|
||||
`stack: none` with a `why:` line instead. The rule is in
|
||||
[docs/TESTING_INTERNALS.md](TESTING_INTERNALS.md#no-gstack-arm).
|
||||
|
||||
<a id="eval-report-no-gstack-row"></a>
|
||||
### `EVAL_REPORT_NO_GSTACK_ROW`
|
||||
|
||||
**Meaning.** `gstack-eval-plan results --check <prereg> <results>` found no
|
||||
single markdown table in the results file with a filled row for every arm
|
||||
identity the preregistration's Arms section lists. Usually the
|
||||
`<model>#no-gstack` row is missing, sits in a separate table from its
|
||||
variants, or holds only placeholders (`-`, `n/a`, `tbd`). It also fires when
|
||||
the preregistration itself fails `prereg --check`.
|
||||
|
||||
**Fix.** Put one row per arm identity, each no-gstack arm beside its variants,
|
||||
in one table, and rerun the check. Publish no number before it prints
|
||||
`RESULTS: ok`. A preregistration with `stack: none` needs no row. The rule is
|
||||
in [docs/TESTING_INTERNALS.md](TESTING_INTERNALS.md#no-gstack-arm).
|
||||
|
||||
<a id="eval-arm-duplicate"></a>
|
||||
### `EVAL_ARM_DUPLICATE`
|
||||
|
||||
@@ -4563,6 +4717,12 @@ GitHub accepted the dispatch). It is never dispatched again blindly.
|
||||
run carrying the nonce, or marks the dispatch absent; only then does
|
||||
`train build --train <nonce>` dispatch it again, under the same nonce.
|
||||
|
||||
On a train with `gates`, a workflow is sent at most twice per prefix. When
|
||||
neither send shows a run carrying the nonce, `train build` (and `train watch`)
|
||||
refuses with this code instead of sending a third: run `train doctor` (a
|
||||
run-name without the nonce is the usual cause), fix the workflow on the base
|
||||
branch, then build a new train.
|
||||
|
||||
<a id="queue-train-waiting"></a>
|
||||
### `QUEUE_TRAIN_WAITING`
|
||||
|
||||
@@ -4583,9 +4743,12 @@ one-PR train.
|
||||
**Meaning.** The train's run (free tests, the full gate or the bisect) has
|
||||
not finished, so there is no verdict yet. The same code covers a `train watch`
|
||||
that reached its timeout, `verify --pr <n> --screen` on a screen run still
|
||||
going, `verify --post-merge` on checks that have not settled, and a
|
||||
`ready --dispatch-ci` workflow run still going at its `--timeout`; each prints
|
||||
the command to rerun as `NEXT:`.
|
||||
going, `verify --post-merge` on checks that have not settled, a
|
||||
`ready --dispatch-ci` workflow run still going at its `--timeout`, a `train
|
||||
build` or `train verdict` while another process holds the train's lease or
|
||||
runs one of its command gates, and a `train land` whose previous slot's
|
||||
post-merge checks have not settled; each prints the command to rerun as
|
||||
`NEXT:`.
|
||||
|
||||
**Fix.** Rerun the printed `NEXT:` command later. Nothing is dispatched again
|
||||
while a run is pending.
|
||||
@@ -4602,6 +4765,10 @@ record. An incomplete gate is never green.
|
||||
one re-dispatch EVAL_POLICY.infraRedispatch allows (`train rebuild --train
|
||||
<nonce> --infra`) and report both runs. Otherwise diagnose the run.
|
||||
|
||||
On a train with `gates` it means a prefix gate is incomplete (a required job
|
||||
that was skipped or never ran, for example); the `PREFIX:` line names the
|
||||
run. `train rebuild` refuses, so diagnose that run and build a new train.
|
||||
|
||||
<a id="queue-train-red"></a>
|
||||
### `QUEUE_TRAIN_RED`
|
||||
|
||||
@@ -4612,6 +4779,11 @@ verdict.
|
||||
**Fix.** Bisect the red cases (`train bisect --train <nonce> --spend-cap
|
||||
<usd>`). After a rebuild's second red, route the cases to a repair lane.
|
||||
|
||||
On a train with `gates` there is no bisect: T<i> is the first red prefix after
|
||||
a green one and every later prefix is red, so slot i is the culprit. The slots
|
||||
before it land (`train land`, or `train watch --land`), the culprit goes back
|
||||
to its lane, and `NEXT:` builds the rest into a new train.
|
||||
|
||||
<a id="queue-train-red-on-main"></a>
|
||||
### `QUEUE_TRAIN_RED_ON_MAIN`
|
||||
|
||||
@@ -4621,6 +4793,9 @@ included: main is broken, not the train's PRs.
|
||||
**Fix.** Fix forward on main first (a one-PR train), then rebuild this train
|
||||
unchanged.
|
||||
|
||||
On a train with `gates`, no bisect runs: T0 (main) is red on its own gate run,
|
||||
or red on anything other than exactly slot 1's `FIXES_BASE_RED` cases.
|
||||
|
||||
<a id="queue-train-unattributed"></a>
|
||||
### `QUEUE_TRAIN_UNATTRIBUTED`
|
||||
|
||||
@@ -4631,6 +4806,10 @@ monotone. One trial per prefix cannot tell a flake from a break.
|
||||
<nonce>`), reporting both runs. Any other pattern: a repair lane, then a
|
||||
changed train once the repair merges.
|
||||
|
||||
On a train with `gates` it means a red prefix is followed by a green one.
|
||||
Repair the red cases or seat the PRs in another order (`--order`); `train
|
||||
rebuild` refuses, so this train never runs again unchanged.
|
||||
|
||||
<a id="queue-train-bisect-capped"></a>
|
||||
### `QUEUE_TRAIN_BISECT_CAPPED`
|
||||
|
||||
@@ -4700,6 +4879,83 @@ ran with `--no-fetch`, verify freezes as before (`QUEUE_TRAIN_FROZEN`). A queue
|
||||
another machine already froze stays frozen; the refusal prints its freeze
|
||||
lines.
|
||||
|
||||
<a id="queue-train-base-red"></a>
|
||||
### `QUEUE_TRAIN_BASE_RED`
|
||||
|
||||
**Meaning.** A conditional (base-red) PR or its fixer cannot ride this train.
|
||||
The train's policy has no `gates`, the fixer is missing or not at slot 1, two
|
||||
PRs declare `FIXES_BASE_RED`, the condition expired, or the fixer's own prefix
|
||||
T1 is red.
|
||||
|
||||
**Fix.** Seat the fixer at slot 1 of a train with gates and build again. A red
|
||||
fixer prefix is the fixer lane's to repair; an expired condition is classified
|
||||
again by its lane ([ship-policy.md](ship-policy.md#train-gates)).
|
||||
|
||||
<a id="queue-train-spend-cap"></a>
|
||||
### `QUEUE_TRAIN_SPEND_CAP`
|
||||
|
||||
**Meaning.** A command gate's next Ubicloud run would pass that gate's spend
|
||||
cap for the train, or `UBI_OWNER` is not set. Nothing more ran; finished runs keep
|
||||
their outcomes.
|
||||
|
||||
**Fix.** Set `UBI_OWNER` to your thread code and rerun `train build --train
|
||||
<nonce>`. For the cap, raise `spend_cap_usd` (or lower `timeout_sec`) on the
|
||||
base branch, then build a new train on the new main: the policy commit moves
|
||||
main past T0.
|
||||
|
||||
<a id="queue-train-land-method"></a>
|
||||
### `QUEUE_TRAIN_LAND_METHOD`
|
||||
|
||||
**Meaning.** This slot must land through an update commit (stamped train,
|
||||
strict protection, conditional PR, or release-file resolution), and
|
||||
`train.land.merge_method` is `rebase`, which would replay commits instead of
|
||||
landing the gated tree.
|
||||
|
||||
**Fix.** Set `merge_method` to `squash` or `merge` on the base branch, then
|
||||
build a new train on the new main: the policy commit moves main off this
|
||||
train's prefixes.
|
||||
|
||||
<a id="queue-train-init-refused"></a>
|
||||
### `QUEUE_TRAIN_INIT_REFUSED`
|
||||
|
||||
**Meaning.** Open PRs bump the version and the policy neither declares
|
||||
`changelog_fragments` nor stamps at merge, so every train merge would conflict
|
||||
on the version. The `VERSION_PRS:` line names them.
|
||||
|
||||
**Fix.** Adopt `changelog_fragments` (recommended) or `queue_mode:
|
||||
stamp-at-merge` on the base branch, then run `train init` again.
|
||||
|
||||
<a id="queue-train-doctor-findings"></a>
|
||||
### `QUEUE_TRAIN_DOCTOR_FINDINGS`
|
||||
|
||||
**Meaning.** The live train block and the repository disagree. Each
|
||||
`FINDING:` line names one problem: a gate workflow missing, without the
|
||||
`nonce` input or run-name, a required job it does not define, a global
|
||||
concurrency group, a push trigger on `train-prefix/` refs, a workflow never
|
||||
green on the base, a command gate cap below what a full train reserves, a
|
||||
merge method GitHub refuses, or `rebase` as the merge method.
|
||||
|
||||
**Fix.** Fix each finding on the base branch (`train init` prints the
|
||||
snippet), then run `train doctor` again.
|
||||
|
||||
<a id="queue-base-red-no-train"></a>
|
||||
### `QUEUE_BASE_RED_NO_TRAIN`
|
||||
|
||||
**Meaning.** `ready --base-red` or `--fixes-base-red` needs a train that gates
|
||||
every prefix, and the base policy has no train block with `gates`.
|
||||
|
||||
**Fix.** Wait for the base to pass the cases and run `/ship --resume`, or turn
|
||||
on a train with gates on the base branch.
|
||||
|
||||
<a id="ship-base-red-fixer"></a>
|
||||
### `SHIP_BASE_RED_FIXER`
|
||||
|
||||
**Meaning.** `ready --base-red` names no fixer, the receipt carries no
|
||||
`base_red` block, or the condition was classified on an older base.
|
||||
|
||||
**Fix.** Pass `--fixer <pr>` naming the PR whose merge fixes the cases; after
|
||||
a base move, run `gstack-base-red classify` and /ship again first.
|
||||
|
||||
<a id="queue-train-frozen"></a>
|
||||
### `QUEUE_TRAIN_FROZEN`
|
||||
|
||||
@@ -4724,6 +4980,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`
|
||||
|
||||
@@ -4765,7 +5106,9 @@ to fix this: the stamp comes only when the slot is called.
|
||||
**Meaning.** The PR body's ship receipt carries a `base_red` block: /ship's
|
||||
Step 5 failed on cases `gstack-base-red` proved are the base branch's own, so
|
||||
/ship paused before READY ([base_red](ship-policy.md#base-red)). Nothing
|
||||
READYs, trains, carries or merges on it.
|
||||
READYs, trains, carries or merges on it, except on a train with `gates`, where
|
||||
`ready --base-red --fixer <pr>` READYs it to ride behind its fixer at slot 1
|
||||
([base-red PRs](ship-policy.md#train-gates)).
|
||||
|
||||
**Fix.** Wait for the base to pass those cases (`gstack-base-red wait --pause
|
||||
<file>`), then run `/ship --resume <record>`, which reruns Step 5 and rewrites
|
||||
@@ -4957,7 +5300,10 @@ or `--timeout` (default 2700 s) passes, prints `POST_MERGE: <sha12>
|
||||
green|red|pending|none (passed= failed= pending= skipped=)` and one
|
||||
`CHECK: <name> <conclusion|status>` line per check, and a timeout is
|
||||
`QUEUE_TRAIN_PENDING` with the same command as `NEXT:`, so rerunning it after
|
||||
an interruption continues the watch.
|
||||
an interruption continues the watch. With `--checks <a,b>` (a workflows
|
||||
gate's `post_merge_checks`, which `train land` and `train watch --land` pass),
|
||||
each named check must also succeed; one that never ran is `QUEUE_TRAIN_PENDING`
|
||||
at the timeout.
|
||||
|
||||
**Fix.** Read the red run named on its `CHECK:` line. Repair on a new PR or
|
||||
roll back the merge; `gstack-ship-queue train status` names the freeze if the
|
||||
|
||||
+6
-1
@@ -11,4 +11,9 @@ on.
|
||||
The separate `triggers:` list in a skill's frontmatter is gstack metadata, not a
|
||||
matching surface. Claude Code and Codex choose a skill from its name and
|
||||
description, so a phrase that appears only under `triggers:` does not activate
|
||||
the skill (Codex output drops the field; the GBrain host keeps it).
|
||||
the skill (Codex output drops the field; the GBrain host keeps it). The four core
|
||||
skills (/office-hours, /review, /qa, /ship) carry their `triggers:` phrases in
|
||||
the description itself: every phrase in the full description, and at least one
|
||||
in the one-line catalog entry Claude Code reads ("test the site" for /qa, "ship
|
||||
it" for /ship). The other skills do not, because under the `core` default they
|
||||
wait for you to type them (docs/proactive.md).
|
||||
Reference in new issue
Block a user