v1.92.62.0 stamp: merge main dd9834d44d into #3832 (land)

This commit is contained in:
garrytan committed 2026-11-02 07:27:33 +00:00
commit ef17b85a81
103 files changed
+4033 -455

No files matched your search

+69 -13
View File
@@ -1002,7 +1002,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>'`,
@@ -1011,12 +1011,12 @@ 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
@@ -1070,7 +1070,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.
@@ -1428,10 +1428,13 @@ No free test may write the developer's real home. `scripts/lib/free-home-guard.t
snapshots `~/.gstack`, `~/.claude`, `~/.codex`, `~/.agents` and `~/.config/gstack`
(live session logs excluded) and fails the run when an entry changes.
- A shard that runs alone (`--shard`, CI, the exclusive host-state shard, the
flaky retry) owns its window, so the failure names its files.
- Full-suite shards run concurrently on one HOME and cannot tell whose write a
change was. The runner guards that whole phase once and names no shard.
- A shard that runs alone (`--shard`, a CI shard at `--jobs 1` or its trailing
serial sub-shard, the exclusive host-state shard, the flaky retry) owns its
window, so the failure names its files.
- Full-suite shards, and the parallel sub-shards of a CI shard at `--jobs k > 1`
([sub-shards](#sub-shards-inside-one-ci-job)), run concurrently on one HOME and
cannot tell whose write a change was. The runner guards that whole phase once
and names no shard.
- `bun run scripts/test-free-shards.ts --attribute-home` names the writer: it
runs every file alone with a private HOME (browser cache and git identity
still come from the real home) and reports each file that wrote a watched
@@ -1450,7 +1453,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`).
@@ -1559,6 +1568,53 @@ refresh to a maintainer.
besides the bot's one refresh commit, later runs leave it alone. Only runs on
`main` push the branch; a dispatch from another ref reports only.
### Sub-shards inside one CI job
`--ci-run <plan> --shard i --result <file> --jobs k` runs one planned CI shard as
k sub-shards side by side on the same machine (`scripts/lib/shard-jobs.ts`). Each
sub-shard is an ordinary free shard child: one `bun test` process, serial
inside, with its own sandbox, process-group kill and log. Bun's worker
`--parallel` mode is still not used (decision V3/D6 in
`scripts/test-free-shards.ts`). The default is `k = 1`, which runs today's one
child, so no workflow changes behavior until it passes `--jobs`.
- `--jobs` applies only to `--ci-run`. It takes a positive whole number up to the
shard's file count (an empty shard takes 1); anything else stops with
`FREE_JOBS_INVALID`. `GSTACK_FREE_JOBS` sets the local full-suite process count
and never changes `--ci-run`. The exclusive host-state shard has one file, so
a workflow passes it `--jobs 1` or no `--jobs`.
- Files are split into disjoint sub-shards packed by the duration seed. Each
sub-shard has an id (`<plan id prefix>:<shard>.<n>`) bound to its file list.
Files in `TREE_MUTATING` or `SLOW_UNDER_LOAD` run after the parallel
sub-shards settle, serially, in one trailing sub-shard.
- The parallel phase uses the concurrent-phase real-home guard, the same one
full-suite mode uses; the serial sub-shard guards itself and names its files.
- The CI shard keeps its k = 1 wall deadline: the parallel sub-shards get it,
and the serial sub-shard gets whatever is left of it, so k never extends a
deadline. The parent waits for every child to settle.
- The parent merges every sub-shard result into the shard's one `outcome`
before the single flaky-retry pass, so the five-file retry cap stays per CI
shard. A missing, duplicate or incomplete sub-shard result fails the shard
with `FREE_SUBSHARD_REJECTED` and vetoes the retry. `--ci-verify` re-checks
the `jobs` and `subShards` records a `k > 1` result carries.
- Sub-shard runs never write `scripts/free-test-durations.json`; only
`--record-durations` does, and only from one-file children.
`FREE_SUBSHARD_TIMEOUT` means a sub-shard hit its wall at `k > 1`. The message
names the file in flight, k, the runner and the CI run id, and prints a serial
reproduce command. If the file passes serially, add it to `SLOW_UNDER_LOAD` in
`scripts/lib/shard-jobs.ts` with that run id and a reason. Never raise the
timeout. `SLOW_UNDER_LOAD` is empty today.
Every `--ci-run` result, at any k, records per-file test and `expect()` counts
in `outcome.counts` (`tests`, `pass`, `fail`, `skip`, `todo`, `expects` per
file, plus shard totals). They come from bun's JUnit reporter, which the runner
writes into the shard sandbox; on the pinned Bun 1.4.2 it attributes both counts
per file inside a multi-file child and leaves the console stream unchanged.
`complete: false` (with a `problem`) means the report was missing or did not add
up to bun's own test count. Counts are evidence for comparing k values; they
never decide a verdict.
### Windows free lane
Windows has minimal support ([support tiers](support-tiers.md)). The required
+3 -2
View File
@@ -317,8 +317,9 @@ stamp on the bare pin leaves the PR conflicting on the CHANGELOG. The lane merge
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.
+1 -1
View File
@@ -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 |
+28 -7
View File
@@ -201,17 +201,37 @@ in a data file still schedules the outside voice.
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.
**security** is a trust-boundary path (trust, redact, sanitize, neutralize,
consent, escape) or line (`shell: true`, `execSync(`, `eval`, `new Function(`,
raw HTML sinks, a shell run through argv, recursive deletes). **data** is user data: a path naming pii, gdpr, ccpa, privacy,
**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 waits for an allowlist classifier: three
rounds of R51 review found bypasses in line-pattern versions of both.
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
@@ -545,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
+48 -8
View File
@@ -401,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
@@ -851,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".
@@ -865,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.
@@ -937,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,
@@ -1629,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
+3 -3
View File
@@ -1006,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
@@ -1030,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
+97 -8
View File
@@ -880,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.**
@@ -3277,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`
@@ -4604,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
@@ -4710,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`
+31 -7
View File
@@ -259,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`. |
@@ -596,12 +596,12 @@ 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; `gstack-autoplan`, the driver a parent
agent uses, has no CEO-only mode yet, and `run.json` does not accept the
`reviewed` status. It needs Claude Code: other hosts inline autoplan's sections
and do not carry this one yet. 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.
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.
@@ -627,6 +627,30 @@ plain prompt.
`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