mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
Merge main dd9834d44d (v1.92.61.0) into gsta203-pr3-subshards (rule 19 grant, GSTA-203 PR 3)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> # Conflicts: # .github/workflows/free-tests.yml # docs/TESTING_INTERNALS.md # scripts/harness-version.json # scripts/lib/shard-jobs.ts # scripts/test-free-shards.ts # test/shard-jobs.test.ts # test/windows-native-workflows.test.ts
This commit is contained in:
commit
f02e95e158
566 files changed
+36561
-8385
No files matched your search
+53
-9
@@ -2,7 +2,7 @@
|
||||
|
||||
gstack uses a declarative host config system. Each supported AI coding agent
|
||||
(Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Hermes,
|
||||
GBrain, Copilot, Antigravity) is defined as a typed TypeScript config object built by the
|
||||
GBrain, Copilot, Antigravity, Gemini CLI) is defined as a typed TypeScript config object built by the
|
||||
`defineHost()` factory. Rendering a new host means creating one file and
|
||||
re-exporting it: the generator, tests, and validation pick it up. Installing
|
||||
it is not free: setup still has one install arm per installable host (see
|
||||
@@ -76,13 +76,13 @@ Inventory of every setup side effect, by phase (line numbers are approximate):
|
||||
|---|---|---|
|
||||
| Build | `browse/dist`, `design/dist`, `make-pdf/dist`, `bin/gstack-cso-*` in the source checkout | source |
|
||||
| Hosts record | `.gstack-installed-hosts` in the source checkout: every host setup installed from it, additive (#1694) | source |
|
||||
| Generation | renders for the recorded hosts only (`.agents/`, `.kiro/`, `.factory/`, `.opencode/`, `.cursor/`, `.copilot/`, `.agy/`) in the source checkout; `gstack-patch-names` rewrites Claude `name:` fields in place | source |
|
||||
| Generation | renders for the recorded hosts only (`.agents/`, `.kiro/`, `.factory/`, `.opencode/`, `.cursor/`, `.copilot/`, `.agy/`, `.gemini-cli/`) in the source checkout; `gstack-patch-names` rewrites Claude `name:` fields in place | source |
|
||||
| Render prune | renders of hosts the record does not name: links into the checkout removed, generated files moved to `$GSTACK_STATE_ROOT/backups/host-renders/<ts>-<id>/`, other files kept | source / state root |
|
||||
| Chromium | Playwright cache (`~/.cache/ms-playwright`), lock under the state root | machine |
|
||||
| Config | `skill_prefix`, `timeline_stop_hook`, team-mode keys in `$GSTACK_STATE_ROOT/config.yaml` | state root |
|
||||
| Claude arm | `~/.claude/skills/gstack` link, one dir per skill with a `SKILL.md` link, alias copies | selected host |
|
||||
| Codex arm | `${CODEX_HOME:-~/.codex}/skills/gstack` runtime root, `gstack-*` links into `.agents/skills`, `.agents/skills/gstack` sidecar | selected host |
|
||||
| Kiro, Factory, OpenCode, Cursor, Copilot, Antigravity arms | the host's `skills/gstack` runtime root (built beside the live one and swapped in) and `gstack-*` links | selected host |
|
||||
| Kiro, Factory, OpenCode, Cursor, Copilot, Antigravity, Gemini CLI arms | the host's `skills/gstack` runtime root (built beside the live one and swapped in) and `gstack-*` links | selected host |
|
||||
| OpenCode commands | `~/.config/opencode/commands/gstack-*.md` with a managed marker (#2629); user command files are never touched | selected host |
|
||||
| Registry | one row per activated install in `installs.tsv` | state root |
|
||||
| Migrations | whatever each `v*.sh` repairs; marker in the state root | state root |
|
||||
@@ -245,12 +245,15 @@ which Google's docs do not state, so the rendered text names no number.
|
||||
|
||||
**Shared `~/.gemini`.** Antigravity CLI and Gemini CLI both live under
|
||||
`~/.gemini`. Gemini CLI reads `~/.gemini/skills` (and `~/.agents/skills`);
|
||||
Antigravity CLI reads `~/.gemini/antigravity-cli/skills`. Setup writes only
|
||||
gstack entries in the latter, uninstall removes only gstack-managed entries
|
||||
there, and `--host auto` selects Antigravity only for `agy` on `PATH` or an
|
||||
existing `~/.gemini/antigravity-cli`, never for a bare `~/.gemini`.
|
||||
`test/setup-install-registry.test.ts` holds both directions. Gemini CLI as a
|
||||
host is not supported. Two overlaps remain: workspace `.agents/skills` is also
|
||||
Antigravity CLI reads `~/.gemini/antigravity-cli/skills`, and Google's
|
||||
migration table lists `~/.gemini/skills` as an Antigravity global path too.
|
||||
Each host's arm writes only gstack entries in its own directory, uninstall
|
||||
removes only gstack-managed entries in each, and `--host auto` selects
|
||||
Antigravity only for `agy` on `PATH` or an existing
|
||||
`~/.gemini/antigravity-cli`, and Gemini CLI only for `gemini` on `PATH`,
|
||||
never either for a bare `~/.gemini`. `test/setup-install-registry.test.ts`
|
||||
holds every direction, including both hosts installed together. Gemini CLI is
|
||||
its own host ("Gemini CLI" below). Two overlaps remain: workspace `.agents/skills` is also
|
||||
where a project-local Codex install puts its Codex-flavored `gstack-*`
|
||||
skills, which Antigravity will list in that repo, and the Antigravity 2.0 app
|
||||
and IDE read `~/.gemini/config/skills`, which this host does not install
|
||||
@@ -277,6 +280,46 @@ root's extra markdown files (`ETHOS.md`, `review/*.md`) as skills, the
|
||||
partial-read behavior of `view_file` on long skills, `ask_question` option
|
||||
handling, and an upgrade from an existing install.
|
||||
|
||||
## Gemini CLI (#1247)
|
||||
|
||||
Shipped as `experimental` (`hosts/gemini.ts`, `./setup --host gemini`, alias
|
||||
`--host gemini-cli`). Verified against Gemini CLI 0.66.0 (its bundled
|
||||
`docs/cli/skills.md` and `SkillManager.discoverSkills`) on 2026-10-31:
|
||||
|
||||
| Fact | Value | Source |
|
||||
|---|---|---|
|
||||
| User skills | `~/.gemini/skills/<skill>/SKILL.md`, and the `~/.agents/skills` alias, which wins inside the user tier | [Agent Skills](https://geminicli.com/docs/cli/skills/), `packages/core/src/skills/skillManager.ts` |
|
||||
| Workspace skills | `.gemini/skills` and the `.agents/skills` alias, loaded only in a trusted folder (`/trust`) | same |
|
||||
| Precedence | built-in < extension < user < workspace; same name: the higher tier wins | same |
|
||||
| Discovery | `<dir>/<name>/SKILL.md`, one level deep; symlinked skill dirs are followed; the name comes from frontmatter `name` | same |
|
||||
| Invocation | the model calls `activate_skill` after you approve; `/skills list` and `gemini skills list` show them | same |
|
||||
| Tool names | `run_shell_command`, `read_file`, `read_many_files`, `write_file`, `replace`, `grep_search`, `glob`, `list_directory`, `ask_user`, `invoke_agent`, `activate_skill`, `enter_plan_mode`, `exit_plan_mode`, `write_todos`, `web_fetch`, `google_web_search` | 0.66.0 bundle |
|
||||
| Context file | `GEMINI.md` | 0.66.0 bundle |
|
||||
|
||||
What the host does: skills in `~/.gemini/skills/gstack-*`, runtime root
|
||||
`~/.gemini/skills/gstack` with `.source-path` for `/gstack-upgrade`, `name:`
|
||||
equal to the `gstack-<skill>` directory, frontmatter limited to `name` and
|
||||
`description` (1024-character limit), the Gemini model overlay, and a
|
||||
one-paragraph tool glossary ahead of the preamble's STATUS rules. The glossary
|
||||
also names Antigravity's `run_command`, `view_file` and `ask_question`,
|
||||
because an Antigravity CLI that reads `~/.gemini/skills` sees these skills.
|
||||
The render directory is `.gemini-cli/` (gitignored), not `.gemini/`, so this
|
||||
repo's own trusted workspace never lists render output as skills. `--local`
|
||||
is refused: it installs only Claude, and Gemini CLI reads workspace skills only
|
||||
in trusted folders.
|
||||
|
||||
Overlaps: inside a trusted repo, a project-local Codex install's `.agents/skills`
|
||||
(Codex-flavored `gstack-*` skills) is the workspace tier, which outranks
|
||||
`~/.gemini/skills`, so Gemini CLI uses those there. gstack never writes
|
||||
`~/.agents/skills`: that alias is shared with other tools and outranks
|
||||
`~/.gemini/skills`. Antigravity's own install stays in
|
||||
`~/.gemini/antigravity-cli/skills`.
|
||||
|
||||
Unverified without a Gemini API key on the build machines (why it is not
|
||||
`full`): a live model run of any skill, `ask_user` option handling, and
|
||||
plan mode with gstack's plan-review skills. Discovery is verified:
|
||||
`gemini skills list` after `./setup --host gemini` lists the `gstack-*` skills.
|
||||
|
||||
## Instruction-only hosts and agent harnesses
|
||||
|
||||
Slate and GBrain have no install arm. OpenClaw and Hermes are harness hosts:
|
||||
@@ -313,6 +356,7 @@ hosts/
|
||||
├── gbrain.ts # GBrain
|
||||
├── copilot.ts # GitHub Copilot CLI
|
||||
├── agy.ts # Google Antigravity CLI
|
||||
├── gemini.ts # Google Gemini CLI
|
||||
└── index.ts # Registry: imports all, derives Host type
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -195,6 +195,11 @@ registered as a short entry that tells the agent to read the skill's full
|
||||
`SKILL.md` in `gstack/skills/`, a plain copy of every skill that OpenClaw
|
||||
reads in pages (`.gstack-skills/` under `--skills all`).
|
||||
Install, verify and remove: [hosts.md](hosts.md#agent-harnesses).
|
||||
Agents in OpenClaw read gstack's skills but rarely run their start command, so
|
||||
there is an opt-in [start hook](hosts.md#start-hook): `gstack-harness register
|
||||
openclaw --start-hook` adds a `gstack-start` plugin that puts a one-line note on
|
||||
every user turn, and grants it the conversation access OpenClaw requires for
|
||||
`before_prompt_build` through `openclaw config set`.
|
||||
|
||||
The ACP artifacts in `openclaw/` are generated by `bun run gen:skill-docs --host openclaw`.
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ gstack/
|
||||
│ ├── codex.ts, factory.ts, kiro.ts # Existing hosts
|
||||
│ ├── opencode.ts, slate.ts, cursor.ts, openclaw.ts # IDE hosts
|
||||
│ ├── hermes.ts, gbrain.ts # Agent runtime hosts
|
||||
│ ├── copilot.ts, agy.ts, gemini.ts # GitHub Copilot CLI, Antigravity CLI, Gemini CLI
|
||||
│ └── index.ts # Registry: exports all, derives Host type
|
||||
├── scripts/ # Build + DX tooling
|
||||
│ ├── gen-skill-docs.ts # Template → SKILL.md generator (config-driven)
|
||||
|
||||
+97
-29
@@ -798,7 +798,9 @@ When a judge's abort signal cancels its request, `callJudge`
|
||||
(`test/helpers/llm-judge.ts`) prints one `llm-judge-aborted` JSON line to
|
||||
stderr: elapsed time, wrapper `attempts` and `retryWaitsMs` (429 waits),
|
||||
`httpRequests` and `httpStatuses` (so SDK retries show), `connectedMs`,
|
||||
`firstEventMs` and `lastEventMs`, the open `block` type, `thinkingChars` and
|
||||
`firstEventMs` and `lastEventMs`, `bytesReceived` and `lastByteMs` (every body
|
||||
byte, pings included, so a silent socket differs from a model thinking on a live
|
||||
connection), the open `block` type, `thinkingChars` and
|
||||
`textChars`, `messageId`, `requestId` and the `reason` (at most 200
|
||||
characters). It holds counts and ids only, never content, and changes no
|
||||
request, score or verdict.
|
||||
@@ -1003,7 +1005,7 @@ with `UBICLOUD: left=<0|1|unknown> owner=<o> vm=<name> others=<n>`, checked
|
||||
against the project's VM list after teardown; anything but `left=0` turns a
|
||||
passing command into exit 1. pregate-remote.sh reads that line from the file
|
||||
named by `UBI_LEFT_FILE`, never from the command's output. No owner-age sweep runs by default (the TTL reaper
|
||||
below covers every owner); set
|
||||
below covers this owner's expired VMs); set
|
||||
`UBI_GC_HOURS` to a positive number (or run `gc HOURS`; `gc 0` is a no-op) to
|
||||
sweep only this owner's VMs older than that. For other commands, use the runner
|
||||
directly: `scripts/ubicloud/ubi-runner.sh run --setup <script> -- '<command>'`,
|
||||
@@ -1012,16 +1014,18 @@ or its `up` / `ssh` / `sync` / `pull` / `down` steps (`--help` lists them).
|
||||
The runner is also the `/ubicloud` skill's engine (`bin/gstack-ubi` execs it).
|
||||
A generated name ends in `-t<hours>` (`UBI_TTL_HOURS`, 1-999, default 3; pregate-remote.sh
|
||||
uses its stop deadline plus 15 minutes, rounded up: `-t1` today), the creator's own statement of how long the VM may live: `reap`
|
||||
destroys every VM in the project whose TTL ended more than ten minutes ago,
|
||||
whichever owner made it, and the caller's own `ubirun-<UBI_OWNER>-*` names
|
||||
without a TTL (older runner copies) once they are `UBI_LEGACY_HOURS` old
|
||||
(default 4). Another owner's TTL-less VM and an untagged one are never reaped,
|
||||
because only a TTL lets reap judge another lane's VM. Every `up` runs it
|
||||
first (`UBI_REAP=0` skips it). Names outside the `ubirun-` prefix are never reaped. The
|
||||
destroys only the caller's own VMs: its `ubirun-<UBI_OWNER>-*` names whose TTL
|
||||
ended more than ten minutes ago, and those without a TTL (older runner copies)
|
||||
once they are `UBI_LEGACY_HOURS` old (default 4). Another owner's VM and an
|
||||
untagged one are never reaped, even after their TTL ends; reap only counts them
|
||||
in its log, because cloud deletes stay with the owner that created the VM
|
||||
(GST-573). Every `up` runs it first (`UBI_REAP=0` skips it). Names outside the `ubirun-` prefix are never reaped. The
|
||||
quota preflight adds the size's vCPUs to the project's running VMs and refuses
|
||||
before the create request when that exceeds the known ceiling
|
||||
(`UBI_QUOTA_VCPUS`, or the maximum the last refusal printed, remembered in the
|
||||
state root; `UBI_QUOTA_WAIT=<seconds>` waits for room instead). `run`, `verify`
|
||||
state root; `UBI_QUOTA_WAIT=<seconds>` waits for room instead). When the usage read fails (`usage` exits 1 on an
|
||||
unreadable VM list or size and skips a VM that 404s), the preflight retries
|
||||
within `UBI_QUOTA_WAIT`, then logs and leaves the decision to the API. `run`, `verify`
|
||||
and `ssh NAME COMMAND` stream the remote output through a watchdog: a heartbeat
|
||||
line every `UBI_HEARTBEAT` seconds (300), and a command silent for
|
||||
`UBI_IDLE_TIMEOUT` seconds (1800) or older than `UBI_RUN_TIMEOUT` (0, unlimited)
|
||||
@@ -1034,7 +1038,7 @@ repository content: its setup script and pull destinations must resolve inside
|
||||
`--src`, and a name it lists under `pass` is forwarded only when the command
|
||||
line grants it (`--pass NAME`, or all of them with `--trust-config`); `--pass`
|
||||
never forwards `UBICLOUD_*`. All of this is pinned offline in
|
||||
`test/ubicloud-runner.test.ts` against a fake API and a fake `ssh`, including
|
||||
`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
|
||||
@@ -1050,7 +1054,7 @@ 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
|
||||
`created_at`. The fake VM in `test/helpers/ubi-runner-fake-vm.ts` runs these phases
|
||||
for real in a directory.
|
||||
|
||||
Under 8 cores (`gstack-doctor`'s `cores` row says so), run the full unit and
|
||||
@@ -1069,7 +1073,7 @@ machine swap, in this order:
|
||||
one side local and the other remote.
|
||||
4. Profiling very large imports (a whole-repo ingest, the full free suite under
|
||||
a profiler) is warned about before it starts: it holds the VM for hours, and
|
||||
any other lane's `up` reaps it once its TTL (`UBI_TTL_HOURS`, default 3)
|
||||
the same owner's next `up` or `reap` destroys it once its TTL (`UBI_TTL_HOURS`, default 3)
|
||||
plus ten minutes has passed, so set `UBI_TTL_HOURS` above the expected
|
||||
duration and name that duration in the thread before launching.
|
||||
|
||||
@@ -1452,7 +1456,13 @@ Monday "Weekly test health" workflow (`bun run test:health --enforce`) reads
|
||||
main's ledgers through `scripts/lib/ci-history.ts` and fails when a file flakes
|
||||
in more than 5% of main free-tests runs over a window of at least 20 runs; a
|
||||
window thinner than 20 runs in the last 7 days widens to the newest 20 runs, and
|
||||
fewer than 20 runs is report-only. `bun run test:health flake-summary <dir>`
|
||||
fewer than 20 runs is report-only. A main push that reused its pull request's
|
||||
run ([CI reuse](pregate.md#ci-reuse)) ran no shards, so it stays out of the
|
||||
window and its denominator, and older runs take its place. Such a run has no
|
||||
artifacts and a green `reuse` job; checking that costs one job listing per
|
||||
run with no artifacts, at most 40 per read. Past that cap, or when a listing
|
||||
fails, the run counts as before and the report says so.
|
||||
`bun run test:health flake-summary <dir>`
|
||||
prints a run's ledgers as a table. A flaky file is fixed at source; the retry
|
||||
never masks it. `bun run eval:pass-rates` lists only the local ledger
|
||||
(`flakeLedgerPath()`, overridden by `GSTACK_FLAKE_LEDGER`).
|
||||
@@ -1549,7 +1559,7 @@ refresh to a maintainer.
|
||||
(`ubicloud-standard-8`); 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
|
||||
dispatches Free Tests, Windows Smoke and Quality gate on it. Those
|
||||
dispatched runs are the seed PR's checks, and Free Tests supplies the required
|
||||
`free-tests` check. GitHub holds the pull_request runs of a token-opened PR as
|
||||
"action required" until someone approves them. A file that fails while recording turns its
|
||||
@@ -1627,8 +1637,16 @@ never decide a verdict.
|
||||
|
||||
### Windows free lane
|
||||
|
||||
Windows has minimal support ([support tiers](support-tiers.md)). The required
|
||||
PR check is `windows-smoke.yml`: one `windows-latest` job runs `./setup`, starts
|
||||
the core skills, drives `/browse` on gstack's own browser, and runs the short
|
||||
test list in its last step, where each file's reason sits beside it.
|
||||
`test/windows-native-workflows.test.ts` keeps that list Windows-safe and fast.
|
||||
|
||||
The curated suite below runs nightly on main and on dispatch, never on a PR.
|
||||
`windows-free-tests.yml` plans on Linux (`--windows-only --ci-plan --shards 6
|
||||
--shard-jobs $WINDOWS_FREE_JOBS`), runs one strict shard per `windows-latest` job
|
||||
--shard-jobs $WINDOWS_FREE_JOBS`), runs one strict serial shard per
|
||||
`windows-latest` job
|
||||
(`bun run test:windows --ci-run`), and verifies every shard receipt on Linux
|
||||
(`--windows-only --ci-verify`). Packing uses Windows-measured durations from
|
||||
`scripts/free-test-durations-windows.json`; the weekly
|
||||
@@ -1715,9 +1733,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`,
|
||||
@@ -1762,18 +1782,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
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ unknown is reserved for existing behavior you could not inspect. In a live audit
|
||||
you never triggered are unknown, not extrapolated.
|
||||
|
||||
**Caps.** Name the path and item behind every cap and show the corrected error line.
|
||||
- **4/10 (Critical finding, title `CAP4:<path>:<item>`):** a path leaks a secret, signals failure as success, gives an unsafe fix, or blocks waiting for a terminal (hangs, or fails for lack of a terminal without naming a non-interactive alternative such as `--yes`).
|
||||
- **6/10 (High finding, title `CAP6:<path>:<item>`):** a path lacks an applicable fix, recovery, context or trace item.
|
||||
- **4/10 (Critical finding, title `CAP4:<path>:<item> <what fails>`):** a path leaks a secret, signals failure as success, gives an unsafe fix, or blocks waiting for a terminal (hangs, or fails for lack of a terminal without naming a non-interactive alternative such as `--yes`).
|
||||
- **6/10 (High finding, title `CAP6:<path>:<item> <what fails>`):** a path lacks an applicable fix, recovery, context or trace item.
|
||||
|
||||
Every mode, DX TRIAGE included, reports every CAP4 finding.
|
||||
|
||||
|
||||
@@ -257,6 +257,13 @@ the same three paths. An errored init is skipped, so a later successful init
|
||||
still binds. This denial applies when no init ever succeeded, or when an
|
||||
errored init follows a good one.
|
||||
|
||||
An init whose output is not its JSON result (extra text, or no single text
|
||||
block) also counts as failed, and the message adds "without a leading cd" to
|
||||
the re-run hint. The one exception is the `Shell cwd was reset to <dir>` line
|
||||
Claude Code adds as the last line of a Bash result when the command's `cd`
|
||||
left the session's folder: the guard removes exactly that line before reading
|
||||
an init or checkpoint result (#3651).
|
||||
|
||||
<a id="init-unbindable"></a>
|
||||
### `init_unbindable`
|
||||
Init ran through a shell variable, substitution, chaining, a pipe or a
|
||||
|
||||
+39
-4
@@ -172,7 +172,7 @@ not a new ask ([carry-forward](unattended.md#carry-forward)).
|
||||
|
||||
**The record.** `gstack-approvals import --autoplan <run dir> --by "<owner>"` (with
|
||||
`--stack <b1,b2,...>` for a stacked PR set) writes the approval row (date, the ask, the owner's exact words, the lane that answered as relayer,
|
||||
what it covers); `gstack-approvals render` puts it in the report and the PR body. Before
|
||||
what it covers; a gate answered with `--words-by agent:<id>` is listed under that agent as its own decision, not the owner's words); `gstack-approvals render` puts it in the report and the PR body. Before
|
||||
training the PR the controller compares that row's words and `gate_rev` with the ask it
|
||||
relayed and the reply it carried back; a row that does not match is not READY (shape A).
|
||||
The brief's done line names the row. A plan approval is not a READY field; the
|
||||
@@ -248,7 +248,8 @@ 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
|
||||
@@ -299,13 +300,26 @@ 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`
|
||||
from `lib/changelog-check.ts`, the `bun -e` one-liner in
|
||||
[ship-policy.md](ship-policy.md#run-a-merge-train), never by hand, and grep for conflict
|
||||
markers before stamping), then restamps, then commits only the merge and the stamp
|
||||
files. In practice slot 1 needs the same merge whenever the base moved after the pin was
|
||||
markers before stamping), then restamps (the restamp stages the stamp files it wrote,
|
||||
so a plain `git commit` after a `--no-commit` merge keeps them), then commits only the
|
||||
merge and the stamp files. In practice slot 1 needs the same merge whenever the base moved after the pin was
|
||||
taken. A stamp commit with one extra byte freezes the queue for every
|
||||
lane behind it.
|
||||
|
||||
@@ -330,6 +344,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,
|
||||
|
||||
@@ -241,7 +241,12 @@ keeps the lease. `bin/gstack-lease list` shows `alive|dead|stale`, and
|
||||
clone too, through `GSTACK_CAPY_CLONED`).
|
||||
2. Preflight: `git`, `curl`, `node`, `jq`, then Bun. The project's pins
|
||||
(`--project <path>`, default the git toplevel of the current directory) are
|
||||
read with the doctor's own parser (`bin/gstack-runtime-pins.ts`: engines,
|
||||
read (a `--project` inside a repository resolves to that repository's git
|
||||
toplevel, so `--project <repo>/<subdir>` means `<repo>`; outside a repository
|
||||
it resolves to the directory's real path, and a value that is not a
|
||||
directory, such as a repo slug, exits 2 with a usage error before anything
|
||||
runs; the pins, the doctor and the install verdict all use the resolved
|
||||
directory) with the doctor's own parser (`bin/gstack-runtime-pins.ts`: engines,
|
||||
packageManager, `.tool-versions`, `.bun-version` and CI `bun-version:` lanes
|
||||
for this platform). CI exact pins for this platform are alternatives: a
|
||||
repository may test several Bun versions (gstack's own CI pins 1.4.2 and
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -96,7 +96,7 @@ The plan-ceo-review-plan-mode (:89, :108), plan-devex-review-plan-mode (:181-222
|
||||
| Case | Check (file:line) | Matches | Structured source | User-visible | Disposition |
|
||||
|---|---|---|---|---|---|
|
||||
| ship-docsync-store | test/skill-e2e-ship-docsync.test.ts:74, :78 | ship report ~ `Documentation…blocked`; not `Documentation: current` | no receipt, no publish call (:75-77) | yes (report status) | (c) |
|
||||
| ship-docsync-completion, ship-docsync-current | test/skill-e2e-ship-docsync.test.ts:81, :97 | parsed docs contract, then report contains `contract.documentation_section` | docs dispatch JSON (ship/sections/documentation.md.tmpl:52-56) | yes | (a) |
|
||||
| ship-docsync-completion, ship-docsync-current | test/skill-e2e-ship-docsync.test.ts:85, :105 | parsed docs contract, then report contains `contract.documentation_section` | docs dispatch JSON (ship/sections/documentation.md.tmpl:52-56) | yes | (a) |
|
||||
| ship-docsync-late-result, -missing-marker, -missing-asset, -launch-failure, -timeout-unsettled, -failure | test/helpers/docsync-fault-eval.ts:28, :30 | non-success report ~ `Documentation…blocked`, no `Documentation: current` outside inline code | actor event log decides dispatch count and publication first (:22-27) | yes | (c) |
|
||||
| ship-docsync-recovery, -stale-before, -stale-after | test/helpers/docsync-fault-eval.ts:31 | report contains the accepted audit id | actor state `acceptedId` | yes | (a) |
|
||||
| docsync-spawned | test/skill-e2e-docsync-spawned.test.ts:60 | `contract.blockers` ~ security/sensitive | parsed docs contract | no | (a) |
|
||||
|
||||
@@ -38,8 +38,8 @@ cause, the held files and the fix.
|
||||
|
||||
| State | Trigger | Attention line (after `ARTIFACTS_SYNC: attention: `) | Fix |
|
||||
|---|---|---|---|
|
||||
| `held` | The secret scan held back flagged files and synced the rest | `status=held held=<N>. The secret scan is holding back <N> file(s); everything else still syncs. See which files and how to fix them: <bin> --status` | Edit the file so the scan no longer matches, or `<bin> --skip-file <path>` (permanent; undo with `--unskip-file <path>`) |
|
||||
| `blocked` | A re-scan still flagged after the hold, so nothing was committed | `status=blocked. The secret scan flagged a file it could not hold back on its own, so nothing is syncing. See the fix: <bin> --status` | Edit or skip the files `--status` names, or clear the batch with `<bin> --drop-queue --yes` |
|
||||
| `held` | The secret scan or the JSON check held back flagged files and synced the rest | `status=held held=<N>. Sync is holding back <N> file(s) that the secret scan flagged or that are not valid JSON; everything else still syncs. See which files and how to fix them: <bin> --status` | Edit the file so the scan no longer matches, or `<bin> --skip-file <path>` (permanent; undo with `--unskip-file <path>`), or repair a file that is not valid JSON |
|
||||
| `blocked` | A re-scan still flagged after the hold, or a file would be held during an unfinished git merge, so nothing was committed | `status=blocked. Sync committed nothing: a flagged file could not be held back on its own, or held files are part of an unfinished git merge. See the fix: <bin> --status` | Edit or skip the files `--status` names, or clear the batch with `<bin> --drop-queue --yes`. For an unfinished merge, repair the held files or finish the merge; skipping files or dropping the queue does not clear it |
|
||||
| `push_failed` | A commit is saved locally but the push failed | `status=push_failed. Synced files are committed locally but the push failed; it retries at skill start. See the cause: <bin> --status` | The fix `--status` names (usually `gh auth status`) |
|
||||
| `error` | The drain stopped before committing and kept the queue | `status=error. The last sync stopped before committing; the queue is kept and retried. See the cause: <bin> --status` | The cause `--status` names: a git index lock, a commit hook, a `git add` failure |
|
||||
| `unknown` | The status file holds a status code outside this table | `status=unknown. The sync status file is unreadable. See it: <bin> --status` | Run `<bin> --once` to rewrite it |
|
||||
@@ -76,6 +76,11 @@ both fixes:
|
||||
~/.claude/skills/gstack/bin/gstack-brain-sync --drop-queue --yes
|
||||
```
|
||||
|
||||
A synced `.json` file that does not parse (often git conflict markers from a
|
||||
merge) is held the same way under rule `invalid-json`. Its only fix is to
|
||||
repair the file; `--skip-file` is not offered, because it would stop that
|
||||
file syncing for good. The next sync re-checks it.
|
||||
|
||||
**Git index lock.** A git process killed mid-write leaves
|
||||
`~/.gstack/.git/index.lock`. The sync removes it once it is older than 10
|
||||
minutes, because every gstack writer to `~/.gstack` takes the same sync lock
|
||||
@@ -107,7 +112,11 @@ After fixing auth, run any skill to retry sync automatically.
|
||||
git's error appears after the colon.
|
||||
|
||||
**Cause.** Could be network issue, rejected push (remote ahead), server 500,
|
||||
or repo access revoked.
|
||||
or repo access revoked. The line is git's own error, not its `To <remote>`
|
||||
banner or `hint:` lines. After a rejected push, brain-sync fetches and merges
|
||||
the other machine's commits; when that merge fails, the status adds
|
||||
`; merging origin failed: <reason>` (for example `CONFLICT (content): Merge
|
||||
conflict in developer-profile.json`).
|
||||
|
||||
**Fix.** Look at `~/.gstack/.brain-sync-status.json` for more detail, or run:
|
||||
```bash
|
||||
|
||||
+18
-3
@@ -91,8 +91,8 @@ gstack-brain-sync --status
|
||||
```
|
||||
|
||||
Shows: the status (`ok`, `idle`, `held`, `collision`, `blocked`,
|
||||
`push_failed`, `error`), files held back by the secret scan with their
|
||||
fixes, files held because their path differs only by letter case from
|
||||
`push_failed`, `error`), files held back by the secret scan or because
|
||||
they are not valid JSON, with their fixes, files held because their path differs only by letter case from
|
||||
another path (`case_held`, `case_names`; see
|
||||
[troubleshooting](troubleshooting.md#artifact-case-collision)), the
|
||||
`drainable` count, the last sync and push times, pending queue depth, and
|
||||
@@ -156,7 +156,7 @@ to it); everything else syncs. The held file stays queued and is re-scanned
|
||||
at every sync, and skill start prints:
|
||||
|
||||
```
|
||||
ARTIFACTS_SYNC: attention: status=held held=1. The secret scan is holding back 1 file(s); everything else still syncs. See which files and how to fix them: <bin> --status
|
||||
ARTIFACTS_SYNC: attention: status=held held=1. Sync is holding back 1 file(s) that the secret scan flagged or that are not valid JSON; everything else still syncs. See which files and how to fix them: <bin> --status
|
||||
```
|
||||
|
||||
To remediate:
|
||||
@@ -191,6 +191,21 @@ append commits. Git's default would conflict at the file tail, but the
|
||||
You shouldn't see conflict prompts. If you do (a real semantic conflict,
|
||||
like two machines editing the same plan), git will stop and prompt.
|
||||
|
||||
When a push is rejected, brain-sync fetches and merges the other machine's
|
||||
commits itself. Local edits to files that merge would change are set aside
|
||||
and put back afterward, and a failed merge is aborted, so `~/.gstack` never
|
||||
holds conflict markers. If a local edit conflicts with the other machine's
|
||||
change, the other machine's version stays in the working tree and your edit
|
||||
is kept in a git stash; `gstack-brain-sync --status` names the stash and the
|
||||
`git -C ~/.gstack --literal-pathspecs restore --source=<stash> -- <path>`
|
||||
command that brings it back.
|
||||
|
||||
JSON files such as `developer-profile.json` have no merge driver, so a
|
||||
merge can leave conflict markers in them. The sync parses every staged
|
||||
`.json` file and holds back one that does not parse (status `held`, rule
|
||||
`invalid-json`) until you repair it, and `gstack-doctor` warns when a synced profile or
|
||||
decision snapshot holds conflict markers.
|
||||
|
||||
## Cross-machine pull cadence
|
||||
|
||||
The preamble runs `git fetch` + `git merge --ff-only` once per 24 hours
|
||||
|
||||
@@ -24,6 +24,8 @@ neither does `./setup --local`.
|
||||
|
||||
**What it writes.** Copies in the harness's own skills folder, never edits to
|
||||
`openclaw.json`, Hermes's `config.yaml` or any context file you did not name.
|
||||
The one exception is the [start hook](#start-hook), which you turn on yourself:
|
||||
it adds a plugin and enables it through the harness's own commands.
|
||||
A context block is opt-in: `gstack-harness register openclaw|hermes|generic
|
||||
--context <file>` writes the marked block into that file only, after printing
|
||||
the file and its scope (`profile` inside the harness's own home, else
|
||||
@@ -67,6 +69,30 @@ characters, the part of a description Hermes's skills index shows.
|
||||
| Grok Bot, any chat-only skill library | nothing on disk | `gstack-harness register generic --print`, then paste the router and block into the app | ask it for a gstack skill | delete the pasted text |
|
||||
| any other harness | the router in an Agent Skills folder you name, and/or a 600-byte routing block between `<!-- gstack:begin harness-routing -->` markers in a context file you name | `gstack-harness register generic --skills-dir <dir>` and/or `--context <file>`; `gstack-harness manifest generic` prints every skill as JSON | the harness's own skill list | `gstack-harness unregister generic` |
|
||||
|
||||
<a id="start-hook"></a>
|
||||
### Start hook (OpenClaw and Hermes, opt-in)
|
||||
|
||||
Every gstack skill starts with a command in its Preamble (`gstack-skill-start`) that sets up the session: whether it runs unattended, questions, consent, upgrade checks. In OpenClaw and Hermes, agents read gstack's skills but almost never run that command (1 start in 29 chances, HARN-18). The start hook is a small `gstack-start` plugin that puts one note of about 210 characters on every user turn, telling the agent to run that command first, exactly as written, with no pipe.
|
||||
|
||||
It is off unless you turn it on, because turning it on changes the harness's own settings:
|
||||
|
||||
| | OpenClaw | Hermes |
|
||||
|---|---|---|
|
||||
| Plugin folder | `${OPENCLAW_STATE_DIR:-~/.openclaw}/extensions/gstack-start` (index.js, openclaw.plugin.json, package.json, note.txt) | `${HERMES_HOME:-~/.hermes}/plugins/gstack-start` (plugin.yaml, __init__.py, note.txt) |
|
||||
| Hook | `before_prompt_build`, returning the note as `prependContext` | `pre_llm_call`, returning the note as `context` |
|
||||
| Settings it changes, through the harness's own CLI | `openclaw config set plugins.entries.gstack-start.hooks.allowConversationAccess true` and `...enabled true` in `openclaw.json`. OpenClaw runs `before_prompt_build` from a plugin it did not bundle only with that grant, which also lets the plugin's hooks see conversation content (the plugin reads only its own note.txt) | `hermes plugins enable gstack-start` adds it to `plugins.enabled` in `config.yaml` (and creates `config.yaml` when there is none) |
|
||||
| Check | `openclaw plugins inspect gstack-start --runtime --json` lists `before_prompt_build` under `typedHooks` | `hermes plugins list` shows `gstack-start` enabled |
|
||||
|
||||
**Turn it on.** `gstack-harness register openclaw --start-hook` (or `hermes`), or `gstack-config set harness_start_hook on` followed by `./setup`, or `GSTACK_HARNESS_START_HOOK=1 ./setup --host openclaw`. The first of these that is set decides: the `--start-hook`/`--no-start-hook` flag, then `GSTACK_HARNESS_START_HOOK` (`1`/`on` or `0`/`off`), then `harness_start_hook`, then the choice saved in the registration's ledger. Whatever decides, the choice is saved, so refresh and upgrades keep it. The hook goes only into the harness profile's own plugin folder; a registration with a custom `--skills-dir`, or OpenClaw run with `OPENCLAW_CONFIG_PATH` pointing at another config, gets no hook (`HARNESS_HOOK_UNSUPPORTED` when you ask for one). A hook you already turned on stays on when a later run happens to have `OPENCLAW_CONFIG_PATH` set.
|
||||
|
||||
**Check it.** The receipt and `gstack-harness status` show `start hook: active` (configured for new sessions; a running OpenClaw gateway or Hermes gateway picks it up when it reloads or restarts), `written` (files in place but not active, with the reason and the command that fixes it), `disabled` (you turned it off in the harness), `unchecked` (a run that left a hook already on alone, or `status --no-check`; `gstack-harness status` checks it) or `off`. When the run that turns the hook on (or `--start-hook`) leaves it inactive, `gstack-harness register` and `./setup --host` exit 4 after registering the skills (`HARNESS_HOOK_INACTIVE`). Later refreshes and upgrades try once more each time, without failing, until the hook is active or you disable the plugin in the harness.
|
||||
|
||||
**Turn it off.** `gstack-harness register openclaw --no-start-hook` (or `hermes`), or `gstack-config set harness_start_hook off` followed by `./setup`. Either turns the plugin off through the harness's CLI and removes its folder. You can also switch it off in the harness (`openclaw config set plugins.entries.gstack-start.enabled false`, `hermes plugins disable gstack-start`); gstack then leaves it off and never turns it back on unless you pass `--start-hook` again. `gstack-harness unregister` and `gstack-uninstall` remove it with the rest (Hermes only disables it when its folder holds an edited file or a kept `.edited-*` copy, so your edit survives), and print the harness command to run when the harness CLI is not on `PATH`.
|
||||
|
||||
**What it costs.** The note is about 50 tokens and lands on every user turn, and each copy stays in the session's history, so a long session pays more (mostly cache reads). HARN-18's panel measures the added cost per trial. OpenClaw also logs that it cannot verify where the plugin came from, because gstack writes it instead of installing it from npm or ClawHub.
|
||||
|
||||
**One owner.** A `gstack-start` folder this registration did not write stops the hook (`HARNESS_HOOK_OWNED_ELSEWHERE`): turn it off from the gstack install that wrote it, or move it aside.
|
||||
|
||||
OpenClaw's ACP route below is unchanged: native registration gives OpenClaw's
|
||||
own agent gstack's skills; ACP spawns Claude Code with the full install.
|
||||
|
||||
@@ -137,6 +163,14 @@ Antigravity only when `agy` is on `PATH` or `~/.gemini/antigravity-cli` exists,
|
||||
never for a Gemini-CLI-only `~/.gemini`. The Antigravity 2.0 app and IDE read
|
||||
`~/.gemini/config/skills`, which this host does not install.
|
||||
|
||||
Gemini CLI loads gstack skills itself when a task matches (`activate_skill`,
|
||||
after you approve); ask for `gstack-office-hours`, or list them with
|
||||
`/skills list`. Setup writes only `~/.gemini/skills/gstack*` and never
|
||||
`~/.agents/skills`, and `--host auto` selects Gemini CLI only when `gemini` is on
|
||||
`PATH`. Inside a repo with a project-local Codex install, Gemini CLI uses the
|
||||
Codex skills in `.agents/skills` (the workspace tier wins), and workspace skills
|
||||
load only in trusted folders (`/trust`).
|
||||
|
||||
Outside reviews require the selected CLI to be installed and authenticated: Claude Code when using gstack in Codex, or Codex on other harnesses. External harnesses discover these commands as `/gstack-claude-code` and `/gstack-codex`; each harness omits its own wrapper. Explicit provider requests keep that provider. The existing `codex_reviews` setting controls automatic outside reviews where supported, regardless of the provider selected.
|
||||
|
||||
`/claude` has been renamed to `/claude-code`. Re-run `./setup --host <name>` to migrate managed installations, including other harnesses sharing the checkout. Setup preserves the previous installation if replacement generation or installation fails and prints repair instructions.
|
||||
@@ -207,6 +241,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).
|
||||
|
||||
+269
-14
@@ -75,10 +75,10 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`).
|
||||
| Key | Meaning |
|
||||
|---|---|
|
||||
| `runner` | the local test command with `{files}` (gstack: `bun test {files}`); unset, it is detected (bun, vitest, jest, pytest, `node --test`, and `flutter test`/`dart test` per Dart package root, through fvm when that package or an ancestor pins it with `.fvmrc` or `.fvm/fvm_config.json` and fvm is on PATH; `cargo test -p <name>` per Rust crate, run from the root of the workspace the crate is a member of (Cargo's `members` globs, `exclude` and `package.workspace`), else the crate directory; a JS/TS test whose nearest package.json is a nested package declaring vitest or jest (a dependency or its `test` script) runs from that package, with package-relative paths, through the package manager its `packageManager` field or the nearest lockfile up to the repo root names: `pnpm exec`, `yarn`, `bun x`, else `npx`; every other JS/TS test keeps the root detection); nothing detected, a Dart SDK, cargo or package manager missing from PATH, a `.dart` file with no `pubspec.yaml` above it, a `.rs` file outside any crate, or one in a crate whose name Cargo would reject (anything but letters, digits, `_` and `-`), is `incomplete` (the runnable groups still run, so a real failure still fails) |
|
||||
| `dependencies` | glob → test files for subprocess targets, runtime reads and generated inputs the import graph cannot see; `[]` declares "no lane needed" (docs, licences) |
|
||||
| `dependencies` | glob → test files for subprocess targets, runtime reads and generated inputs the import graph cannot see; `[]` declares "no lane needed" (docs, licences). A `[]` that matches a deleted test does not skip it: the deleted test still runs its owning suite, and a file naming it that `[]` covers (a durations list) counts as covered. With this key present, even empty, the policy [declares lanes](#lane-requirements) |
|
||||
| `empty_lanes` | extra globs for the docs/no-tests empty lane. Any Markdown file already qualifies with no pin file when no test depends on it (the same import graph as other files: a quoted path, a `join()` of path segments, an import, or a `dependencies` entry). reStructuredText and AsciiDoc count as Markdown here. README, CHANGELOG, CONTRIBUTING, LICENSE, AGENTS and CLAUDE qualify even without `.md`, and so does licence text under any vendored name (`roboto-LICENSE.txt`, `LICENSE-MIT`, `licenses/*.txt`). Source code and files under a test directory never qualify. A test that reads the file still selects that test |
|
||||
| `selection` | `workflow.yml/job` → a command printing the lane's file list, for matrices a planner computes (gstack: `scripts/test-free-shards.ts --list`) |
|
||||
| `warn` | check ids downgraded to warn; `secrets` and required lanes never |
|
||||
| `warn` | check ids downgraded to warn; `secrets` and required lanes never. `"lanes"` downgrades only a touched file with no lane: the selected tests still run, a red lane stays `fail` (`PREGATE_LANE_FAILED`) and an unfinished one `incomplete`; a green run is `warn` `PREGATE_NO_LANE` naming the unmapped files |
|
||||
| `strays.allow` | intentional root files or new top-level directories |
|
||||
| `local` | `workflow.yml/job` lanes a local run satisfies: a platform-agnostic toolchain (an `ubuntu-latest` suite that runs the same on a Mac), or a repo whose CI is off. Their tests run here instead of becoming `requires-remote`, except that on a machine under 8 cores with a `remote_gate` or a remote `test_backend` the small-machine rule still records the test tier as a `requires-remote` obligation. `--explain` notes an id that matches no workflow lane |
|
||||
| `guards` | `[{ id, when: [globs], command, fix?, timeout_s? }]`: repo commands a lane must pass when a touched file matches `when` (manifest checks, subprocess goldens the import graph cannot see); see [guards](#guards) |
|
||||
@@ -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)). `--from-run` and the main-push reuse job verify runs against them too ([from-run](#from-run), [CI reuse](#ci-reuse)) |
|
||||
| `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`
|
||||
@@ -138,6 +141,19 @@ unmapped file is `PREGATE_NO_LANE` again: the warning is for the first /ship
|
||||
in a new repository (three Captains lost their first pass to it), never a way
|
||||
to hide missing coverage. `--policy-from head` with a head policy is strict too.
|
||||
|
||||
<a id="lane-requirements"></a>
|
||||
**Lane requirements.** "Any policy" above means a policy that declares lanes:
|
||||
one with a `dependencies`, `empty_lanes`, `selection` or `local` key, even an
|
||||
empty one (`gstack-pregate init` writes the first three). A policy with none of
|
||||
them, such as
|
||||
`{"timeout_s": 1800}` to give long tests more time, keeps the no-policy lane
|
||||
verdicts: an unmapped file is the `warn` `PREGATE_NO_POLICY`, the selected
|
||||
tests run, a red lane is `fail`, and the starter says the active policy
|
||||
declares no lanes instead of claiming there is none. So raising
|
||||
`timeout_s` never needs a `"**": []` catch-all. Add `dependencies` when you
|
||||
want unmapped files to fail. The same rule applies to the user-owned file and to
|
||||
a `--policy-from head` bootstrap (gbrain-evals #272, GST-775).
|
||||
|
||||
`gstack-pregate init` writes a draft built from the repo, not a fixed example.
|
||||
It prints the languages it found and the runner each language's tests would
|
||||
get, then maps every file the import graph cannot place to the tests of the
|
||||
@@ -157,7 +173,7 @@ The list lives in its own file, not in `pregate.json`, because older pregate rea
|
||||
|
||||
```
|
||||
gstack-pregate [--stage preflight|tests|all] [--only <check,..>] [--allow-repo-commands] [--policy-from base|head]
|
||||
[--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--explain] [--json]
|
||||
[--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--from-run <id|url>] [--explain] [--json]
|
||||
gstack-pregate verify | clear --remote <workflow.yml/job> --run-url <url> | lanes | validate | init
|
||||
```
|
||||
|
||||
@@ -175,12 +191,23 @@ 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
|
||||
literal, the whole test tree (nightly- and Heavy-only files included) is
|
||||
searched for the old value; a consumer the branch did not update fails with
|
||||
searched for the old value in files that name the constant or, when the old
|
||||
value is longer than one character, its module (the module's file name not
|
||||
followed by another extension, or a path ending
|
||||
in its stem followed by its own extension, for a JS/TS module also any JS/TS
|
||||
extension or `.d.ts`, a sentence-ending `.`, or anything but a word
|
||||
character, `.` or `-`; so a sibling `<stem>.txt`, `<stem>-v2.ts` or
|
||||
`<file>.snap`, or `<stem>.ts` beside a shell module, is not the module); a
|
||||
consumer the branch did not update fails with
|
||||
`file:line`. A version-history row, a line that starts a bracketed row with
|
||||
the old value (`[45, "<digest>"]`), is skipped when the same table (a run of
|
||||
consecutive lines starting with `[`) also holds a row for the new value that
|
||||
@@ -259,9 +286,10 @@ Two stages with separate clocks, both printed:
|
||||
`.cjs`, `.mts`, `.cts`, then `index.ts`, `index.tsx`, `index.js`, `index.jsx`
|
||||
and `index.mjs` inside the directory; the first file that exists wins.
|
||||
A touched source file with no lane is `FAIL lanes — <file> has no test lane`
|
||||
once any policy exists (the base copy, the user-owned file or `--policy-from
|
||||
head`); with no policy anywhere it is the `warn` `PREGATE_NO_POLICY` with a
|
||||
starter printed ([no policy](#no-policy)); never a pass. Markdown that no
|
||||
once a policy that [declares lanes](#lane-requirements) exists (the base copy,
|
||||
the user-owned file or `--policy-from head`); with no policy anywhere, or one
|
||||
that declares no lanes, it is the `warn` `PREGATE_NO_POLICY` with a starter
|
||||
printed ([no policy](#no-policy)); never a pass. Markdown that no
|
||||
test depends on is a docs/no-tests empty lane
|
||||
instead (`docs/no-tests: N documentation file(s)`), including when the base
|
||||
branch has no `.gstack/pregate.json`. The dependency is the same import graph
|
||||
@@ -435,7 +463,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 +472,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 +643,215 @@ 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 or a list of
|
||||
every OS the run's test jobs ran on (the profile's `os` must be one of them).
|
||||
|
||||
**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 one the profile lists (`push`,
|
||||
`workflow_dispatch` or `pull_request`), and it completed with success. A
|
||||
`pull_request` run tested GitHub's merge commit, not its head, so its
|
||||
`tree.json` must name the head (`head_sha`) and the merge commit (`sha`), and
|
||||
the tree git computes for that merge commit must equal the head's tree: the
|
||||
head already contained its base, so the merge tested exactly the head tree
|
||||
(`tree_mismatch` otherwise). 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 a
|
||||
follow-up (GST-555), so a repository that declares lane profiles clears those
|
||||
lanes with `gstack-pregate clear --run`.
|
||||
|
||||
**gstack's own profiles.** gstack's `.gstack/pregate.json` declares two, and
|
||||
names neither after a lane: `ci-reuse/free-tests` (the complete Free Tests run:
|
||||
every non-matrix job and the `free-tests` aggregate, `os` Linux) and
|
||||
`ci-reuse/windows-free-tests` (`windows-plan` and the `windows-free-tests`
|
||||
aggregate, `os` Windows). Each accepts `pull_request` and `workflow_dispatch`
|
||||
runs and reads the `tested-tree` artifact, which both aggregates upload only
|
||||
after their strict verification passes. The main-push reuse job and
|
||||
`--from-run` read them. No lane has a profile, so every macOS and Windows lane
|
||||
still clears by `--run-url` as before. A pull request cut before these
|
||||
workflows uploaded `tested-tree` has no record a lane profile could verify, so
|
||||
lane profiles wait until open pull requests carry the producer (GST-555).
|
||||
|
||||
**`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, and `--from-run` never skips them.
|
||||
Pull-request required checks and dispatched runs always run fresh; only a
|
||||
push to main reuses ([below](#ci-reuse)).
|
||||
|
||||
<a id="from-run"></a>
|
||||
### The tests stage from a Free Tests run (`--from-run`)
|
||||
|
||||
`gstack-pregate --stage tests --from-run <run id|url>` takes a Free Tests run
|
||||
that already passed on this exact tree, for example the one CI ran on the
|
||||
pushed head, instead of running its files again here. The run is verified
|
||||
online, once, against the base branch's run profiles for this machine's OS
|
||||
whose workflow holds a lane this machine runs (gstack's `ci-reuse/free-tests`
|
||||
on Linux), with pregate.json's own head as a further veto head. Its tree must be this tree exactly (no
|
||||
stamp-only substitution). Then:
|
||||
|
||||
- The selected local test files the run's `tree.json` lists count as passed.
|
||||
The lanes check records them under `from_run` with the run receipt
|
||||
(`run tree=… tests=… runs=<id>/<attempt> profiles=… heads=… verified=…`) and
|
||||
prints `REUSE: accepted <n> of <m> selected local test file(s) from <receipt>;
|
||||
<k> still run`.
|
||||
- The rest run as they would have: locally, or recorded as the remote gate
|
||||
obligation on a small machine, or through `--remote`.
|
||||
- A run that does not verify prints `REUSE: refused reason=<code> — …; every
|
||||
selected test runs here` and changes nothing else. The codes are
|
||||
verifyRun's, plus `tree_mismatch` (another tree), `no_tests`, `no_profile`
|
||||
(no profile for this OS covers a local lane's workflow), `no_verified_run`
|
||||
(more than one profile applied and each refused; the detail lists every
|
||||
refusal) and `profiles_unavailable` (the base config or the GitHub origin
|
||||
could not be read).
|
||||
|
||||
`--from-run` applies to the tests stage only, so `--stage preflight` refuses it
|
||||
as a usage error. It changes nothing in the preflight stage: with `--stage all`
|
||||
the preflight checks, the secrets scan included, run as before, and
|
||||
`--stage tests` runs no preflight check in that invocation, with or without it.
|
||||
|
||||
<a id="ci-reuse"></a>
|
||||
### Main pushes reuse the merged pull request's run
|
||||
|
||||
A squash merge whose pull request head already held main produces a push with
|
||||
the identical tree, and Free Tests and Windows Free Tests used to run on it a
|
||||
second time. Now each workflow starts with a `reuse` job that runs only on a
|
||||
push to `main`, with job-level `contents: read` and `actions: read` (every other
|
||||
job keeps the workflow's `contents: read`). It runs
|
||||
`scripts/ci-tree-reuse.ts find`, which:
|
||||
|
||||
1. finds the pull request whose merge commit is the pushed commit
|
||||
(`repos/{repo}/commits/{sha}/pulls`, one page of at most 10), merged into
|
||||
main from a same-repository head (`no_pr`, `ambiguous_pr`, `fork_head`);
|
||||
2. requires that head's tree to equal the pushed tree (`tree_mismatch`);
|
||||
3. lists that head's `pull_request` and `workflow_dispatch` runs of the same
|
||||
workflow, newest first, at most 10 (`no_candidate`); and
|
||||
4. accepts the first one `verifyRun` accepts under the profile in the pushed
|
||||
tree's `.gstack/pregate.json` (`no_verified_run` lists each refusal; a newer
|
||||
red run on the head vetoes an older green one).
|
||||
|
||||
It writes `reused=true` with the run's id and link, or `reused=false` and the
|
||||
reason, and prints one `REUSE:` line in the step summary. Every suite job
|
||||
needs `reuse` and runs unless it succeeded with `reused=true`; their
|
||||
conditions start with `!cancelled()` because GitHub otherwise skips a job whose
|
||||
needed job was skipped, and `reuse` is skipped on every pull request and
|
||||
dispatch run. The required aggregates (`free-tests`, `windows-free-tests`)
|
||||
choose a path in their first step, before any download. A verified reuse writes
|
||||
the reused run's link to the summary and passes; anything else, a failed or
|
||||
skipped `reuse` job included, takes the strict path unchanged. Only the strict
|
||||
path records and uploads `tested-tree`, after its verification passes, so a
|
||||
red, partial or reused run never leaves a record and a reuse is never reused.
|
||||
The pull request lookup needs no `pull-requests` permission on a public
|
||||
repository; a private fork of this setup would need `pull-requests: read`,
|
||||
and without it every push falls back to the full run. Windows Free Tests now
|
||||
runs nightly and by hand only (docs/support-tiers.md), never on a push, so its
|
||||
`reuse` job is always skipped and every nightly run takes the strict path.
|
||||
|
||||
<a id="guards"></a>
|
||||
## Guards and the lane-done rule
|
||||
|
||||
@@ -672,6 +913,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.
|
||||
|
||||
@@ -82,7 +82,7 @@ a time; each is independent.
|
||||
|
||||
| File | Why | Without it |
|
||||
|---|---|---|
|
||||
| `.gstack/pregate.json` | Maps changed paths to test lanes, so `/ship` runs the tests that cover the diff and the remote gate selects correctly ([docs/pregate.md](pregate.md)) | `/ship` fails every touched file with `PREGATE_NO_LANE`. Write it once, from one owner, before the crew starts. |
|
||||
| `.gstack/pregate.json` | Maps changed paths to test lanes, so `/ship` runs the tests that cover the diff and the remote gate selects correctly ([docs/pregate.md](pregate.md)) | `/ship` warns on every touched file with no test lane (`PREGATE_NO_POLICY`) and prints a starter policy; the selected tests still run. A policy that sets only `timeout_s` or other keys without `dependencies`, `empty_lanes`, `selection` or `local` behaves the same. Write it once, from one owner, before the crew starts. |
|
||||
| `.gstack/ship-policy.json` | `queue_mode`, the `train` block (gate and free workflow names, the full-gate check, the train size and fill wait), the stamp files ([docs/ship-policy.md](ship-policy.md)) | Per-PR queue; `train …` refuses with `QUEUE_TRAIN_OFF`. |
|
||||
| `CHANGELOG.md` + `VERSION` | A stamp per merge and a twice-weekly release with notes ([docs/releases.md](releases.md)) | Skip the version and changelog steps; the train still works on the tree receipt. |
|
||||
| `AGENTS.md` validation discipline | Diagnose before fixing, focused checks during repair, one full suite at the end on a frozen tree | Threads retry blindly and report passes from older revisions. |
|
||||
@@ -284,7 +284,7 @@ measured later does not count anything that happened after that week.
|
||||
| Gate runs green / red / cancelled | completed runs of the gate workflow (`workflow_dispatch` by default; `--gate-event any`); cost from the run's `report-verdict-a*` artifact (`collector-outcomes.json` `.verdict.costUsd`), else the `**$N** rule/judge cost` or `cost=$N` line of the PR's verdict comment | green on 95%+ of runs, cancelled near 0, cost known for every run |
|
||||
| Known gate cost per merged PR | known cost / merged PRs; unknown-cost runs make it a floor | known, stable or falling week over week |
|
||||
| Red-train rate | runs whose head branch starts with `--train-prefix`, cancelled excluded; red = conclusion other than success; red backstops and screens shown beside it | under 5% (each red train is a bisect plus a rebuild, about two gates) |
|
||||
| Flaky events | `flake-records-a*` records of the newest gate runs (count, known rerun spend) and `flake-ledger-*` entries of free runs on the default branch; a run whose artifacts cannot be read is counted and named, never scored 0 | 0 repeat cases; under 1 flaky case per 10 gates; free retries under 1% of runs |
|
||||
| Flaky events | `flake-records-a*` records of the newest gate runs (count, known rerun spend) and `flake-ledger-*` entries of free runs on the default branch; a run whose artifacts cannot be read is counted and named, never scored 0, and a main push that reused its pull request's run (no artifacts, `reuse` job green) is left out and named | 0 repeat cases; under 1 flaky case per 10 gates; free retries under 1% of runs |
|
||||
| Issues opened / closed | GitHub search, created and closed in the window | closed ≥ opened; nothing severe older than a week |
|
||||
| Releases cut | releases published in the window, drafts excluded | at least one tagged release a week, on schedule |
|
||||
|
||||
|
||||
+101
-23
@@ -93,11 +93,14 @@ incomplete coverage, which blocks until the reviewer is rerun.
|
||||
for every reviewer (the gate scan and QA probes keep the whole candidate) and
|
||||
classifies with `--range-base`. Only a blocking finding in that range or in
|
||||
the diff is `fix`; every below-floor finding is a `note`.
|
||||
3. **Two full passes is the cap**, plus at most one fix-check; a scoped pass
|
||||
([below](#scoped-pass)) is a full pass. A blocking
|
||||
finding still open after the last allowed step stops the run and names the
|
||||
finding (path, severity, problem) for the owner; a third full pass is the
|
||||
owner's call, never the loop's. The cap approves nothing.
|
||||
3. **Two rounds is the cap** (R51): a full pass, a scoped pass
|
||||
([below](#scoped-pass)) or a fix-check is a round. After round 2 the run
|
||||
finishes with its open notes listed in the PR body and receipt, or stops for
|
||||
the owner when a blocking finding is open, or was fixed in round 2 and is
|
||||
unverified, or a round-2 fix edited a gate-class path, naming it (path,
|
||||
severity, problem). A third round is the owner's
|
||||
grant of one scoped fix-check (recorded in `review_loop_exception`), never the
|
||||
loop's. The cap approves nothing.
|
||||
|
||||
<a id="transitions"></a>
|
||||
### Transitions
|
||||
@@ -112,10 +115,12 @@ incomplete coverage, which blocks until the reviewer is rerun.
|
||||
| a full pass that fixed a blocking finding, below the cap, on a diff below full depth with a fix range of at most 150 runtime lines | scoped pass on the range |
|
||||
| a full pass that fixed a blocking finding, below the cap (full depth, a larger range, or a range or depth it could not read) | full pass on the range |
|
||||
| a full pass whose fix touched a gate-class path, below the cap | full pass on the range |
|
||||
| a full pass that fixed only below-floor findings | one fix-check |
|
||||
| a full pass at the cap that fixed a blocking finding | one fix-check |
|
||||
| a full pass that fixed only below-floor findings | one fix-check (round 2) |
|
||||
| a light pass 1 whose below-floor fixes change no runtime lines | done |
|
||||
| round 2 (a full pass or a fix-check) that fixed a blocking finding or edited a gate-class path | stop: name it for the owner, who may grant one scoped fix-check |
|
||||
| round 2 (a full pass or a fix-check) that fixed only below-floor findings | done: notes in the PR body |
|
||||
| a full pass that leaves a blocking finding open (fixed or not) | stop: name it for the owner |
|
||||
| a fix-check that found nothing blocking (below-floor fixes before it included) | done |
|
||||
| a fix-check that found nothing blocking | done |
|
||||
| a fix-check that found a blocking finding | stop: name it for the owner |
|
||||
|
||||
A **fix-check** is one reviewer reading the touched hunks, the fix list, the
|
||||
@@ -186,10 +191,47 @@ in a data file still schedules the outside voice.
|
||||
|
||||
| Depth | When | Pass 1 dispatches |
|
||||
|---|---|---|
|
||||
| **light**, runtime 0 | docs and tests only, or only registered outputs | the core checklist only; the outside voice when the diff publishes numbers |
|
||||
| **light**, runtime 1–50 | `SMALL_DIFF_LINES` or fewer runtime lines | core, one **batched** specialist (the batched checklists below plus the Red Team prompt in one context), the native adversarial pass (kept: backtest inconclusive, GSTA-147 D5) and the outside voice on numbers |
|
||||
| **standard** | more than 50 runtime lines | one batched specialist at `medium` and the Red Team at 200 runtime lines (or on a CRITICAL); the batched reader leaves docs and registered outputs out of its diff; adversarial and outside voice read everything |
|
||||
| **full** | gate class, or any classifier error | one batched specialist on the whole diff, the Red Team always, no hit-rate gating |
|
||||
| **light**, runtime 0 | docs, tests, eval write-ups, or only registered outputs | one reader: the core checklist; the outside voice instead reads a diff that publishes numbers |
|
||||
| **light**, runtime above 0 | code with no risk class, at any size (R51: size never raises the depth) | core, one **batched** specialist (the batched checklists below plus the Red Team prompt in one context), the native adversarial pass (kept: backtest inconclusive, GSTA-147 D5) and the outside voice on numbers |
|
||||
| **standard** | `--depth-floor standard`, or a light diff raised by a fixed blocker or a CRITICAL | one batched specialist at `medium` and the Red Team at 200 runtime lines (or on a CRITICAL or a raise); the batched reader leaves docs and registered outputs out of its diff; adversarial and outside voice read everything |
|
||||
| **full** | gate class, a [risk class](#risk-classes), or any classifier error | one batched specialist on the whole diff, the Red Team always, no hit-rate gating |
|
||||
|
||||
<a id="risk-classes"></a>
|
||||
**Risk classes (R51).** `lib/review-risk.ts` reads a code diff's paths and its
|
||||
added and deleted lines; any class makes the depth full (`risk:<class>` in
|
||||
`reasons=`, `review_depth.risk` in the receipt). money, auth and secret are the
|
||||
[targeted pass](#targeted-pass-on-money-gate-auth-and-secret-code)'s matchers.
|
||||
**money** also reads metering and charge words in a path or on a line (meter,
|
||||
metering, metered, charge, charged, billable, billed, credits) and three
|
||||
pairs: usage beside a token or cost word, proxy beside billing, and a client
|
||||
beside a cost or charge word, so a CLI's usage text and a plain proxy stay out
|
||||
(GST-850, from gbrain-evals #289, whose metering proxy and billing clients read
|
||||
`required=none`). **security** is a trust-boundary path (trust, redact, sanitize, neutralize,
|
||||
consent, escape) or line (`shell: true`, `exec(` and `execSync(`, `eval`, `new Function(`,
|
||||
raw HTML sinks, a shell run through argv, recursive deletes), any call to an
|
||||
outbound HTTP client (`fetch`, `http(s).get` or `.request`, axios, got, undici,
|
||||
`requests`, `httpx`, urllib, with no exemption for a literal URL), `new URL` from
|
||||
input, redirect toggles, proxy settings and host allowlists, archive extraction
|
||||
and path traversal (`tar.x`, adm-zip, yauzl, extract-zip, `extractall`, `tar -x`,
|
||||
`unzip`, `path.join` or `resolve` on request or entry input, a `'..'` check added
|
||||
or removed), and any changed line that names the `child_process` or
|
||||
`node:child_process` module, in any import or require spelling. The GST-850
|
||||
rules are coarse on purpose and parse no import clause or call arguments;
|
||||
`test/review-risk-linear.test.ts` holds each of them under 100 ms on
|
||||
200,000-character hostile lines, its anchor followed by a long run of the
|
||||
characters it repeats included. That claim covers only the rules GST-850 adds,
|
||||
not R51's older ones. **data** is user data: a path naming pii, gdpr, ccpa, privacy,
|
||||
personal, account, customer, cookie, persistence, storage or database, or a line
|
||||
that drops a table or column, deletes rows or bulk-deletes records. Auth scope,
|
||||
migrations and deleted code paths were already gate class. `RISK_RULES` hashes
|
||||
the patterns and `DEPTH_RULES` includes it.
|
||||
|
||||
**UI text and SVGs** stay code (light unless a risk class applies). A docs path
|
||||
for UI copy, locale values and chart SVGs was built as an allowlist in GST-850 and
|
||||
dropped for review cost: its first review round found a new bypass class in each
|
||||
version, the same pattern as three rounds of R51. When a diff publishes numbers,
|
||||
the batched specialist's diff keeps the write-up's text files instead of leaving
|
||||
docs out.
|
||||
|
||||
**Batched checklists (the lean ensemble, GSTA-167 part 2).** One specialist
|
||||
subagent reads Testing and Security on every code or gate diff, plus
|
||||
@@ -230,7 +272,7 @@ and lockfiles stay runtime. To adopt it, commit an entry like this on the base b
|
||||
|
||||
**Gate paths for the machinery itself.** An edit to `lib/review-depth.ts`,
|
||||
`lib/review-collect.ts`, `bin/gstack-review-plan`, `lib/review-scope.ts`,
|
||||
`lib/review-targeted.ts`, `review/specialists/targeted.md`, `lib/ship-queue*.ts`, `lib/regen.ts`, `.gstack/generated.json`,
|
||||
`lib/review-targeted.ts`, `lib/review-risk.ts`, `review/specialists/targeted.md`, `lib/ship-queue*.ts`, `lib/regen.ts`, `.gstack/generated.json`,
|
||||
`ship/sections/review-army.md.tmpl`, `ship/sections/adversarial.md.tmpl` or
|
||||
`scripts/resolvers/review-army.ts` is gate class: changing how much review runs gets
|
||||
the most review. `rules=` (`DEPTH_RULES`) hashes the deciding constants and patterns,
|
||||
@@ -271,8 +313,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
|
||||
@@ -281,6 +324,10 @@ the specialists, audits and watchers at the host default too
|
||||
prompt. At runtime 0 light runs no adversarial pass either.
|
||||
- Standard reads thresholds on runtime lines, not the whole diff, so a test-heavy
|
||||
diff with few runtime lines dispatches fewer specialists.
|
||||
- R51: size never raises the depth; non-risky code is light at any size, and a risk
|
||||
class makes it full. Two rounds is the cap, a fix-check
|
||||
included: a blocker fixed in round 2 stops for the owner instead of earning a
|
||||
fix-check, and a light pass 1 whose below-floor fixes change no runtime lines is done.
|
||||
- A JSON, CSV, TSV or raster data file no code names is docs, not runtime (GSTA-167
|
||||
part 4a). A binary image no code names is no longer a `binary_code_path` error
|
||||
that forces full depth; one that code names still is.
|
||||
@@ -353,6 +400,7 @@ puts in the receipt:
|
||||
| `review_loop.fixed` | findings fixed in this PR |
|
||||
| `review_loop.scope` | the `review_scope` the loop ran under |
|
||||
| `review_loop.fix_checks` | fix-checks this ship ran (never passes) |
|
||||
| `review_loop.rounds` | passes plus fix-checks (R51 caps it at 2; the queue's `REVIEW_DEPTH:` line prints it) |
|
||||
| `review_loop.dispatches` | reviewer dispatches by role: `specialist`, `red-team`, `adversarial-native`, `outside-voice`, `fix-check`, `targeted` |
|
||||
| `review_loop.est_input_tokens` | prompt plus diff bytes the dispatches read, divided by 4: a relative estimate for comparing runs, not a bill |
|
||||
| `review_targeted` | the [targeted pass](#targeted-pass-on-money-gate-auth-and-secret-code): the `classes` it covered, `rules`, `brief_sha`, the receipt's own `product_patch_id`, `status` (`done` only with its output collected and the committed diff it was rendered on still the one at verify, else `failed`; absent when write got no record, which the queue reports as `legacy`), `findings` and `raised_by`; a carry keeps it while the product patch is unchanged |
|
||||
@@ -365,7 +413,7 @@ specialist stats they record). The summary line appends `fix_checks=`,
|
||||
loop prints the old line.
|
||||
|
||||
Beside the counters, `write` records `review_depth` (the depth, runtime lines,
|
||||
rules hash, base and registry commits, and the gate scan bound to the head) and
|
||||
rules hash, base and registry commits, the gate scan bound to the head and the risk classes) and
|
||||
the queue refuses a receipt below the depth its head needs
|
||||
([ship-policy.md#ship-receipt](ship-policy.md#ship-receipt)). A stop the owner
|
||||
accepted after a fix-check edit goes in `review_loop_exception`, never in prose
|
||||
@@ -375,7 +423,8 @@ The receipt's summary line prints `passes=<n> blocking_found=<k> deferred=<m>`,
|
||||
which is what a scorecard tracks (target: a median of 1 pass per PR). A
|
||||
receipt carried across a base merge or a test-only fix
|
||||
([ship-policy.md#receipt-carry](ship-policy.md#receipt-carry)) keeps these
|
||||
counters, because the reviewed product diff is unchanged.
|
||||
counters, because the reviewed product diff is unchanged or its only
|
||||
difference passed a scoped review.
|
||||
|
||||
## The scope knob
|
||||
|
||||
@@ -383,8 +432,8 @@ counters, because the reviewed product diff is unchanged.
|
||||
|
||||
| Value | Pass 1 | Blocking finding in unchanged code | Pass 2+ | Cap |
|
||||
|---|---|---|---|---|
|
||||
| `frozen` (default) | the diff | filed as an issue, listed as deferred | the range since the previous pass, blocking only | 2 full passes + 1 fix-check |
|
||||
| `diff-plus-blocking` | the diff | fixed in this PR | the range since the previous pass, blocking only | 2 full passes + 1 fix-check |
|
||||
| `frozen` (default) | the diff | filed as an issue, listed as deferred | the range since the previous pass, blocking only | 2 rounds (a fix-check is one) |
|
||||
| `diff-plus-blocking` | the diff | fixed in this PR | the range since the previous pass, blocking only | 2 rounds (a fix-check is one) |
|
||||
| `all` | everything | fixed in this PR | the whole diff, every finding | 3 fix cycles |
|
||||
|
||||
`diff-plus-blocking` is the fallback for a repository where deferred issues pile
|
||||
@@ -460,7 +509,11 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
literal diff base, the plan's `DIFF_CMD` excludes, stack, test framework,
|
||||
learnings, checklist paths, the Red Team's merged findings and specialist count,
|
||||
and the outside voice's branch diff. A section that lost an anchor or placeholder
|
||||
refuses (`BRIEF_RENDER_FAILED`) instead of rendering something else.
|
||||
refuses (`BRIEF_RENDER_FAILED`) instead of rendering something else, and so does
|
||||
a slot the section leaves unfilled (every reader but fix-check and the outside voice).
|
||||
That check runs before the findings and learnings go in, and they reach the reader
|
||||
as written, so a finding that quotes `origin/<base>` neither fails the render nor
|
||||
gets filled (GST-797).
|
||||
2. The dispatch program passes that file's content unchanged as the reader's
|
||||
prompt and runs `sent` on what it passed: a hash that differs from the rendered
|
||||
one is `BRIEF_MISMATCH`, and that reader's review does not count.
|
||||
@@ -476,6 +529,15 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
adversarial voice's FIXABLE collects as CRITICAL (normalizer v2, GSTA-167). An unnumbered, unlabeled bullet with no file:line
|
||||
under a paragraph finding whose Severity or Blocks line has not come yet is that finding's detail, so the field line after it
|
||||
lands on the finding; a numbered item is its own finding.
|
||||
A negated severity word still collects at its label ("found nothing CRITICAL", "No P0/P1 issues"), so a reader's
|
||||
closing summary is a known phantom: it stays at that label in the judged set, lead judgment can fold it only into a
|
||||
finding at least as severe, and it counts toward what blocks. GST-766 skipped negated labels and lost
|
||||
real blockers named in an exception clause ("No CRITICAL issues except the token leak in log.ts:3"); GSTA-167
|
||||
reverted that. Each prose finding's line also
|
||||
records the Severity and Blocks its own text states as `stated`, beside the collected label and never in its place;
|
||||
a classified finding that carries `stated` shows it in its deferred-review issue next to the label ("CRITICAL (the
|
||||
reviewer wrote Severity: Low, Blocks: no)"). /ship's Step 9.4 does not copy `stated` into its findings file yet: that
|
||||
instruction lives in a reviewer brief that changes only after a no-loss replay. What blocks is unchanged.
|
||||
A phantom finding costs a fold; a lost one costs the gate.
|
||||
A reply with no finding counts as a review only with the clean marker its own brief asks for: `NO FINDINGS` alone on a
|
||||
line for the specialists, the batched specialist, the targeted reader, the Red Team and fix-check, the `Recommendation:`
|
||||
@@ -503,8 +565,9 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
prompts prints `VERIFY: no brief records` (exit 3), never clean.
|
||||
6. `pass --base <b> --out <dir> --pass <id> -- <plan flags>` runs one pass from a
|
||||
staged manifest (`<dir>/pass-manifest.json`): audit readers marked first, then
|
||||
the specialists with QA and the other audits, then the Red Team, then Step 11's
|
||||
voices. Each reviewer keeps its plan entry and is rendered by the same code as
|
||||
the specialists with the other audit readers, then the Red Team, then Step 11's
|
||||
voices. A QA extra is refused before any reader is created: Step 9.2.1's probes
|
||||
are the parent's. Each reviewer keeps its plan entry and is rendered by the same code as
|
||||
`render`, once its inputs exist; the Red Team's findings file is the specialists'
|
||||
`merged.jsonl` lines byte for byte. A brief leaves the bin in indexed chunks of at
|
||||
most 65,536 characters (`chunk`), so no tool output line carries a whole brief and a
|
||||
@@ -513,11 +576,26 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
create whose result was lost by its title and never starts a reader twice; a
|
||||
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.
|
||||
unchanged by the gstack Capy skill. A full repeat pass takes `--diff-base` (RANGE_BASE; a scoped pass, none), a
|
||||
fix-check plan renders its reader from `--fix-list`, `--hunks`, `--tests` and `--callers`,
|
||||
`--audit-only` runs just the `--extras` readers (Step 14.5's docs audit), and the fork
|
||||
point is found against `origin/<base>` first, as preflight does.
|
||||
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
|
||||
|
||||
- `lib/review-brief.ts`, `lib/review-ledger.ts`, `lib/review-pass.ts`, `lib/capy-review-pass.js`, `bin/gstack-review-dispatch`: [verbatim dispatch](#verbatim-dispatch) (`test/review-dispatch.test.ts`, `test/review-pass.test.ts`).
|
||||
- `lib/ship-parts.ts`, `bin/gstack-ship-parts`: the section text each /ship phase uses, returned once per run inside the phase program before it; `next --resend` sends an unreceived batch again (`test/ship-parts.test.ts`).
|
||||
|
||||
- `scripts/resolvers/review-army.ts`: the floor and `NO FINDINGS` in every specialist and Red Team prompt (rendered into `review/sections/review-army.md` and `ship/sections/review-army.md`), and the lead judgment's "On record" line that turns a [ruled class](#rulings-below-the-floor) into a note; in `/ship` every specialist and Red Team prompt it renders carries its diff base as a literal sha the parent writes in (`DIFF_BASE=<diff-base>`: the merge base on pass 1, `RANGE_BASE` from pass 2 on) beside its plan's `DIFF_CMD` excludes, because a reviewer is its own process and never sees the parent's shell variables; the fan-out batches above.
|
||||
- `ship/sections/review-army.md` Step 9: the plan's `REVIEW_DEPTH`, `GATE_SCAN` and `--depth-floor` on every run, the depth fields of the review-log row, passes, `RANGE_BASE`, classify, defer, render, the cap check before any edit, the Step 11 late-defer row, scratch files under `.gstack/tmp/`, the cap decisions; `ship/sections/adversarial.md` Step 11: the outside voice reads the same range and scopes queued fixes the same way.
|
||||
|
||||
+10
-3
@@ -41,7 +41,7 @@ unless `--label` is given.
|
||||
|
||||
**Counting calls.** `gstack-ship-budget calls --events <file> [--since <iso>] [--until <iso>]` reads
|
||||
the host's transcript items (a JSON array, or `{ "items": [...] }` as Capy's `tools.thread.get`
|
||||
transcript returns) and prints
|
||||
transcript returns; a leading `~` or `~/` in `<file>` means the home directory) and prints
|
||||
`CALLS: total= decision= keepalive= idle_gaps_over_300s= max_gap_s= first= last= source=`. Every tool
|
||||
item is one parent call; a `wait`, or a program that only reads status (plus at most one bash for its
|
||||
note), is a keep-alive, and a gap over 300 s between calls outlives the prompt cache
|
||||
@@ -76,8 +76,14 @@ one program measure nothing: Capy books a call after its program ends.
|
||||
|
||||
A jam's own counter includes its own subagents, outside voices too (measured 2026-10-30: +10.92M
|
||||
across a six-reader dispatch with two ~0.35M parent calls between). `"this"` is the root thread,
|
||||
whose counter also holds every sibling tree, so a subagent running /ship passes its own jam id;
|
||||
a root thread's own jam id is its `"this"`. A Capy handoff child cannot read its own jam id, so it
|
||||
whose counter also holds every sibling tree. A subagent running /ship cannot read its own jam id, so it
|
||||
reads `"this"` and starts with `--subagent --reading-from this --meter-scope tree` (`GSTACK_SHIP_SUBAGENT=1` in its
|
||||
brief stands in for `--subagent` only; `--reading-from this` is still required): the run is tree-scoped, and every BUDGET, NOTE, BUDGET_FINAL
|
||||
and BUDGET_TIMELINE line it prints is followed by `METER: tree (root counter, upper bound)`. A
|
||||
subagent that asks for `--meter-scope self` while reading `"this"` is refused
|
||||
(`METER_SCOPE_PARENT`). A `--token-cap` on a tree-scoped run stops on that upper bound, so sibling
|
||||
lanes count against it. Exact per-run cost comes from a root replay thread: a root thread that runs
|
||||
only that /ship, whose `"this"` is its own counter. A root thread's own jam id is its `"this"`. A Capy handoff child cannot read its own jam id, so it
|
||||
verifies with no reading and prints `METER: parent`; the parent's handoff and reconcile readings,
|
||||
taken on its own counter (which holds the child), measure it.
|
||||
|
||||
@@ -286,6 +292,7 @@ stop rules) before claiming a saving from the cap or from delegation.
|
||||
|
||||
[BUDGET_CAP_REACHED](troubleshooting.md#budget-cap-reached) ·
|
||||
[BUDGET_READING_MISSING](troubleshooting.md#budget-reading-missing) ·
|
||||
[METER_SCOPE_PARENT](troubleshooting.md#meter-scope-parent) ·
|
||||
[BUDGET_READING_REJECTED](troubleshooting.md#budget-reading-rejected) ·
|
||||
[BUDGET_METER_UNAVAILABLE](troubleshooting.md#budget-meter-unavailable) ·
|
||||
[BUDGET_LEDGER_INVALID](troubleshooting.md#budget-ledger-invalid) ·
|
||||
|
||||
+308
-36
@@ -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`. |
|
||||
|
||||
@@ -92,7 +92,7 @@ hard-code:
|
||||
"changelog": "CHANGELOG.md",
|
||||
"history": "squash-ok",
|
||||
"stamp_sensitive_gates": ["tests"],
|
||||
"changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-free-tests", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] },
|
||||
"changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-smoke", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] },
|
||||
"min_gstack_version": "1.91.340.0"
|
||||
}
|
||||
```
|
||||
@@ -210,16 +210,18 @@ READY PR as conflicting each time main stamps; the train merges the CHANGELOG
|
||||
by section, so the lane never merges main for it. The conflict does mean GitHub
|
||||
has no merge ref for the PR, so no `pull_request` CI runs on the head and
|
||||
`ready` prints `ci: none at <head12>` with a `ci-hint:` line. The lane then
|
||||
runs `ready … --dispatch-ci free-tests.yml,windows-free-tests.yml [--timeout
|
||||
runs `ready … --dispatch-ci free-tests.yml,windows-smoke.yml [--timeout
|
||||
<s>]` (under a train policy): it dispatches each named workflow on the branch
|
||||
(`gh workflow run <wf> --ref <branch>`, skipped when a run at this head already
|
||||
concluded), waits for the runs (`CI_DISPATCH: <wf> --ref <branch> run <id> <url>
|
||||
<conclusion>`), clears every requires-remote pregate lane whose workflow is one
|
||||
of them with the run URL (`PREGATE_CLEARED: <lane> <url>`; a lane of another
|
||||
workflow stays uncleared and is named in the refusal), and then reads the head
|
||||
as usual, which passes because the dispatched check runs attach to the commit;
|
||||
the `ci-hint:` line then says those non-pull_request runs count. A red run
|
||||
clears no lane and refuses (`QUEUE_NOT_READY`); a run still going at `--timeout`
|
||||
concluded, and skipped with a note when the base has the workflow file but the
|
||||
branch predates it, since GitHub reads the file from the branch), waits for the
|
||||
runs (`CI_DISPATCH: <wf> --ref <branch> run <id> <url> <conclusion>`), clears
|
||||
every requires-remote pregate lane whose workflow is one of them with the run
|
||||
URL (`PREGATE_CLEARED: <lane> <url>`; a lane of another workflow stays
|
||||
uncleared and is named in the refusal), and then reads the head as usual,
|
||||
which passes because the dispatched check runs attach to the commit; the
|
||||
`ci-hint:` line then says those non-pull_request runs count. A red run clears
|
||||
no lane and refuses (`QUEUE_NOT_READY`); a run still going at `--timeout`
|
||||
(default 2700 s) is `QUEUE_TRAIN_PENDING`, and rerunning the command keeps
|
||||
waiting; a fork head is never dispatched here (`QUEUE_TRAIN_UNTRUSTED`). By hand the same four steps are `gh workflow
|
||||
run <wf> --ref <branch>`, wait, `gstack-pregate clear --remote <lane> --run-url
|
||||
@@ -345,13 +347,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>"
|
||||
@@ -398,6 +401,28 @@ release paths (version files, CHANGELOG, fragments, `release_outputs`,
|
||||
train head and branch are S_k, and the one full gate runs on S_k, the exact
|
||||
tree the last merge publishes.
|
||||
|
||||
**One land lock.** `train land` takes the same repository land lock as
|
||||
[`land`](#land), `refs/notes/gstack-land`, as `train <nonce> slot <i> (#<pr>)`,
|
||||
before its first read of main, so a standalone land and a train slot never
|
||||
both publish onto one main: whichever starts second refuses with
|
||||
`QUEUE_LAND_LOCKED`, naming the holder. Once a slot has merged, the train keeps
|
||||
the lock in phase `between`, which only the same train's next slot takes over,
|
||||
until its last landable slot lands; a refusal before a later slot's push (a
|
||||
failed `gh` or `git fetch` read, say) keeps it too, so no standalone land slips
|
||||
in while the train waits on a rerun. Before any slot merged, a refusal before
|
||||
the push releases it. After a push, a failure keeps the lock in its phase and a
|
||||
rerun of the same slot takes it back. A stop that freezes the queue
|
||||
leaves the lock in phase `freeze`, which `--break-lock` never breaks: only the
|
||||
repair train the freeze admits takes it over, and it is released when that
|
||||
train clears the freeze. `--break-lock` prints the holder (`LOCK_HOLDER:`),
|
||||
keeps land's staleness, mid-push and drift rules, refuses a `between` lock
|
||||
younger than 10 minutes, and refuses a live train run's lock unless
|
||||
`--break-train-lock` is also given. `train watch --land` goes through `train
|
||||
land`, so it stops with `NEXT:` on `QUEUE_LAND_LOCKED` like any refusal. A train
|
||||
`train land` refuses (one built without stamped prefixes or a gate) still merges
|
||||
through `verify --train` and `gh pr merge`, which neither take nor check this
|
||||
lock.
|
||||
|
||||
Slots merge with `train land --train <nonce> --pr <n>`, never a slot call:
|
||||
|
||||
1. The record must be the gated head's own chain. Walking first parents from
|
||||
@@ -439,6 +464,209 @@ 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 (merge-tree scratch objects aside): 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, CI green at that head; a READY head whose CI is red, still
|
||||
running or absent prints a `NOT_GREEN` line and stays out of the plan). A
|
||||
READY PR whose CI, head or diff cannot be read fails the whole plan with
|
||||
`QUEUE_PROOF_MISSING`. Over the
|
||||
trigger (6) it prints `WAVE_ADVISED` and suggested groups of 3 to 6: PRs
|
||||
whose changes to a common code file conflict (`git merge-tree` cannot merge
|
||||
the two heads, or the check itself fails) always share a group, so that
|
||||
conflict is resolved once. Hub
|
||||
files never link PRs: anything under `docs/`, Markdown (generated skill
|
||||
renders included; their `.tmpl` sources still link) and every `.json` or
|
||||
`.jsonl` file (ledgers and registries). Most PRs append to them, and
|
||||
linking through them once chained a 55-PR queue into a single area
|
||||
(GST-770). 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 +796,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),
|
||||
@@ -645,8 +873,8 @@ git fetch origin main
|
||||
git merge --no-ff --no-commit origin/main # expect a CHANGELOG.md conflict only
|
||||
bun --no-env-file --config=/dev/null -e "import { mergeChangelogsWithNotes } from './lib/changelog-check'; import { spawnSync } from 'node:child_process'; import { writeFileSync } from 'node:fs'; const s = (n) => spawnSync('git', ['show', ':' + n + ':CHANGELOG.md'], { encoding: 'utf8', maxBuffer: 1 << 26, timeout: 30000 }).stdout; const m = mergeChangelogsWithNotes(s(2), s(3), s(1)); for (const n of m.notes) console.error('CHANGELOG: carried as a Gate integrity correction: ' + n); if (!m.text) { console.error('CHANGELOG needs a hand merge'); process.exit(1); } writeFileSync('CHANGELOG.md', m.text);"
|
||||
git add CHANGELOG.md && git commit --no-edit
|
||||
gstack-restamp --next --base main --pr 42 --expect-base <main sha> --expect-order <order> --allow-repo-commands
|
||||
git add CHANGELOG.md VERSION package.json <release outputs> && git commit -m "v<X> stamp" && git push
|
||||
gstack-restamp --next --base main --pr 42 --expect-base <main sha> --expect-order <order> --allow-repo-commands # stages what it writes (STAGED:)
|
||||
git commit -m "v<X> stamp" && git push
|
||||
```
|
||||
|
||||
A conflict in any file other than CHANGELOG.md means the merge isn't the train's prefix. Stop and report it. When the helper says the CHANGELOG needs a hand merge, take main's CHANGELOG unchanged and put your own entry above it. `verify` accepts the resulting head because it contains main and its tree equals T<i> modulo stamps, with its CHANGELOG judged as main's plus lines your pin added, however the conflict was resolved; the `head:` line says "T<i> with main merged in".
|
||||
@@ -659,8 +887,12 @@ introduced). A line the base added since the pin's fork point is the base's,
|
||||
whether the base merge is committed or still staged (`stage stamp_paths` says
|
||||
"N line(s) the base added since the fork point are the base's"; train 24,
|
||||
2026-10-27, moved main's `DOCS_CAPS_KEY_VERSION` in both slots before this
|
||||
rule). A file the restamp lists that the slot call did not name is not a stamp:
|
||||
leave it out and report it. The tree receipt refuses such a move after the fact
|
||||
rule). Restamp stages what it writes and prints `STAGED:`; commit what restamp
|
||||
staged. A file the slot call does not name is not a stamp: `git restore --staged
|
||||
--worktree` it and report it. A stamp file holding unstaged edits of your own refuses before
|
||||
anything is written (`RESTAMP_UNSTAGED_EDITS`), and a staging failure leaves the
|
||||
files written for the noop rerun to stage (`RESTAMP_NOT_STAGED`), so a bare
|
||||
commit after a `--no-commit` base merge can no longer drop the stamp (#3527). The tree receipt refuses such a move after the fact
|
||||
only while the moved version is older than the base's at the build, so the
|
||||
restamp, not the receipt, is what keeps the base's key in place.
|
||||
|
||||
@@ -731,7 +963,18 @@ saw, itself the prefix before modulo stamps) plus the PR's own entry under the
|
||||
same rule. One CHANGELOG rule applies before and after, so a head the pre-merge
|
||||
verify accepted confirms after its squash merge with the same CHANGELOG, and a
|
||||
stacked child whose one `[Unreleased]` became two released sections is not a
|
||||
mismatch (train 24, 2026-10-27, froze on exactly that). Anything else is
|
||||
mismatch (train 24, 2026-10-27, froze on exactly that). Before
|
||||
`MERGE_CONDITION`, the head must also carry its own stamp: its `VERSION` above
|
||||
main's, and its CHANGELOG headed with that version directly above main's top
|
||||
release, with no `[Unreleased]` section main does not have; otherwise
|
||||
`QUEUE_TRAIN_UNSTAMPED` names the rule and both versions (train 33 merged #3527
|
||||
under #3526's version without it). verify prints `STAMP_VERSION:` from the head
|
||||
itself. A post-merge confirmation needs GitHub to report the PR MERGED: an open
|
||||
PR whose stamp-only diff already matches main (a release-only PR, #3530) takes
|
||||
the pre-merge path, and any other open or unreadable PR refuses
|
||||
`QUEUE_PROOF_MISSING` without freezing, as does a verify that could not read
|
||||
the PR's current head (`--no-fetch` or a failed fetch) instead of judging the
|
||||
unstamped pin. Anything else is
|
||||
`QUEUE_TRAIN_TREE_MISMATCH` (or `QUEUE_TRAIN_MOVED`). When nothing of the train
|
||||
has merged, that is a plain refusal, and a new train is built on current main. When
|
||||
earlier slots already merged, main holds a prefix no gate covered on its own,
|
||||
@@ -774,7 +1017,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 +1026,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]`
|
||||
@@ -1014,13 +1260,13 @@ construction:
|
||||
fix-range`) or in the diff is `scope: fix`; nothing below the floor earns a
|
||||
pass. `NO FINDINGS` is the expected reviewer result for a clean range, never a
|
||||
failed review.
|
||||
3. **Two full passes plus one fix-check is the cap**, stepped by the
|
||||
[transition table](review-policy.md#transitions); a
|
||||
[scoped pass](review-policy.md#scoped-pass) counts as a full pass. A pass that applied no edits
|
||||
3. **Two rounds is the cap** (R51), stepped by the
|
||||
[transition table](review-policy.md#transitions); a full pass, a
|
||||
[scoped pass](review-policy.md#scoped-pass) and a fix-check each count as a round. A pass that applied no edits
|
||||
continues to Step 10: the loop ships with its deferred and noted items named in
|
||||
the PR body. A blocking finding still open after the last allowed step stops
|
||||
the run and names the finding (path, severity, problem) for the owner instead
|
||||
of a third full pass.
|
||||
of a third round; the owner may grant one scoped fix-check.
|
||||
|
||||
`gstack-review-scope summary --since <REVIEW_SINCE>` reads the branch's review-log
|
||||
rows for this invocation and writes the counters the ship receipt carries
|
||||
@@ -1080,7 +1326,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
|
||||
@@ -1100,7 +1348,8 @@ receipt when the base moves:
|
||||
| What changed since /ship | The receipt |
|
||||
|---|---|
|
||||
| a merge of the base, product diff unchanged (tests, release metadata, registered outputs the pregate's regen check proved) | carries; no review reruns |
|
||||
| a merge of the base whose resolution changed product code | a fresh /ship (`RECEIPT_CARRY_PRODUCT_CHANGED`) |
|
||||
| a merge of the base that edited lines next to the branch's unchanged hunks (the branch's +/- lines identical and in the same places) | carries after one fix-check reviewer reads the hunks and comes back clean (`RECEIPT_CARRY_SCOPED_REVIEW`, then `carry --scoped-review <record.json>`) |
|
||||
| a merge of the base whose resolution changed product code, or moved the branch's lines elsewhere | a fresh /ship (`RECEIPT_CARRY_PRODUCT_CHANGED`) |
|
||||
| a rebase or force-push onto a tree the receipt covers, product diff against the new base unchanged | carries (`carry.ancestry: rewritten`) |
|
||||
| any other rebase or force-push | a fresh /ship (`RECEIPT_CARRY_TREE_MISMATCH`) |
|
||||
| a stacked PR whose parent landed (squash or merge), retargeted to the base with the base merged in, HEAD's product diff exactly the one reviewed against the parent branch | carries (reason `stack parent <branch> landed`) |
|
||||
@@ -1135,13 +1384,19 @@ falling back to the whitespace-stripping default, and a `git diff` or
|
||||
patch-ids differ, the refusal names up to `CARRY_NAMED_PATHS` (20) product
|
||||
paths whose patch against the base changed, walking only the paths the branch
|
||||
itself touched at either head (a base merge brings in every path the base
|
||||
changed, none of which can differ between the two base diffs). Product means every changed path that is not test code (`test/**`,
|
||||
changed, none of which can differ between the two base diffs), and of those
|
||||
only the paths that changed between the two heads or the two bases (a path with
|
||||
the same blob on both sides has the same patch). The product diff streams
|
||||
through a file under the git directory (`.git/gstack-patch-id`) into
|
||||
`git patch-id --verbatim`, so a PR with thousands of paths is not held in
|
||||
memory; a directory a killed run left there is removed after an hour. Product means every changed path that is not test code (`test/**`,
|
||||
`spec/**`, `e2e/**`, `**/*.test.<code ext>`, `**/*.spec.<code ext>` and the
|
||||
other `CARRY_TEST_GLOBS` in `lib/ship-receipt.ts`; an `openapi.spec.yaml` is
|
||||
product) or release metadata (`CHANGELOG.md`, `agents-digest/**`, each
|
||||
`<changelog_fragments.dir>/<slug>.md` fragment when the base policy sets
|
||||
`changelog_fragments` (not the directory's README.md or a nested path; a README
|
||||
change also makes the range's fragments product), and `VERSION`
|
||||
change the branch itself makes also makes the range's fragments product, while one
|
||||
a base merge brings in does not), and `VERSION`
|
||||
or `package.json` only while the change is a version stamp and nothing else: a
|
||||
dependency, a script or a new key makes them product). A path the policy's
|
||||
`stamp_files`, `stamp_paths` or `release_outputs` name is metadata only while
|
||||
@@ -1172,11 +1427,18 @@ along the receipt's own history, `rewritten` once any hop was a rebase;
|
||||
`CARRY: carried from <old> to <new> (<reason>)` and the `GSTACK_RESULT:
|
||||
skill=ship status=complete` line, and its summary line ends with
|
||||
`carried_from=<old12>`. `CARRY: current` means the receipt already names HEAD;
|
||||
`--dry-run` prints `CARRY: would carry …` and writes nothing.
|
||||
`--dry-run` prints `CARRY: would carry …` and writes nothing. When the only
|
||||
difference is base context next to the branch's unchanged hunks and none of
|
||||
carry's other checks would refuse, `--dry-run` refuses
|
||||
`RECEIPT_CARRY_SCOPED_REVIEW` without writing the packet; carry without
|
||||
`--dry-run` writes it.
|
||||
|
||||
A changed product path, or a product patch-id that differs, is
|
||||
[`RECEIPT_CARRY_PRODUCT_CHANGED`](troubleshooting.md#receipt-carry-product-changed)
|
||||
(exit 3; the refusal names the paths), and a working tree with uncommitted
|
||||
(exit 3; the refusal names the paths), except when the only difference is
|
||||
base context next to the branch's unchanged hunks, which is
|
||||
[`RECEIPT_CARRY_SCOPED_REVIEW`](troubleshooting.md#receipt-carry-scoped-review)
|
||||
until one fix-check reviewer clears the packet; a working tree with uncommitted
|
||||
changes is [`RECEIPT_CARRY_REFUSED`](troubleshooting.md#receipt-carry-refused).
|
||||
|
||||
**Rewritten history.** `write` records `head_tree` (the reviewed head's tree)
|
||||
@@ -1202,7 +1464,12 @@ the carry when the branch has an open PR, in this order, and never reruns the
|
||||
review loop for a carried receipt:
|
||||
|
||||
1. `gstack-ship-receipt carry --pr <n> --base <base> --dry-run`. `CARRY: would
|
||||
carry` is eligibility only; a refusal continues the full ship.
|
||||
carry` is eligibility only; a refusal continues the full ship, except
|
||||
`RECEIPT_CARRY_SCOPED_REVIEW`, for which `gstack-ship-preflight` prints
|
||||
`NEXT: carry route`: fix what the base merge broke, commit and push, run carry
|
||||
without `--dry-run` to write the packet, have one fix-check reviewer read it,
|
||||
then carry with `--scoped-review <record.json>` and `--pregate`. A blocking
|
||||
finding continues the full ship.
|
||||
2. At that pinned HEAD: the CHANGELOG check, the size-cap check the policy
|
||||
declares, and `gstack-pregate` (the remote tests stage when
|
||||
`requires_remote` asks for it). A failing check, or HEAD moving, continues
|
||||
@@ -1399,9 +1666,12 @@ command:
|
||||
2. **Lock.** Take the repository land lock, `refs/notes/gstack-land` (its own
|
||||
ref, never the trains notes ref), by exact lease. It records the PR, holder,
|
||||
phase and journal. A held lock is `QUEUE_LAND_LOCKED` naming the holder;
|
||||
`--break-lock` takes over a crashed holder's lock and is refused while that
|
||||
holder entered a push or a merge less than 10 minutes ago, and for a drift
|
||||
lock. A refusal before the push releases the lock.
|
||||
`--break-lock` prints the holder (`LOCK_HOLDER:`), takes over a crashed
|
||||
holder's lock and is refused while that holder entered a push or a merge
|
||||
less than 10 minutes ago, and for a drift lock. `train land` shares this
|
||||
lock ([one land lock](#train-land)); a train slot's lock needs
|
||||
`--break-train-lock` as well while its train still lands, and a `freeze`
|
||||
lock is never broken. A refusal before the push releases the lock.
|
||||
3. **Merge and stamp.** In a scratch worktree, merge the base into the pin.
|
||||
Conflicts in release fields only (the version file and `release_outputs`
|
||||
take the base's side, the CHANGELOG merges by section, a manifest or npm
|
||||
@@ -1492,7 +1762,9 @@ hand edit), and the replacement the current one, so a dependency pin
|
||||
that happens to equal the old version is `changed`; `stamp_patterns`
|
||||
templates count as stamp forms too). With `stamp_files` set, a file outside it
|
||||
is `changed (stamp_files: not in the repo's stamp_files allowlist)` whatever
|
||||
its diff. Any other byte is a change.
|
||||
its diff. Any other byte is a change. A diff git cannot produce between the two
|
||||
heads fails closed with `EVIDENCE_IDENTITY_UNKNOWN` and git's reason; it never
|
||||
reads as no change.
|
||||
|
||||
Equivalence is decided per gate. A lane in `stamp_sensitive_gates` always
|
||||
`rerun`s; a verified lane is `reusable` only when the tree is same-modulo-stamps
|
||||
|
||||
+12
-5
@@ -962,8 +962,15 @@ blank (`PREREG_INCOMPLETE`). `arms` resolves the `eval-arms` model role against
|
||||
`lib/pricing.ts` and stops on an unpriced model (`EVAL_PRICE_MISSING`); `dry-run`
|
||||
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.
|
||||
items, records actual spend, extrapolates each priced arm to the full set against
|
||||
the cap (`EVAL_BUDGET_EXHAUSTED`) and prints the spend table the approval question
|
||||
shows. An arm on a subscription with no extra charge is declared with
|
||||
`pilot start --seat-metered <arm,...>`: it reserves $0, settles as `unknown`, and
|
||||
stays out of the projection until one of its attempts settles a real charge.
|
||||
A run stops at the first quota or rate-limit error on such an arm.
|
||||
`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.
|
||||
@@ -999,8 +1006,8 @@ VM. `/ubicloud` runs it there through one entry point, `bin/gstack-ubi`
|
||||
(`scripts/ubicloud/ubi-runner.sh`), with the discipline a shared quota needs:
|
||||
every VM is named `ubirun-<owner>-<epoch>-<suffix>-t<hours>`, so its owner
|
||||
(`UBI_OWNER`, required to create one) and TTL (`UBI_TTL_HOURS`, default 3) are
|
||||
visible to everyone; every `up` first reaps VMs whose TTL ended, whichever owner
|
||||
made them, and its own owner's VMs with no TTL once they are 4 hours old; every run
|
||||
visible to everyone; every `up` first reaps its own owner's VMs whose TTL ended,
|
||||
and those with no TTL once they are 4 hours old, never another owner's; every run
|
||||
ends with `UBICLOUD: left=0` or fails; the
|
||||
quota preflight refuses before the create request when the project's running
|
||||
VMs plus the new size exceed the known ceiling (`UBI_QUOTA_VCPUS`, or the
|
||||
@@ -1023,7 +1030,7 @@ Claude: UBICLOUD_API_KEY: set; UBI_OWNER=lane7
|
||||
gstack-ubi verify (.gstack/ubicloud.json: bootstrap.sh, "bash scripts/verify.sh", standard-16)
|
||||
ubi-runner: reap: no expired VMs
|
||||
ubi-runner: creating eu-central-h1/ubirun-lane7-1761500000-9f3a2c10-t3 (standard-16)
|
||||
ubi-runner: ubirun-lane7-1761500000-9f3a2c10-t3 carries a 3h TTL (any later run of this script reaps it); if this process dies first: gstack-ubi down ubirun-lane7-1761500000-9f3a2c10-t3
|
||||
ubi-runner: ubirun-lane7-1761500000-9f3a2c10-t3 carries a 3h TTL (this owner's next up or reap destroys it once it expires); if this process dies first: gstack-ubi down ubirun-lane7-1761500000-9f3a2c10-t3
|
||||
ubi-runner: heartbeat: command 300s elapsed, last output 2s ago
|
||||
ubi-runner: command exited 0 after 1140s
|
||||
ubi-runner: destroyed eu-central-h1/ubirun-lane7-1761500000-9f3a2c10-t3
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Support tiers
|
||||
|
||||
gstack has two support tiers. Full support gets the whole test suite, paid evals and new work. Minimal support means gstack is not totally broken, and nothing more. Garry Tan set this on 2026-11-01: "we don't want it to be totally broken, but we just don't need to invest that much into it."
|
||||
|
||||
## Full support
|
||||
|
||||
- **Operating systems:** macOS and Linux.
|
||||
- **Models:** Claude and OpenAI models, both as the host agent and as a reviewer.
|
||||
- **Testing:** the full free suite and the paid evals.
|
||||
|
||||
## Minimal support
|
||||
|
||||
- **Windows** (Git Bash or MSYS): with a default Git for Windows install, `./setup` installs, and the core skills start without errors. The core skills are `/ship`, `/review`, `/qa-only`, `/investigate` and `/browse` on gstack's own browser. Once the `windows-smoke` CI job lands, it checks this on every pull request (see Windows CI below).
|
||||
- **Gemini models:** the core workflows run to the end without errors when Gemini is the host agent or a reviewer. We check this by hand or with one cheap smoke run, not with an eval cell.
|
||||
|
||||
That is the whole promise. Other skills, optional parts such as `/cso`'s native helper, non-default shell settings and speed are not covered.
|
||||
|
||||
## What we stop doing
|
||||
|
||||
- No Windows-only or Gemini-only features, parity work, tuning or polish.
|
||||
- No new Windows or Gemini CI jobs or eval cells. Windows CI tuning stops.
|
||||
- A Windows-only or Gemini-only report that doesn't break install, startup or a core workflow closes as won't fix ([triage rules](triage.md#closing-an-issue)), with a link to this page. A report that does break one of them gets a small fix.
|
||||
|
||||
## Windows CI
|
||||
|
||||
- **Today:** the six-shard Windows free suite (the `Windows Free Tests` workflow, `windows-free-tests`) runs on every pull request and every push to main, and it is a required check.
|
||||
- **Once `windows-smoke` lands:** `windows-smoke` becomes the required Windows check on pull requests. The six-shard suite then runs nightly on main, or when someone starts it by hand, and never blocks a pull request. A red nightly run gets a fix only when it shows an install, startup or core-workflow break.
|
||||
|
||||
## What doesn't change
|
||||
|
||||
These tiers don't change which models gstack's own evals test, or which cross-family review voice gstack uses.
|
||||
+626
-34
@@ -314,6 +314,20 @@ network.
|
||||
|
||||
**Expected result.** `OUTSIDE_STATUS: completed`.
|
||||
|
||||
<a id="outside-review-truncated"></a>
|
||||
### `... outside review unavailable: the reviewer hit its output token limit and the response is cut off (...)`
|
||||
|
||||
**Meaning.** The api runner's provider stopped at the output token limit
|
||||
(Anthropic `stop_reason: max_tokens`, OpenAI `finish_reason: length`), so the
|
||||
review ends mid-sentence and its findings may be missing.
|
||||
|
||||
**Fix.** Re-run with a larger `--max-tokens` (`gstack-outside-voice run` or
|
||||
`gstack-autoplan run`), or shorten the prompt. `gstack-autoplan run` sends a
|
||||
default of 16384 to both providers, and OpenAI counts reasoning tokens toward
|
||||
it; `gstack-outside-voice run` sends that default to Anthropic only.
|
||||
|
||||
**Expected result.** `OUTSIDE_STATUS: completed`.
|
||||
|
||||
<a id="outside-review-missing-markers"></a>
|
||||
### `... outside review unavailable: the response lacks the markers this gate requires (...)`
|
||||
|
||||
@@ -550,6 +564,25 @@ local `origin/<base>` at `<rev>`.
|
||||
**Fix.** Run `git fetch origin <base>` outside the sandbox and rerun /review if
|
||||
the base has moved.
|
||||
|
||||
<a id="review-content-changed"></a>
|
||||
### `review_freshness: STALE content changed during review: the tree moved from <start> to <end> while <skill> ran ...` / **Review records missing or mismatched**
|
||||
|
||||
**Meaning.** The working tree changed between the pass's `gstack-review-log --start` and
|
||||
its logged row: an edit, commit or merge landed while reviewers were still reading. The
|
||||
reviewers may not have seen that change, so the row's `review_binding.state` is `changed`.
|
||||
Step 11.5 cannot bind it and inserts `9 → 10 → 11 → 11.5`. Logging the same results again
|
||||
cannot repair it.
|
||||
|
||||
**Fix.** Keep the checkout frozen (no edits, commits, merges or pushes) from a pass's start
|
||||
until every reviewer in it has returned and its row is logged. If the mid-pass change was
|
||||
your own fix to that pass's findings, log the pass with `"converged":false` and the fix
|
||||
as `"action":"fixed"`. `gstack-review-plan` then picks the repeat: one fix-check reading
|
||||
the touched hunks when every fix is below the blocking floor, and 11.5 accepts that
|
||||
fix-check record as Step 9's. Rerun Step 11's native pass on the same frozen tree.
|
||||
|
||||
**Expected result.** `gstack-review-read` shows both records at one tree with
|
||||
`review_freshness` `CURRENT`, and Step 11.5 continues to Step 12.
|
||||
|
||||
<a id="review-fingerprint-tmpdir"></a>
|
||||
### `gstack-wtree: cannot create a temp index under <dir>; set TMPDIR to a writable directory` / `gstack-review-log: cannot create a private object directory under <dir>; no working-tree fingerprint. Set TMPDIR to a writable directory.`
|
||||
|
||||
@@ -755,13 +788,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.`
|
||||
@@ -793,7 +880,11 @@ pick a new `--output`), then copy the recovery file to where you want it.
|
||||
**Meaning.** On Codex, Factory, OpenCode, Cursor, Copilot and Kiro every bash
|
||||
block finds gstack on its own: an exported `GSTACK_ROOT` (with `bin/` and
|
||||
`lib/`), then a repo-local install, then the host's global install
|
||||
(`$CODEX_HOME/skills/gstack` on Codex). None existed.
|
||||
(`$CODEX_HOME/skills/gstack` on Codex). None existed. A skill installed for one
|
||||
folder (Hermes, or the Codex install `./setup` writes) is the exception for its
|
||||
first block: that block runs the folder's own `bin/gstack-skill-start` and
|
||||
ignores an exported `GSTACK_ROOT`. When the folder is missing, the block prints
|
||||
this line and its start command then fails.
|
||||
|
||||
**Fix.**
|
||||
|
||||
@@ -1109,6 +1200,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>`
|
||||
|
||||
@@ -1163,8 +1276,8 @@ git -C ~/.claude/skills/gstack update-ref -d 'refs/gstack/rescue/<oid>'
|
||||
<a id="setup-runtime-root-contains-checkout"></a>
|
||||
### `gstack setup: refusing to replace <root> for <host>: this runtime directory contains the source checkout setup runs from (<checkout>), so replacing it would delete the checkout. Nothing was changed.`
|
||||
|
||||
**Meaning.** Setup installs Codex, Factory, OpenCode, Cursor, Copilot and
|
||||
Antigravity by building a runtime root (such as `~/.cursor/skills/gstack`) and
|
||||
**Meaning.** Setup installs Codex, Factory, OpenCode, Cursor, Copilot,
|
||||
Antigravity and Gemini CLI by building a runtime root (such as `~/.cursor/skills/gstack`) and
|
||||
swapping it in for the old one, and Kiro by writing into `~/.kiro/skills/gstack`.
|
||||
Your gstack checkout lives at that root or inside it, so the swap would delete
|
||||
the checkout (#3084), and Kiro would link the checkout into itself. Setup refuses before
|
||||
@@ -1551,6 +1664,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
|
||||
@@ -1864,6 +1979,18 @@ owner's call: run the printed `RECOVER:` line; its resume needs
|
||||
**Fix.** In the execute program that runs the command, read
|
||||
`(await tools.thread.get({ thread_id: "<your own jam id>" })).cost.tokens` and pass it as `--reading <n>`.
|
||||
|
||||
<a id="meter-scope-parent"></a>
|
||||
### `gstack-ship-budget: ... (METER_SCOPE_PARENT)`
|
||||
|
||||
**Meaning.** `start` was told the run is a subagent (`--subagent` or `GSTACK_SHIP_SUBAGENT=1`) reading
|
||||
`"this"` (`--reading-from this`), the root thread's counter, which also holds every sibling lane, and
|
||||
asked for `--meter-scope self`. A self scope would print that counter as the run's own cost. Exit 2;
|
||||
no run started.
|
||||
|
||||
**Fix.** Start with `--meter-scope tree`. Every cost line then prints
|
||||
`METER: tree (root counter, upper bound)`. Exact per-run cost comes from a root replay thread
|
||||
(`docs/ship-budget.md`).
|
||||
|
||||
<a id="budget-reading-rejected"></a>
|
||||
### `gstack-ship-budget: ... (BUDGET_READING_REJECTED)`
|
||||
|
||||
@@ -2128,6 +2255,25 @@ anything.
|
||||
|
||||
---
|
||||
|
||||
<a id="setup-deps-migrate-failed"></a>
|
||||
### `gstack: WARN kept the full dependency install (SETUP_DEPS_MIGRATE_FAILED; ...): <cause>`
|
||||
|
||||
**Meaning.** Your install was made before v1.91.231.0, so it still carries the
|
||||
test and eval packages (about 520 MB, mostly the Claude Agent SDK). setup moved
|
||||
`node_modules` aside to install production dependencies only, that install
|
||||
failed, and setup put the full `node_modules` back. Nothing was removed, and
|
||||
gstack keeps working on the full install.
|
||||
|
||||
**Fix.** Fix the cause the warning names (no network, a proxy, a full disk),
|
||||
then run `./setup` again. Developing gstack in this checkout? Mark it, and
|
||||
setup keeps the dev packages from then on:
|
||||
|
||||
```bash
|
||||
git config gstack.contributor true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="chromium-install-exit-127"></a>
|
||||
### `Chromium bootstrap: chromium-install — <bun> x playwright install chromium exited 127 (command not found); install Bun from bun.sh (winget Bun ships no bunx), or skip with ./setup --no-browser (Git Bash) or .\setup.ps1 --no-browser (PowerShell)`
|
||||
|
||||
@@ -2818,6 +2964,34 @@ fetching it cannot create that ref, so pass a branch that exists on `origin`
|
||||
(`git branch -r --list 'origin/*'` lists them). `gstack-pregate` says so in
|
||||
its fix instead of suggesting the fetch.
|
||||
|
||||
<a id="history-read-failed"></a>
|
||||
### `cannot read <path> at <ref>. <cause> (git version <v>). Fix: see docs/troubleshooting.md#history-read-failed (HISTORY_READ_FAILED)`
|
||||
|
||||
**Meaning.** A CI job or a local release tool read a file at an older commit
|
||||
and git could not produce it. The PR and train path jobs (Free Tests' plan, the Version Gate,
|
||||
and E2E Evals' route, screen summary, planner, slices, reruns, bisect and
|
||||
report) check out blobless clones (`filter: blob:none`): every commit and
|
||||
tree is there, but an older file's contents are fetched from GitHub the first
|
||||
time something reads them. When that fetch fails three times (a GitHub outage,
|
||||
a network drop), or the ref does not resolve at all, the read fails here
|
||||
instead of counting as "the file is absent". A file the commit really does not
|
||||
have still reads as absent, as before. No ship policy at the base means no
|
||||
train, and no VERSION means `0.0.0.0`. The route and backstop policy reads (inline
|
||||
in their workflows), `.github/scripts/history-read.sh` (the Version Gate's base
|
||||
`VERSION`) and `lib/history-read.ts` (the Version Gate's train classification)
|
||||
print this line. The Version Gate prints the same code when
|
||||
`git fetch origin <base>` fails three times. `gstack-restamp` and
|
||||
`gstack-ship-queue land` print it too when they cannot read the base branch's
|
||||
CHANGELOG, which they check fragments against (`lib/changelog-fragments.ts`
|
||||
reads it through `lib/history-read.ts`); outside a partial clone they do not
|
||||
retry.
|
||||
|
||||
**Fix.** Rerun the failed job once GitHub is reachable. If it fails the same
|
||||
way on a healthy network, check that the ref exists on `origin` and that the
|
||||
runner's git supports partial clone (`git --version` 2.22 or newer; the eval
|
||||
image asserts it at build time in `.github/docker/Dockerfile.ci`). For a local
|
||||
tool, run `git fetch origin <base>` and rerun it.
|
||||
|
||||
<a id="policy-invalid"></a>
|
||||
### `POLICY_INVALID`
|
||||
|
||||
@@ -3003,9 +3177,14 @@ or pass `--force-gate <reason>` in `/ship` to run the full gate again.
|
||||
**Meaning.** The bundle (or ledger record) lacks a committed tree id, runtime
|
||||
versions, lockfile hashes or the command hash, so no reuse decision can rest on
|
||||
it. Imported assertions without verification count as unknown too.
|
||||
`gstack-tree-receipt` also fails with this code when git cannot produce the
|
||||
diff between the gated and current heads (a missing object, a timeout): the
|
||||
error names git's reason, and a failed diff is never read as no change.
|
||||
|
||||
**Fix.** Rebuild the bundle on the gate machine after the lanes ran on a
|
||||
committed head: `gstack-evidence bundle --out <file> --label <lane>...`.
|
||||
committed head: `gstack-evidence bundle --out <file> --label <lane>...`. For
|
||||
the tree receipt, fix what git's reason names (fetch a missing head with
|
||||
`git fetch origin <sha>`), then rerun.
|
||||
|
||||
<a id="evidence-bundle-invalid"></a>
|
||||
### `EVIDENCE_BUNDLE_INVALID`
|
||||
@@ -3102,6 +3281,42 @@ result.
|
||||
**Fix.** Run `gstack-autoplan status --out <dir>` and submit for the attempt
|
||||
the last `next` printed (`ATTEMPT: ... phase=<p> voice=<v>`).
|
||||
|
||||
<a id="ceo-only-conflict"></a>
|
||||
### `CEO_ONLY_CONFLICT`
|
||||
|
||||
**Meaning.** `gstack-autoplan next --ceo-only` was combined with `--ui`,
|
||||
`--developer-tool` or `--agent-primary` (on the command line or in the plan's
|
||||
`gstack-autoplan-scope` marker), each of which adds a phase a CEO-only run
|
||||
never opens; or the marker names `--ceo-only` (the mode is a command-line
|
||||
choice for one run, so a plan can't pin it); or the plan is `<out>/ceo-review.md`
|
||||
(directly, through a symlink or a hard link), the report the run would write
|
||||
over it. The finish checks that last case again before it writes the report.
|
||||
|
||||
**Fix.** Drop the conflicting flag (or the marker token), move the plan out of
|
||||
the report path, or run the full review without `--ceo-only`.
|
||||
|
||||
<a id="ceo-only-after-init"></a>
|
||||
### `CEO_ONLY_AFTER_INIT`
|
||||
|
||||
**Meaning.** `next --ceo-only` named a run directory that already holds a full
|
||||
review run. A run keeps the mode it started with.
|
||||
|
||||
**Fix.** Start the CEO-only run in a new `--out` directory, passing
|
||||
`--ceo-only` on its first `next`.
|
||||
|
||||
<a id="ceo-only-no-gate"></a>
|
||||
### `CEO_ONLY_NO_GATE`
|
||||
|
||||
**Meaning.** `answer`, `recommend` or `revise` was run on a CEO-only run, or
|
||||
`gstack-approvals import` was pointed at one. A CEO-only
|
||||
run writes no gate, ends `status=reviewed` and is never approved, so there is
|
||||
nothing to answer, change or import. (`budget` and `close --deferred` still
|
||||
work: they are how a budget-stopped CEO-only run recovers.)
|
||||
|
||||
**Fix.** Read `<run dir>/ceo-review.md`. For a gate and an approval, start a
|
||||
full review in a new directory: `gstack-autoplan next --out <new dir> --plan
|
||||
<file>` without `--ceo-only`.
|
||||
|
||||
<a id="attempt-mismatch"></a>
|
||||
### `ATTEMPT_MISMATCH`
|
||||
|
||||
@@ -3229,6 +3444,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 +3556,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
|
||||
@@ -3387,9 +3651,12 @@ never runs them: declare `"**/integration_test/**": []` (and
|
||||
them. A `[]` declaration added only on the branch is refused under
|
||||
`--policy-from head` ([`PREGATE_HEAD_POLICY_LOOSENS`](#pregate-head-policy-loosens)),
|
||||
so land it in its own PR first.
|
||||
With no policy anywhere (no base copy, no user-owned file) an unmapped file is
|
||||
With no policy anywhere (no base copy, no user-owned file), or a policy with no
|
||||
`dependencies`, `empty_lanes`, `selection` or `local` key (a timeout-only file), an unmapped file is
|
||||
the warning [`PREGATE_NO_POLICY`](#pregate-no-policy) with a starter policy
|
||||
printed, not this error; `gstack-pregate init` writes that draft and lists the
|
||||
printed, not this error. Under `warn: ["lanes"]` it is a `warn` with this code,
|
||||
given only after the selected tests ran green (a red lane is still
|
||||
`PREGATE_LANE_FAILED`); `gstack-pregate init` writes that draft and lists the
|
||||
files it could not place. `--explain` prints every mapping and why. For a file with no lane it also prints the import
|
||||
specifiers in test files that resolved to no repo file (for example a
|
||||
`tsconfig.json` `paths` alias such as `@/lib/x` whose target is missing) and any
|
||||
@@ -3490,6 +3757,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,
|
||||
@@ -3521,8 +3794,9 @@ differing path is a change.
|
||||
### `PREGATE_NO_POLICY`
|
||||
|
||||
**Meaning.** The repository has no pregate policy yet (no `.gstack/pregate.json`
|
||||
on the base branch and no user-owned `<state root>/projects/<slug>/pregate.json`)
|
||||
and a touched file maps to no test lane. Under no policy that is a `warn`, not
|
||||
on the base branch and no user-owned `<state root>/projects/<slug>/pregate.json`),
|
||||
or its policy declares no lanes (no `dependencies`, `empty_lanes`, `selection` or `local` key, a
|
||||
timeout-only file for example), and a touched file maps to no test lane. Under no policy that is a `warn`, not
|
||||
`PREGATE_NO_LANE`: the selected tests still ran (a red lane is still `fail`),
|
||||
and the table printed a starter policy drafted from the repository
|
||||
(`PREGATE_STARTER:` lines: runner, test directories, CI lanes, the JSON between
|
||||
@@ -3533,7 +3807,8 @@ Details: [docs/pregate.md](pregate.md#no-policy).
|
||||
overwrite an existing file without `--force`), edit each `TODO` (a test that
|
||||
names the file, a `dependencies` entry, or `[]` for a file with no behaviour),
|
||||
run `gstack-pregate validate`, then commit `.gstack/pregate.json` on the base
|
||||
branch. From then on an unmapped file is `PREGATE_NO_LANE`.
|
||||
branch. When a policy already exists, add the starter's `dependencies` to it
|
||||
instead. From then on an unmapped file is `PREGATE_NO_LANE`.
|
||||
|
||||
<a id="pregate-environment"></a>
|
||||
### `PREGATE_ENVIRONMENT`
|
||||
@@ -3585,7 +3860,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 +3870,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 +3953,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`
|
||||
|
||||
@@ -4348,7 +4644,10 @@ train still building, running or merging, `train watch` on an unknown record,
|
||||
unreadable run), `verify --pr <n> --train <nonce>` when main's tree mismatches
|
||||
and whether a slot merged on GitHub cannot be read (so whether the queue must
|
||||
freeze is unknown; it never answers "nothing merged" from an unreadable
|
||||
state), `train land` when the base or the train branch cannot be fetched or
|
||||
state), `verify --pr <n> --train <nonce>` when GitHub does not report the PR
|
||||
MERGED and it is not a release-only PR whose stamp-only diff already matches
|
||||
main, or when it could not read the PR's current head (`--no-fetch` or a failed
|
||||
fetch), without freezing the queue, `train land` when the base or the train branch cannot be fetched or
|
||||
the record's T0 and S_1..S_k are not the gated head's first-parent chain
|
||||
(each S_i's second parent slot i's pin), `train land` when main's tree is the
|
||||
slot's S_i but GitHub does not report the PR as MERGED (a record whose slot
|
||||
@@ -4454,14 +4753,60 @@ the name already landed. Then rerun.
|
||||
<a id="queue-land-locked"></a>
|
||||
### `QUEUE_LAND_LOCKED`
|
||||
|
||||
**Meaning.** Another `gstack-ship-queue land` holds the repository land lock
|
||||
(`refs/notes/gstack-land`), or took it from under this run, so this one would
|
||||
race it for the next version. The line names the holder, its PR and phase.
|
||||
**Meaning.** Another `gstack-ship-queue land`, or a `train land` slot, holds the
|
||||
repository land lock (`refs/notes/gstack-land`), or took it from under this run,
|
||||
so this one would race it onto main. The line names the holder: `#<pr>` for a
|
||||
land, `train <nonce> slot <i> (#<pr>)` for a train, with its holder and phase. A
|
||||
train keeps the lock in phase `between` from one merged slot to the next, so no
|
||||
standalone land slips in between slots.
|
||||
|
||||
**Fix.** Wait for the holder to finish. If it crashed, rerun with
|
||||
`--break-lock`; that is refused while the holder entered a push or a merge
|
||||
less than 10 minutes ago. A lock in phase `drift` is released by landing its
|
||||
repair PR ([QUEUE_LAND_DRIFT](#queue-land-drift)).
|
||||
`--break-lock`, which prints the holder (`LOCK_HOLDER:`). That is refused while
|
||||
the holder entered a push or a merge less than 10 minutes ago, while a train is
|
||||
between slots for less than 10 minutes, and for a live train run unless you also
|
||||
pass `--break-train-lock`. A lock in phase `drift` is released by landing its
|
||||
repair PR ([QUEUE_LAND_DRIFT](#queue-land-drift)); a lock in phase `freeze` is
|
||||
released by the verify that clears the freeze
|
||||
([QUEUE_TRAIN_FROZEN](#queue-train-frozen)). If that release push failed, the
|
||||
next `land` or `train land` takes the lock over once the freeze record it fetches
|
||||
shows the freeze cleared after the lock was written; `--break-lock` never does.
|
||||
|
||||
<a id="queue-train-unstamped"></a>
|
||||
### `QUEUE_TRAIN_UNSTAMPED`
|
||||
|
||||
**Meaning.** `verify --pr <n> --train <nonce>` found the slot head it would let
|
||||
merge without its own stamp: the head's `VERSION` is not above main's, or its
|
||||
CHANGELOG's top section is not headed with that version directly above main's
|
||||
top release (or keeps an `[Unreleased]` section main does not have). The line
|
||||
names the rule, the head's version and main's. Train 33 merged #3527 this way
|
||||
(a no-commit base merge, a restamp whose files were never staged, a bare
|
||||
commit) and shipped it under #3526's version. Nothing was merged.
|
||||
|
||||
**Fix.** On the lane, run `gstack-restamp --next` on the head, commit what
|
||||
restamp staged, push, and rerun the verify.
|
||||
|
||||
<a id="restamp-not-staged"></a>
|
||||
### `RESTAMP_NOT_STAGED`
|
||||
|
||||
**Meaning.** `gstack-restamp` wrote the stamp but `git add` failed, so a bare
|
||||
`git commit` would leave the stamp out. The files stay written, and
|
||||
`.gstack/tmp/restamp-staging.json` records what is still to stage.
|
||||
|
||||
**Fix.** Clear what blocked `git add` (a stale `.git/index.lock`, for
|
||||
example) and rerun `gstack-restamp`; the rerun is a noop that stages the files
|
||||
it wrote. When any of them changed since restamp wrote it (you committed by
|
||||
hand, switched branches or merged the base), the marker is stale: the noop
|
||||
rerun drops it and stages nothing, and a stamp still owed is restamped as usual.
|
||||
|
||||
<a id="restamp-unstaged-edits"></a>
|
||||
### `RESTAMP_UNSTAGED_EDITS`
|
||||
|
||||
**Meaning.** A file `gstack-restamp` would stamp holds unstaged edits of yours.
|
||||
Restamp stages what it writes, so those edits would ride along in the stamp
|
||||
commit. Nothing was written.
|
||||
|
||||
**Fix.** Stage (`git add`) or stash your own edits to the named files, then
|
||||
rerun `gstack-restamp`.
|
||||
|
||||
<a id="queue-land-conflict"></a>
|
||||
### `QUEUE_LAND_CONFLICT`
|
||||
@@ -4563,6 +4908,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 +4934,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 +4956,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 +4970,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 +4984,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 +4997,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 +5070,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 +5171,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 +5297,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 +5491,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
|
||||
@@ -5120,7 +5657,9 @@ compare (a receipt written before `head_tree`, whose head is not in this
|
||||
clone), the PR was retargeted to the base after its stack parent landed but
|
||||
the receipt records no `product_patch_id` and `origin/<parent>` no longer
|
||||
resolves, so the reviewed range cannot be recomputed, or git could not
|
||||
compare the two product diffs or read the working tree's status. It also refuses when an owner exception the receipt records
|
||||
compare the two product diffs (including when carry cannot create its temp
|
||||
directory under `.git/gstack-patch-id`; the message names it) or read the
|
||||
working tree's status. It also refuses when an owner exception the receipt records
|
||||
no longer stands, and, for a receipt from a gstack that records review
|
||||
depth, when HEAD needs a deeper review than the receipt records, `--head` is
|
||||
not the checked-out HEAD, or the receipt records `gstack_version` without
|
||||
@@ -5183,11 +5722,41 @@ retargeted stacked PR the parent branch and the base.
|
||||
**Fix.** Run /ship on the new head. The receipt carries only across a
|
||||
conflict-only base merge, a test-only change, release metadata, a registered
|
||||
output the pregate's regen check proved, a rebase onto a covered tree with
|
||||
the recorded product diff, or a stacked PR's retarget once its parent fully
|
||||
landed on the base, which the
|
||||
the recorded product diff, a stacked PR's retarget once its parent fully
|
||||
landed on the base, or base context next to unchanged hunks after a clean
|
||||
scoped review ([`RECEIPT_CARRY_SCOPED_REVIEW`](#receipt-carry-scoped-review)), which the
|
||||
`carry` object in the carried receipt records with the product patch-id and
|
||||
the range-diff evidence ([unattended.md](unattended.md#the-terminal-line)).
|
||||
|
||||
<a id="receipt-carry-scoped-review"></a>
|
||||
### `RECEIPT_CARRY_SCOPED_REVIEW`
|
||||
|
||||
**Meaning.** Every product path the branch touches has the reviewed +/- lines
|
||||
in the same places, but the base edited context lines next to some of them, so
|
||||
the 3-line patches differ (GST-384: main reworded the line above the PR's
|
||||
hunk). The merge may have spliced the two, as in a JSON entry where the branch
|
||||
changed one key and the base the next, so the receipt does not carry silently.
|
||||
The refusal names the paths and a packet file with the hunks that differ and
|
||||
the base's own change. The same lines moved somewhere else are
|
||||
`RECEIPT_CARRY_PRODUCT_CHANGED`, not this code.
|
||||
|
||||
**Fix.** Dispatch one fix-check reviewer
|
||||
([review-policy.md](review-policy.md)) on the packet. When it comes back clean,
|
||||
write `{"head":"<HEAD>","paths":[...],"reviewer":"<model>","verdict":"clean"}`
|
||||
and rerun `gstack-ship-receipt carry ... --scoped-review <record.json>`; the
|
||||
carry reason records the reviewer and the paths. A blocking finding means /ship
|
||||
on the new head. `gstack-ship-release finish` stops the same way with
|
||||
`context=N` and takes `--context-reviewed <record.json>` bound to the tree;
|
||||
write that record beside the packet, outside the worktree, so it does not change
|
||||
the tree it is bound to. Finish also stops this way for test code changed since
|
||||
review with no gate edit (`tests=N`): the packet holds those test hunks, and the
|
||||
reviewer checks that each test still tests what it did. A packet that cannot be
|
||||
written (an unwritable `.git`) is a STOP or `RECEIPT_CARRY_REFUSED` naming the path.
|
||||
`carry --dry-run` reports this code without writing the packet, and only when
|
||||
none of carry's other checks would refuse; carry without `--dry-run` writes it.
|
||||
In /ship Step 1, `gstack-ship-preflight` prints the route as `NEXT: carry
|
||||
route (Step 1.4)`.
|
||||
|
||||
<a id="receipt-prose-mismatch"></a>
|
||||
### `RECEIPT_PROSE_MISMATCH`
|
||||
|
||||
@@ -5242,7 +5811,10 @@ over` line names (or raise its cap on the base branch). When the owner accepts
|
||||
the named risk instead, record it as `docs.exception: { accepted_by, date, via,
|
||||
reason }` and carry the `Documentation exception: accepted by <owner> <date> via
|
||||
<ref>: <reason>` line in the PR body's `## Documentation` section, word for word
|
||||
([ship-policy.md](ship-policy.md#ship-receipt)).
|
||||
([ship-policy.md](ship-policy.md#ship-receipt)). Then save the post-audit snapshot with
|
||||
`gstack-docs-candidate snapshot --exception .gstack/tmp/docs-audit.json`;
|
||||
`gstack-ship-release finish` stops on a blocked audit whose snapshot lacks the
|
||||
exception.
|
||||
<a id="gstack-pr-wait"></a>
|
||||
## gstack-pr-wait
|
||||
|
||||
@@ -5600,6 +6172,26 @@ The router's description (at most 300 bytes) and the context block (at most 600
|
||||
|
||||
There is no ledger for that harness or id under `$GSTACK_STATE_ROOT/harness`. `gstack-harness status` lists every registration on this machine with its id.
|
||||
|
||||
<a id="harness-hook-unsupported"></a>
|
||||
### `gstack-harness: <harness> has no start hook` / `the start hook goes into the <harness> profile <dir>, and this registration uses another skills folder` (HARNESS_HOOK_UNSUPPORTED)
|
||||
|
||||
The [start hook](hosts.md#start-hook) exists for OpenClaw and Hermes only, and it goes into the harness profile's own plugin folder, the one the harness scans. A registration with `--skills-dir` somewhere else has no profile gstack can trust, and with `OPENCLAW_CONFIG_PATH` set OpenClaw reads a config outside the profile, where gstack's settings change would not land, so `--start-hook` is refused and nothing is written. Register without `--skills-dir` (set `OPENCLAW_STATE_DIR` or `HERMES_HOME` to pick the profile), or drop `--start-hook`. When the choice comes from `GSTACK_HARNESS_START_HOOK` or `harness_start_hook` instead of the flag, such a registration just gets no hook, and its receipt says so.
|
||||
|
||||
<a id="harness-hook-choice-invalid"></a>
|
||||
### `gstack-harness: GSTACK_HARNESS_START_HOOK="<value>" is not on or off` (HARNESS_HOOK_CHOICE_INVALID)
|
||||
|
||||
The start hook switch takes `on` or `off` (`GSTACK_HARNESS_START_HOOK` also takes `1` or `0`). Nothing was registered. Fix the value, or unset it so each registration keeps its saved choice.
|
||||
|
||||
<a id="harness-hook-owned-elsewhere"></a>
|
||||
### `gstack-harness: a gstack-start plugin already exists at <dir> and this registration did not write it` (HARNESS_HOOK_OWNED_ELSEWHERE)
|
||||
|
||||
Each harness profile has one start hook, owned by the gstack registration that wrote it, so that removing one install can never take another install's hook away. This folder is not in this registration's ledger: another gstack checkout or state root wrote it, or someone made it by hand. Nothing was written. Run `gstack-harness register <harness> --no-start-hook` from the gstack install that wrote it (its `gstack-harness status` shows the hook), or move the folder aside, then register again.
|
||||
|
||||
<a id="harness-hook-inactive"></a>
|
||||
### `gstack-harness: <harness> start hook is written: <why>. Turn it on: <command>` (HARNESS_HOOK_INACTIVE, exit 4)
|
||||
|
||||
The skills are registered and the start hook's files are in place, but the hook is not active in the harness. gstack turns the hook on through the harness's own CLI and then reads it back, so this means the CLI was not on `PATH`, one of its commands failed or timed out (the message quotes its last output), or the harness's settings block the plugin: OpenClaw's `plugins.allow` set without `gstack-start`, `gstack-start` in `plugins.deny`, or `hooks.allowPromptInjection: false`. gstack never edits your allow or deny lists. Run the printed command, then `gstack-harness status`; `--start-hook` again retries the whole step. `./setup --host` exits 4 for the same reason. Refreshes and upgrades retry on their own without failing, and stop once you disable the plugin in the harness.
|
||||
|
||||
<a id="harness-qm-credentials"></a>
|
||||
### `gstack-harness: no QM admin credential in the environment` / `... needs --yes` (HARNESS_QM_CREDENTIALS)
|
||||
|
||||
|
||||
+109
-13
@@ -65,10 +65,12 @@ A run that writes a record ends with one line the parent greps:
|
||||
GSTACK_RESULT: skill=<name> status=<status> run=<dir>
|
||||
```
|
||||
|
||||
`status` is one of `complete`, `gate_pending`, `incomplete`, `interrupted` or
|
||||
`refused`; `run` is where the run's record is. `/autoplan`
|
||||
`status` is one of `complete`, `reviewed`, `gate_pending`, `incomplete`,
|
||||
`interrupted` or `refused`; `run` is where the run's record is. `/autoplan`
|
||||
always ends `gate_pending` when it reaches the final gate, because the gate is
|
||||
written and never approved. Pure queries (`gstack-artifact schema`,
|
||||
written and never approved. `/autoplan --ceo-only` ends `reviewed`: it runs the
|
||||
CEO phase alone, asks nothing, writes no gate and approves nothing (see
|
||||
[CEO-only runs](#ceo-only-runs)). Pure queries (`gstack-artifact schema`,
|
||||
`gstack-gate render`) print no result line.
|
||||
|
||||
Not every workflow writes this line or a run directory today:
|
||||
@@ -237,8 +239,13 @@ and reruns carry with `--pregate .gstack/tmp/pregate.json`. `CARRY: carried from
|
||||
`GSTACK_RESULT: skill=ship status=complete run=<dir>` line means the receipt
|
||||
now names HEAD with a `carry` object holding the `git range-diff` evidence, and
|
||||
`ship-run.json` gains `carried_from` and `carry_reason`; the `review_loop`
|
||||
counters are kept, because the reviewed product diff is unchanged. `CARRY:
|
||||
current` means nothing moved. `CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)`,
|
||||
counters are kept, because the reviewed product diff is unchanged or its only
|
||||
difference passed a scoped review. `CARRY: current` means nothing moved.
|
||||
`CARRY: refused (RECEIPT_CARRY_SCOPED_REVIEW)` means the base edited context
|
||||
next to reviewed hunks. Carry without `--dry-run` writes the packet the
|
||||
refusal names (a dry run writes none), one fix-check reviewer reads it, and a
|
||||
clean record reruns carry with `--scoped-review <record.json>`
|
||||
([troubleshooting](troubleshooting.md#receipt-carry-scoped-review)). `CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)`,
|
||||
`(RECEIPT_CARRY_TREE_MISMATCH)` or `(RECEIPT_CARRY_REFUSED)` with the refused
|
||||
result line means the product diff changed, or history was rewritten onto a
|
||||
tree no review saw, so /ship runs again;
|
||||
@@ -252,7 +259,7 @@ tree no review saw, so /ship runs again;
|
||||
| File | Schema | Contents |
|
||||
|------|--------|----------|
|
||||
| `run.json` | `run` v1 | The manifest: `run` id, `status`, `required_phases`, each phase's `native` and `outside` outcome (`status`, `model`, `provider`), input `snapshot` hashes, `counts`, every artifact's `path` (relative to the run directory) and `sha256`, `session_kind`, `gate_rev`, `deviations`, `consumed_by`. |
|
||||
| `decisions.jsonl` | `decisions` v1 | One row per decision: `id` (`<run>-<label>` at gate revision 1, `<run>-r<rev>-<label>` after a reopen, since labels restart at each gate), `title`, `options`, `recommended`, `kind` (`auto`, `approval`, `user_challenge`), `status` (`pending`, `approved`, `overridden`, `rejected`), `gate_rev`, and once answered `chosen`, `answered_at`, `reply`. |
|
||||
| `decisions.jsonl` | `decisions` v1 | One row per decision: `id` (`<run>-<label>` at gate revision 1, `<run>-r<rev>-<label>` after a reopen, since labels restart at each gate), `title`, `options`, `recommended`, `kind` (`auto`, `approval`, `user_challenge`), `status` (`pending`, `approved`, `overridden`, `rejected`, or `auto_kept` in a [CEO-only run](#ceo-only-runs)), `gate_rev`, and once answered `chosen`, `answered_at`, `reply`. |
|
||||
| `findings.jsonl` | `findings` v1 | One row per reviewer finding: `id`, `phase`, `voice` (`native`/`outside`), `model`, `severity`, `title`, `disposition`, `plan_items`, `native_counterpart` (string or null), `source`. Rows share the review-log row shape, so `gstack-review-log --findings` derives review status from this file. |
|
||||
| `tasks.jsonl` | `tasks` v2 | Implementation tasks with `id`, `title`, `status`, `depends_on`, `findings` (ids in `findings.jsonl`), `blocked_by`, `acceptance`, optional `pr`/`item`/`tier`. |
|
||||
| `timing.json` | `timing` v1 | Per-phase `started_at`, `ended_at`, `wall_s`, `outside_s`, `native_s`; `started_at` and `total_wall_s` for the run; the Phase 0 `estimate`. |
|
||||
@@ -369,7 +376,14 @@ the plan.
|
||||
refuses to open without one (`CONSENSUS_MISSING`).
|
||||
3. **Loop `next` until `status=gate_pending`.** Eng always runs last. The gate
|
||||
(`gate.json`, `gate_rev`) lists user challenges as approval items,
|
||||
disagreements as auto items and `p1` plan approval; nothing is approved.
|
||||
disagreements as auto items, each open Critical or High finding as an
|
||||
auto item (a twin pair is one item; `a` applies the reviewer's fix to the
|
||||
plan and is recommended, `b` carries it into the build as a blocking task)
|
||||
and `p1` plan approval; nothing is approved. An unanswered auto item whose
|
||||
recommendation changes the plan keeps the plan's direction, so only `rec`,
|
||||
`p1b` (approve with the recommended changes) or an explicit `<id>a` applies
|
||||
a fix; a finding carried with `b` stays
|
||||
open, so a rerun that raises it again asks again.
|
||||
4. **Answer the gate** with the reply grammar and the revision you read:
|
||||
```bash
|
||||
$AP answer --out <dir> --gate-rev 1 --reply "rec" --by human # every recommendation, plan changes included (APPLY: ...; amend_pending)
|
||||
@@ -380,12 +394,18 @@ the plan.
|
||||
means an agent relayed them, and then `--owner-reply` (or `-` for stdin,
|
||||
`--owner-reply-file <path>`) stores the owner's words verbatim
|
||||
(`OWNER_REPLY_REQUIRED`); owner words that parse to different choices are
|
||||
refused (`GATE_OWNER_REPLY_MISMATCH`). An unattended run refuses an answer
|
||||
refused (`GATE_OWNER_REPLY_MISMATCH`). The words default to the owner's: an
|
||||
agent that answers in its own words (a Captain deciding on its own
|
||||
authority) must pass `--words-by agent:<id>` with its own `--by` id. They
|
||||
are then recorded as `answer_words` with `words_by`, never as
|
||||
`owner_reply`; a later gate never carries them (`CARRY: no`), and
|
||||
`gstack-approvals import` lists that row under the agent, not the owner.
|
||||
An unattended run refuses an answer
|
||||
without `--by` (`ANSWER_BY_REQUIRED`, exit 3) and records the value as
|
||||
`answered_by` on every decision row and in the review record's gate line,
|
||||
so a gate an agent closed is distinguishable from one the owner closed.
|
||||
`gstack-autoplan recommend` changes one recommendation before relay; it
|
||||
bumps the gate's `rec_rev`, and a later `rec` needs the `--rec-hash` from
|
||||
bumps the gate's `rec_rev`, and a later `rec` or `p1b` needs the `--rec-hash` from
|
||||
the render the owner saw (`GATE_REC_STALE`). Every `GATE:` line is followed
|
||||
by `GATE_SPEND:` and `GATE_REC_COST:` (also `gate_cost` in `--json`; see
|
||||
`autoplan/DRIVER.md`).
|
||||
@@ -435,7 +455,19 @@ Concurrency and recovery: every state write is a locked read-modify-write
|
||||
(`test/autoplan-run-recovery.test.ts`, `test/autoplan-run-concurrency.test.ts`,
|
||||
`test/autoplan-answer-invalidation.test.ts`). `--spend-cap` admits each attempt
|
||||
through `spend.json` (`SPEND_CAP_EXCEEDED` refuses, exit 3); host-subagent
|
||||
spend is `unknown` and counted in `worst_case`. `--deadline <min>` makes the
|
||||
spend is `unknown` and counted in `worst_case`. Under a cap, in any session
|
||||
kind (or unattended on Capy), each host-subagent attempt, and each api attempt
|
||||
whose model has no price, reserves the default per voice unless
|
||||
`next --estimate-usd <n>` sets the reservation, which the run keeps for later
|
||||
phases (`run` reserves an api attempt's priced worst case, prompt bytes / 3
|
||||
input tokens plus the output limit, and settles it at its token usage; every
|
||||
native call that went out and bound no result, whether it failed, was cancelled,
|
||||
had its result refused, or was in flight when the process died (the next `run`
|
||||
finds it), settles at its usage, or at its full reservation when the usage is
|
||||
unknown, while one refused before anything was sent, such as with no API key,
|
||||
keeps its attempt open for the rerun, and a failed outside call leaves the phase
|
||||
native-only and stays counted at its reservation); `submit
|
||||
--usage-usd <n>` settles an attempt at its real cost. `--deadline <min>` makes the
|
||||
`run` subcommand cancel the children it started and marks the run
|
||||
`interrupted`.
|
||||
|
||||
@@ -515,9 +547,8 @@ A gate revision after the first (`gate_rev` 2 and later, written after a
|
||||
it when nothing new needs their decision. `gstack-autoplan` prints
|
||||
`CARRIED: gate_rev=<n> <- gate_rev=<m> words="<their words>" by=<relayer> reason=<why>`,
|
||||
then the usual `APPROVED:` lines, and journals `gate_carried`. The carry needs
|
||||
all of: the earlier gate approved (`p1a` or `p1b`) with `rec` or "do it"
|
||||
(not item tokens), no item there left pending, and the owner's words
|
||||
recorded; that gate was not itself carried; every accepted change incorporated
|
||||
all of: the earlier gate approved (`p1a` or `p1b`), no item there left
|
||||
pending, and the owner's words recorded; that gate was not itself carried; every accepted change incorporated
|
||||
or settled; the plan file's bytes equal those recorded at the gate or at the
|
||||
`revise` that took the changes (any later edit refuses, an ELI5-only one
|
||||
included); no urgent or
|
||||
@@ -526,6 +557,13 @@ Anything else prints `CARRY: no (<why>)` (journal `carry_refused`) and the gate
|
||||
goes to the owner as before. `--no-carry-forward` on the first `next` turns it
|
||||
off for the run.
|
||||
|
||||
An earlier answer given item by item (`<id><option>` tokens, not `rec` or "do
|
||||
it") carries only what the owner already answered: every item on the new gate
|
||||
must be carried by origin (the same proposal, so the same choice), and only an
|
||||
unreviewed post-cap delta (below) may be new. The carry replays the owner's own
|
||||
`p1` choice and records their tokens verbatim as its basis; any new or changed
|
||||
item, "do it" cases included, goes to the owner.
|
||||
|
||||
- **"do it".** When the owner's words were "do it" (their words, not a
|
||||
`captain:` relay of them), a new `auto` item that changes no scope, cost or
|
||||
direction, whose recommended option changes no plan text, and is no round
|
||||
@@ -555,6 +593,64 @@ and `gstack-owner-brief render --captain` opens with "Nothing pending.
|
||||
Carried forward from gate_rev <m>", followed by any items taken under "do it"
|
||||
without being shown to the owner.
|
||||
|
||||
## CEO-only runs
|
||||
|
||||
`/autoplan --ceo-only` runs the CEO review by itself and asks nothing. It runs
|
||||
in the session that invokes the skill and needs Claude Code: other hosts inline
|
||||
autoplan's sections and do not carry this one yet. A parent agent on any host
|
||||
gets the same run from the driver with `gstack-autoplan next --ceo-only` (see
|
||||
below). It is for a harness or a crew that wants the strategy pass without the
|
||||
full gauntlet, such as an eval comparing gstack's CEO review with a plain
|
||||
prompt.
|
||||
|
||||
- **What runs:** Phase 0, the outside-reviewer preflight and the CEO phase.
|
||||
Design, DX, Eng and the final gate do not run.
|
||||
- **How it decides:** every question takes its recommendation and is logged in
|
||||
the Decision Audit Trail. The review mode comes from plan-ceo-review's 0E
|
||||
rule (file count, then greenfield, added capability or fix), not autoplan's
|
||||
fixed SELECTIVE EXPANSION, and the mode line says `CEO-only run`. Consent,
|
||||
destructive and irreversible options take the conservative choice.
|
||||
- **What it never does:** apply a user challenge (the plan's direction stays;
|
||||
each challenge is listed), chain to another skill, promote the CEO plan, or
|
||||
approve anything.
|
||||
- **One model:** add `--no-outside` to skip the outside voice, so every
|
||||
reviewer runs on the host model.
|
||||
- **Where it writes:** the run directory (`.gstack/tmp/autoplan/` in the
|
||||
repository), the reviewed plan, and CEO plans and task files under
|
||||
`GSTACK_STATE_ROOT`. Point that variable inside a trial folder to keep every
|
||||
write there.
|
||||
- **What it leaves:** the reviewed plan (its original is kept at the restore
|
||||
point), `<run dir>/ceo-review.md` (user challenges, scope added,
|
||||
conservative choices, findings and the audit trail), and the last line
|
||||
`GSTACK_RESULT: skill=autoplan status=reviewed run=<dir>`.
|
||||
|
||||
`test/skill-e2e-autoplan-ceo-only.test.ts` runs it on a plan that makes the
|
||||
interactive CEO review ask a question and fails if this mode asks anything.
|
||||
|
||||
**From the driver.** `gstack-autoplan next --out <dir> --plan <file>
|
||||
--ceo-only` opens only the CEO phase; the parent dispatches and submits its
|
||||
voices as usual. When CEO closes, the run prints `REVIEWED:` and ends
|
||||
`GSTACK_RESULT: skill=autoplan status=reviewed run=<dir>`, with
|
||||
`<dir>/ceo-review.md` in the same sections, hashed in `run.json`. Each item a
|
||||
gate would have asked becomes a `decisions.jsonl` row of kind `auto` and status
|
||||
`auto_kept` (nobody answered: the plan's direction is kept, and a round-cap item
|
||||
only the owner can accept, such as a Critical left open, stays undecided with
|
||||
no chosen option), and tasks are blocked on no decision. An open Critical or
|
||||
High finding, which a full gate asks about, gets no row: nobody decides it, and
|
||||
it stays in the report's Findings and in `tasks.jsonl` for the owner. The driver never edits the plan, writes no `gate.json` and never
|
||||
approves, so `gstack-artifact validate` accepts the run as it stands.
|
||||
|
||||
- The flag goes on the first `next` only. It refuses beside `--ui`,
|
||||
`--developer-tool` or `--agent-primary` on the command line or in the plan's
|
||||
scope marker, when the marker names it, and on a plan at the report path
|
||||
(`CEO_ONLY_CONFLICT`); a directory that holds a full run refuses it
|
||||
(`CEO_ONLY_AFTER_INIT`).
|
||||
- `answer`, `recommend` and `revise` refuse a CEO-only run, and so does
|
||||
`gstack-approvals import` (`CEO_ONLY_NO_GATE`): there is no gate to answer and
|
||||
nothing was approved. A run stopped by `--spend-cap` recovers as a full run
|
||||
does: `budget` then `resume`, or `close --phase ceo --deferred`, which ends
|
||||
it `reviewed`.
|
||||
|
||||
## Owner brief
|
||||
|
||||
`gstack-owner-brief render --out <dir>` is what the owner sees at the final
|
||||
|
||||
+9
-1
@@ -8,7 +8,7 @@ If gstack is installed on your machine:
|
||||
~/.claude/skills/gstack/bin/gstack-uninstall
|
||||
```
|
||||
|
||||
This handles skills, symlinks, harness registrations (the files `gstack-harness` wrote into OpenClaw, Hermes, a Dot or another harness, from their ledgers), global state (`~/.gstack/`), project-local state, browse daemons, and temp files. Use `--keep-state` to preserve config and analytics. Use `--force` to skip confirmation.
|
||||
This handles skills, symlinks, harness registrations (the files `gstack-harness` wrote into OpenClaw, Hermes, a Dot or another harness, from their ledgers, plus the opt-in [start hook](hosts.md#start-hook), which it turns off through the harness's own CLI), global state (`~/.gstack/`), project-local state, browse daemons, and temp files. Use `--keep-state` to preserve config and analytics. Use `--force` to skip confirmation.
|
||||
|
||||
## Option 2: Manual removal (no local repo)
|
||||
|
||||
@@ -54,6 +54,7 @@ rm -rf "${HERMES_HOME:-$HOME/.hermes}/skills/gstack" 2>/dev/null
|
||||
rm -rf ~/.cursor/skills/gstack* 2>/dev/null
|
||||
rm -rf ~/.config/opencode/skills/gstack* 2>/dev/null
|
||||
rm -rf ~/.gemini/antigravity-cli/skills/gstack* 2>/dev/null
|
||||
rm -rf ~/.gemini/skills/gstack* 2>/dev/null
|
||||
|
||||
# 6. Remove temp files
|
||||
rm -f /tmp/gstack-* 2>/dev/null
|
||||
@@ -71,6 +72,13 @@ hook, the AskUserQuestion PreToolUse/PostToolUse hooks, and the Stop hooks
|
||||
(session timeline, plus verify-gate if you opted in). Left in place, they error
|
||||
on every matching event once the install directory is gone.
|
||||
|
||||
If you turned on the OpenClaw or Hermes [start hook](hosts.md#start-hook),
|
||||
manual removal also leaves its `gstack-start` plugin turned on. Turn it off
|
||||
with `openclaw config unset plugins.entries.gstack-start` or `hermes plugins
|
||||
disable gstack-start`, then delete
|
||||
`${OPENCLAW_STATE_DIR:-~/.openclaw}/extensions/gstack-start` or
|
||||
`${HERMES_HOME:-~/.hermes}/plugins/gstack-start`.
|
||||
|
||||
## Clean up CLAUDE.md
|
||||
|
||||
The uninstall script does not edit CLAUDE.md. In each project where gstack was added, remove the `## gstack` and `## Skill routing` sections.
|
||||
|
||||
+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