Merge origin/main (d9b0d600, v1.91.267.0) into absorb-b-pr-body-blast-radius-prose-lint (rule 19 grant, GSTA-36; CHANGELOG via mergeChangelogs, NOTICE.md additive)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-27 05:01:43 +00:00
commit 5619199800
114 files changed
+8354 -397

No files matched your search

+2 -1
View File
@@ -26,7 +26,7 @@ gstack/
│ ├── gen-agents-digest.ts # Generates the budget-capped instruction-tier digest (agents-digest/)
│ ├── host-config.ts # HostConfig interface + validator
│ ├── host-config-export.ts # Shell bridge for setup script
│ ├── resolvers/ # Template resolver modules (preamble, aside = the Aside driver contract + research, browse = $B fallback setup + command reference, qa = surface-aware QA/exploration, sections = lazy loading, design, design-checklist = renders review/design-checklist.md from lib/design-catalog.ts, review-dashboard / plan-gates / spec-review / review-scope, outside-voice = outside-voice primitives + the one failure policy, outside-voice-steps = second opinion / adversarial / plan and doc review, gbrain, etc.)
│ ├── resolvers/ # Template resolver modules (preamble, aside = the Aside driver contract + research, browse = $B fallback setup + command reference, qa = surface-aware QA/exploration, sections = lazy loading, design, design-checklist = renders review/design-checklist.md from lib/design-catalog.ts, design-craft = craft-section paragraphs and the Persuade checks for /design-review's craft audit and /plan-design-review, review-dashboard / plan-gates / spec-review / review-scope, outside-voice = outside-voice primitives + the one failure policy, outside-voice-steps = second opinion / adversarial / plan and doc review, gbrain, etc.)
│ ├── lib/shard-engine.ts # Shared shard engine for both test lanes: spawn, group kill, strict verdicts, per-shard sandbox, logs, duration seeds, flags
│ ├── skill-check.ts # Health dashboard
│ ├── test-free-shards.ts # Free-lane policy on the shard engine (GSTACK_FREE_JOBS, opt-in flaky retry)
@@ -78,6 +78,7 @@ gstack/
├── cso/ # /cso skill (OWASP Top 10 + STRIDE security audit)
├── design-consultation/ # /design-consultation skill (design system from scratch)
├── design-shotgun/ # /design-shotgun skill (visual design exploration)
├── design-motion/ # /design-motion skill (showcase loops: deterministic capture, MP4 and GIF exports)
├── open-gstack-browser/ # /open-gstack-browser skill (launch GStack Browser)
├── connect-chrome/ # symlink → open-gstack-browser (backwards compat)
├── setup-browser-cookies/, pair-agent/, skillify/ # Fallback-engine skills (cookie import, shared-browser tunnel, codify a /scrape)
+47
View File
@@ -1527,6 +1527,53 @@ the `recordGraceMs` option of `test/helpers/office-hours-attempt.ts` (default
5000) and the `timeoutMs` argument of `clearCookieTargetStorage(page, origin,
timeoutMs)` (default 15000).
## Candidate-side blinding
A paid case has two readers that must not know what the other knows: the
candidate (the agent under test) must not know it is being graded, and the
judge must not know which arm it is grading. `/eval-plan` Step 1c states the
three rules a preregistration carries; this is what they mean for gstack's own
fixtures.
- **The candidate's view carries no eval word.** Nothing the candidate can read
(its working directory, file names, the task prompt, commit messages, the
environment) says `eval`, `test`, `judge`, `experiment`, `rubric`, `score`,
`compare`, `benchmark`, `candidate` or `arena`. The project's own `test/`
directory is exempt when the task is about those tests, and so is the skill
text an arm loads as its own instructions (a gstack SKILL.md and the ETHOS.md it
points at use these words freely); the rule governs what the case builds. The
hermetic home a case builds (`test/helpers/hermetic-env.ts`) is the boundary:
what is copied in is visible, what stays in the runner is not.
- **Fixture roots are project-shaped.** A fixture the candidate works in is
named like the project it imitates, with the files that project would hold.
Runner-side fixtures (captured transcripts, judge calibration sets, census
reports) are never shown to a candidate and may keep descriptive names.
- **The judge grades labels and reads, not claims.** A judge prompt names arms
`A` and `B`, never the model or run, and a chain-following dimension is scored
from the files the transcript shows the candidate opened, not from what the
candidate says it read.
An audit on 2026-10-26 (GSTA-124) found these fixture directory names on the
word list: `test/fixtures/arm-benchmark/` and its four children, `browse-judge/`,
`cso-eval/`, `eval-history/`, `judge-calibration/` and its children, two shard
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`
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`,
`test-audit-report-only`) carry the word `test`; and `test/skill-e2e-health.test.ts`
creates `gstack-health-eval-*`. That list comes from a scan of the `mkdtemp`
prefixes in the e2e tests, not from a proof that no other root carries a listed
word. The audit matched whole
lower-case words in path components under `test/fixtures/`; a substring match
(`factory-scorecard/`, `ios-qa/FixtureApp/Tests/`) was not counted. Renaming is a
separate change with its own touchfile updates; the list is here so that change
can be scoped.
## Running evals as an agent: detach
Moved verbatim from CLAUDE.md (#2096 size limit). The short rule stays in
+26 -1
View File
@@ -298,6 +298,26 @@ the machine's) and every `requires-remote` obligation carries a receipt
receipt then carries `pregate` and `requires_remote`. `gstack-pregate verify`
runs the same check standalone.
**By tree.** Every record `gstack-pregate` writes is also mirrored, byte for
byte, to `<state root>/pregate/by-tree/<tree>.json` (the state root from
docs/state-root.md). `gstack-ship-queue ready` reads the checkout's
`.gstack/tmp/pregate.json` first; when that file is missing or was recorded
for another tree, or is current but still waits on a remote receipt, it looks
the current tree up in the mirror and uses that record when it verifies,
printing `PREGATE: … (from the state root's pregate/by-tree/<tree12>.json,
recorded by another checkout)`. This is for the lane whose `gstack-pregate
--remote` had to run from a separate clone (a worktree cannot host it): the
clone's record clears the lane's own checkout as long as both hold the same
tree. Only a run that holds the tests stage stands in (what `--stage tests
--remote` writes, `checks=[lanes]`; a `--only timing` or preflight-only run
is "a run without the tests stage"), and it must be recorded against
`origin/<base>` and not under `--policy-from head`;
otherwise the `PREGATE:` line says why the mirror cannot stand in. A record
whose own checks failed or stayed incomplete is final and no mirror overrides
it. An explicit `--pregate <file>` that verifies is used as before; a mirror
for a different tree changes nothing. Writing the mirror is best effort (a
temp file renamed into place) and never fails the record itself.
Unknown flags exit 2 with usage on every verb (an agent told to run a flag this
gstack lacks never gets a green it did not ask for). Tier 2 checks still to come
(`ratchets`, `lint`, `links`, `hermetic`, `patches`, `integrator`) are named in
@@ -891,7 +911,12 @@ gstack-issue-links [--base <ref>] [--issue <n>]... [--ci-run <id>]... [--superse
Reads `origin/<base>..HEAD`. `Fixes #N` only on explicit closure intent: a
`Fixes`/`Closes`/`Resolves #N` trailer line or `--issue N`. A bare mention
(a subject's `(#N)` included), a partial fix, a deferred ask and every
cross-repo `owner/repo#N` are `Refs`. Same-repo references are printed as
cross-repo `owner/repo#N` are `Refs`. A parenthesised named reference,
`(name #N, #M)`, `(name PR #N)` or `(name issue #N)`, where the name is one
token that is not a closure, reference, filler or version word, is a
cross-repo `Refs name#N`, never a same-repo Refs line or a promotion
candidate; `(see #12)`, `(closing #12)` and `(#301)` stay bare refs.
Same-repo references are printed as
promotion candidates (`reference #N ← <commit line> (promote: fix #N)` on
stderr, `candidates` in `--json`): the owner's `fix #N` reply reruns with
`--issue N`; an unattended run keeps them `Refs` and lists them in the
+18
View File
@@ -56,3 +56,21 @@ if you stored `off` before that, it takes the answer as given and does not ask.
If collisions with other skill packs keep coming up under `core`, the fallback
Garry approved on 2026-10-25 is `off` by default.
## Questions a skill asks on its own
The setting above governs which skills gstack offers unasked. A second rule
governs what a running skill may ask you unasked: a question whose answer the
skill could observe is not yours to answer. Whether a change is faster, whether
two renders differ, whether an endpoint accepts a payload, whether a test still
fails: each of those is settled by running the thing (time it, render both,
call it, run the test) and reporting the result, not by a prompt. That holds
for observations that are local, read-only and free; a call that changes state,
reaches a live or third-party system or spends money is still yours to approve.
That extends the one-way-door rule in `/plan-tune`, which covers destructive,
architectural and security questions, to spend and third-party reach. The only
questions that come to you are product and preference calls (what the product
should do, what you want, what you are willing to spend or risk), and they
arrive with the observation already in hand so the choice is between measured
options. `/plan-tune` applies the same rule when it audits the question log: an
observable question is a defect in the asking skill, not a preference to tune.
+34 -16
View File
@@ -21,21 +21,39 @@ commit on main's first-parent history, moved only by this workflow, only
forward.
**Green** means, on that exact commit: the `free-tests` check run succeeded
(the status main requires), the `main-backstop` check run succeeded, every
check run on it has completed and none failed, and `refs/notes/gstack-trains`
holds no active freeze. The Main backstop gate workflow publishes
`main-backstop` on each push to main from a job of its own: success when a
green train record covers the tree or the backstop gated it green, neutral
when main moved past the push before it was gated (superseded) or a freeze's
own backstop run owns it (deferred), failure when it froze the queue or could
not finish. Neutral is never green: a commit whose own tree was never gated is
not released. A push that gets no backstop run at all (a dependabot push, a
policy without a train block) has no `main-backstop` check run and is not
green either; a cancelled check run counts as red. A commit with a check run still going is skipped and the next
older one is considered, back through 7 days or 200 first-parent commits. The
workflow runs no tests and no paid evals of its own; it only reads what the
trains and the backstop already recorded. With nothing green in the window it
fails and lists why each commit was skipped.
(the status main requires), the commit was gated (below), every check run on
it has completed and none failed, and `refs/notes/gstack-trains` holds no
active freeze. A cancelled check run counts as red. A commit with a check run
still going is skipped and the next older one is considered, back through 7
days or 200 first-parent commits. The workflow runs no tests and no paid evals
of its own; it only reads what the trains and the backstop already recorded.
With nothing green in the window it fails and lists why each commit was
skipped.
**Gated** means one of two pieces of evidence. The first is the
`main-backstop` check run: the Main backstop gate workflow publishes it on
each push to main from a job of its own, success when a green train record
covers the tree or the backstop gated it green, neutral when main moved past
the push before it was gated (superseded) or a freeze's own backstop run owns
it (deferred), failure when it froze the queue or could not finish. The second
is a train record in `refs/notes/gstack-trains` (kind `train`, state
`green`, `merging` or `merged`) whose `merged[]` lists the commit's sha or
its tree: those entries are the main commits `gstack-ship-queue verify
--train` confirmed, so a commit a green train merged is gated even when trains
merged faster than the backstop could settle and its own backstop run ended
neutral or never ran. The `CANDIDATE` line says which: `gated by
main-backstop` or `gated by train <nonce>`. Train evidence relaxes nothing
else: a red backstop run, a failed or missing `free-tests` run, any other red
check run or a run still going keeps the commit out, and a record in any other
state (`red`, `superseded`, `free-green`, …) is not evidence. A commit with
neither a successful backstop run nor a train record is not released: a
dependabot push, a policy without a train block, or a tree that was never
gated.
Another repository runs the same script with its own names:
`--required-check <name>` (default `free-tests`), `--backstop-check <name>`
(default `main-backstop`) and `--notes-ref <ref>` (default
`refs/notes/gstack-trains`). `weekly-release.yml` passes none of them.
A cut that stopped half-way (tag and `stable` pushed, release not created) is
finished by the next run, by cron or by hand: `stable` itself stays a
@@ -135,7 +153,7 @@ is exercised against real check runs before it merges.
| Line | Meaning |
|---|---|
| `CANDIDATE: <sha> (<date>)` | the commit that will be (or was) released |
| `CANDIDATE: <sha> (<date>) gated by main-backstop|train <nonce>` | the commit that will be (or was) released, and which evidence gated it (`[hotfix: --ref]` before `gated by` on a `--ref` cut) |
| `NOTHING_NEW: origin/main <sha> is already released as v<X>` | nothing merged since the last release; exit 0 |
| `NO_GREEN_COMMIT: ...` with `SKIPPED:` lines | nothing in the window passed the gate; exit 1 (a dry run exits 0: the skips are its answer) |
| `REFUSED: non-fast-forward: ...` | the candidate is behind stable; stable only moves forward |
+104 -22
View File
@@ -43,7 +43,7 @@ refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
| `queue_mode` | `claim-at-open` \| `stamp-at-merge` | `claim-at-open` | See [stamp-at-merge](#stamp-at-merge). `claim-at-open` is today's queue (`gstack-next-version` claims a slot when the PR opens). |
| `release_tool` | command or `null` | `null` | The repo's own release command (`bun run release:restamp`). Runs in an isolated staging worktree under the [release-tool contract](#release-tool-contract). |
| `release_outputs` | globs | `[]` | Paths the release tool may write beyond the version files, lockfiles, CHANGELOG and `stamp_paths`. Anything else is `RESTAMP_OUTPUT_OUTSIDE_ALLOWED`. |
| `stamp_paths` | globs | `[]` | Where exact `(was X)`, `since: X` and `vX` mentions of the branch's tentative version are rewritten on a restamp. |
| `stamp_paths` | globs | `[]` | Where exact `(was X)`, `since: X` and `vX` mentions of the branch's tentative version (quoted or bare: `since: 'X'` counts) are rewritten on a restamp. |
| `changelog` | path or `null` | `CHANGELOG.md` | Re-headed on a restamp: the PR's `## [Unreleased]` (or old-version) section moves to the top as `## [<version>] - <date>`. |
| `mirror` | relative path or `null` | `null` | A paired checkout to restamp identically (tier 2: validated and confined today, not yet run). |
| `history` | `merge-only` \| `squash-ok` | `squash-ok` | `merge-only` makes `/ship` refuse a squash or rebase plan; `gstack-ship-policy check-history` reports a rewritten branch (`HISTORY_POLICY_VIOLATION`). |
@@ -52,7 +52,7 @@ refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
| `pr_count` | `{ default_pr_count, split_requires_owner }` | `{1, true}` | The PR-count row in plan reviews. |
| `maintainers` | GitHub logins | `[]` | Maintainer set for `/review` contributor mode; empty means repository push permission. |
| `stamp_files` | globs | `[]` | The repo's stamp rule: when set, only these files may differ by a stamp (list the version file, manifests, plugin trees and the files holding the PR's own `since:` rows). It applies before every other stamp rule, so a doc `vX` mention outside it is a difference. |
| `stamp_patterns` | literal templates | `[]` | At most 16 templates of at most 128 characters, each with exactly one `{version}` slot (`"version": "{version}"`, `since: '{version}'`, `<!-- x-stamp: {version} -->`); matched literally at a version boundary, never as a regex. The restamp writer and the tree receipt use them beside the built-in forms. |
| `stamp_patterns` | literal templates | `[]` | At most 16 templates of at most 128 characters, each with exactly one `{version}` slot (`"version": "{version}"`, `since: '{version}'`, `<!-- x-stamp: {version} -->`); matched literally at a version boundary, never as a regex. The restamp writer, its mention scanner and the tree receipt use them beside the built-in forms. |
| `restamp_command` | command or `null` | `null` | The repo's own restamp, run first in the staging worktree; see [restamp_command](#restamp-command). |
| `migrations` | `{ dir, id }` or `null` | `null` | The migration directory and a regex with exactly one capture group for the number in a file name (`"^v(\\d+)-"`); `gstack-pregate regate` reports `MIGRATION_COLLISION`. |
| `gate_command` | command or `null` | `null` | The repo's full pre-merge gate. `gstack-ship-queue gate-ahead` and `ready` print it for the caller to run (in the background or in CI); gstack never executes it. Needs `min_gstack_version`. |
@@ -187,18 +187,25 @@ 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
dispatches the free workflows on the branch (`gh workflow run free-tests.yml
--ref <branch> && gh workflow run windows-free-tests.yml --ref <branch>`),
clears the requires-remote pregate lanes with those runs
(`gstack-pregate clear --remote <lane> --run-url <run>`) and reruns `ready`,
which passes because the dispatched check runs attach to the commit; the
`ci-hint:` line then says those non-pull_request runs count. The `ci-hint:` line is
advisory text for the lane; nothing parses it, and a PR whose head is on a fork
gets a hint that leaves the dispatch to the coordinator. `ready --screen` on such a
PR refuses, because there is no merge ref to screen (`QUEUE_SCREEN_STALE_REF`,
`<remote> has no refs/pull/<n>/merge`); that refusal's "merge <base> into the
branch" advice does not apply to a CHANGELOG-only conflict, so the lane READYs
with `--no-screen` after the skill's own paid cases ran focused.
runs `ready … --dispatch-ci free-tests.yml,windows-free-tests.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`
(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
<run>`, rerun `ready`. The `ci-hint:` line is advisory text for the lane;
nothing parses it. `ready --screen` on such a PR has no merge ref to screen,
so it builds the scratch merge described below (`SCREEN_MERGE:`, pushed as
`screen/<pr>-<head12>`) and screens that; only a real conflict with the base
(or a remote whose refs cannot be listed) is `QUEUE_SCREEN_STALE_REF`. A CHANGELOG-only conflict is screenable, and a
lane whose skill's own paid cases ran focused may still READY with `--no-screen`.
`ci:` counts the latest run per check (one app's check name), as GitHub's
rollup does: a check that was re-run or superseded on the same commit (a
@@ -393,7 +400,7 @@ explicit choice:
```bash
gstack-ship-queue ready --pr 42 # prints SCREEN: lines and the recommendation, refuses
gstack-ship-queue ready --pr 42 --no-screen # READY receipt; the PR rides unscreened
gstack-ship-queue ready --pr 42 --screen [--broad] # dispatches one paid screen on the PR's merge SHA (QUEUE_SCREEN_STALE_REF when GitHub's merge ref lags the head)
gstack-ship-queue ready --pr 42 --screen [--broad] # dispatches one paid screen on the PR's merge SHA (a scratch merge when GitHub's merge ref lags the head; QUEUE_SCREEN_STALE_REF only on a real conflict)
```
Without a choice it prints the `SCREEN:` selection (with
@@ -410,10 +417,13 @@ the policy, so a repeat follows the same run and a broad screen never adopts
a narrow one. The READY block gains `screen: …`, and the receipt is written
locally and to the notes ref. `--dry-run` prints the dispatch without sending
it. Before dispatching, `ready` fetches the merge ref and requires its second
parent to be the PR head: a missing or lagging merge ref (GitHub stops
recomputing it while the PR conflicts with the base, CHANGELOG-only conflicts
included) is `QUEUE_SCREEN_STALE_REF`, because a screen dispatched there plans
no cases and reports success. A finished screen that planned no cases (no
parent to be the PR head and its first parent the current base: a missing,
lagging or unfetchable merge ref (GitHub stops recomputing it while the PR
conflicts with the base, CHANGELOG-only conflicts included) is replaced by a
scratch merge of the head onto the base, pushed as `screen/<pr>-<head12>`
(the `SCREEN_MERGE:` line says which); a screen dispatched on a stale ref would
plan no cases and report success. Only a real conflict with the base (or a
remote whose refs cannot be listed) is `QUEUE_SCREEN_STALE_REF`. A finished screen that planned no cases (no
`paid-plan` artifact, which a superseded run never uploads, or a plan manifest
with nothing planned) is never followed as a screen: `ready --screen`
dispatches a fresh one and `train build` prints `SCREEN: #<n> run <id> planned
@@ -547,9 +557,81 @@ It runs the normal free tests, full gate and verification on main plus the
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
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
`POST_MERGE: <sha12> green|red|pending|none (passed= failed= pending= skipped=)`.
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
state, so rerunning it after a machine restart continues the watch.
**6. Clean up.** `gstack-ship-queue train prune [--train <nonce>] [--dry-run]`
deletes the `train/<nonce>` branches of merged, superseded and dropped trains
(`WOULD-DELETE:`, `DELETED:`, `GONE:`).
(`WOULD-DELETE:`, `DELETED:`, `GONE:`). `train prune --stale` also settles a
`merging` or `green` train whose every PR is MERGED or CLOSED on GitHub (its
PRs landed through another train or by hand): it logs each PR's state on a
`STALE:` line, marks the record `stale` and deletes its branch; a train with an
OPEN PR is kept (`KEEP: … still needs the train`). The same run deletes every
`screen/<pr>-*` scratch branch whose PR is MERGED or CLOSED and keeps one whose
PR is still OPEN. `train forget --train <nonce> | --receipt
<pr>@<sha> [--dry-run]` removes one record (a settled train with its attempts,
or one READY receipt) from local state and from `refs/notes/gstack-trains`
with an exact lease on the notes ref, printing `FORGET:` then `FORGOTTEN:`; a
train that is still building, running or merging (`built`, `pushed`,
`free-pending`, `free-green`, `dispatch-unknown`, `dispatch-absent`,
`gate-pending`, `merging`) is refused; a rejected lease removes nothing and
restores the local notes ref.
**Following a train without a daemon.** `gstack-ship-queue train watch --train
<nonce> [--timeout <s>]` is the one resumable follow command: it reads the
record, runs the step its state asks for (`train build --train` to push, run
the free tests and dispatch the gate; `train status` to settle an unconfirmed
dispatch), waits for the pending run by polling gh, reads the verdict and
prints the last step's `NEXT:`. It never dispatches anything the record would
not dispatch on its own, so rerunning it after an interruption continues from
the record (`WATCH:` lines say what it did; a timeout, default 7200 s, is
`QUEUE_TRAIN_PENDING` with the same command as `NEXT:`; `--free-timeout <s>`,
default 2700, bounds the wait for the free tests).
**Screens are read from the run.** `ready --screen` and `verify --pr <n>
--screen` read a dispatched screen's verdict from the run's own artifacts
(`gate-binding-a<attempt>`, `report-verdict-a<attempt>`), never from a lane's
prose, and record it in the READY receipt (`screen.verdict`): `SCREEN_VERDICT:
#<n> run <id> GREEN|RED|NOT-RUN|INCOMPLETE [red=…] [flaky=…]` plus one
`FLAKY:` line per rule-16 flaky case. `PENDING` is a run still going,
`UNKNOWN` one the queue cannot read, and `INCOMPLETE` a finished run that bound
no verdict (its report job did not finish): `ready --screen` dispatches afresh
instead of following it, `verify --screen` refuses it (`QUEUE_PROOF_MISSING`)
and `train build` seats the pin unscreened, as for `NOT-RUN`. A RED screen refuses `ready` under
either choice, is never re-dispatched at the same merge SHA and is not seated
by `train build`, whose `NEXT:` names the build without that PR
(`QUEUE_SCREEN_RED`); a run that concluded success with no slice (its route
job found the head superseded or the plan selected nothing) is `NOT-RUN`:
`verify --screen` refuses with `QUEUE_SCREEN_NOT_RUN`, `ready --screen`
dispatches afresh instead of following it, and `train build` seats the pin
unscreened with a `SCREEN: … counted as not-run` line. When GitHub's
`refs/pull/<n>/merge` lags the PR head or is missing, `ready --screen` plans
the screen on a scratch merge of the head onto the base, pushed as
`screen/<pr>-<head12>` (`SCREEN_MERGE:` names it; `prune --stale` deletes it
once the PR merged or closed); only a real conflict with the base (or a remote
whose refs cannot be listed) is `QUEUE_SCREEN_STALE_REF`.
**Stacked tests before the gate.** `train build --prs … --dry-run --stack-tests
[--stack-tests-command "<cmd>"]` runs each PR's new test files (added relative
to the base, with a name ending in `.test`, `_test`, `.spec` or `_spec` and a
JS or TS extension, bun's own rule, so a helper or fixture under `test/` is not
one) on the integrated prefix that includes it, T<k>, in a scratch worktree, and prints
`STACK_TESTS: #<pr> T<k> <n> new test file(s) pass|FAIL (<files>)` or `no new
test files`. A failure is `QUEUE_TRAIN_STACK_RED` (nothing written, pushed or
dispatched): the tests pass on the PR head alone only because nothing before
it in the train had changed what they read, so fix them against that prefix or
reorder with `--order`. The default command is `bun --no-env-file
--config=/dev/null test` with the files appended.
**Escape hatches.** Remove the `train` block on the base branch to return
every lane to the per-PR queue at once. `--urgent` ships one PR alone,
@@ -715,7 +797,7 @@ Stages, each journaled in `.gstack/tmp/restamp-journal.json`:
1. **version**: `gstack-version-bump write --version <X>` in the staging worktree (VERSION, the manifest, npm lockfiles); skipped when `restamp_command` owns the version.
2. **release_tool**: the policy command, under the contract below.
3. **changelog**: the PR's `[Unreleased]` or old-version section moves to the top under `<X>`, above its predecessor's entry. A duplicate heading the PR introduces (`CHANGELOG_DUPLICATE_HEADING`: the headings are counted against the base's CHANGELOG at `origin/<base>` and at the fork point, so a version the base already heads twice passes as long as the PR adds no further copy) and a top entry that is not `<X>` (`CHANGELOG_TOP_MISMATCH`) are errors; the section being re-headed itself may exist only once.
4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites, plus every `stamp_patterns` template, within `stamp_paths` and `stamp_files`. `X` is the branch's own tentative stamp (its VERSION when it moved past base), else `--was <X>`, else the version an unstamped branch would have taken next (`base + bump`); mentions of a version the base never released (not a CHANGELOG heading at base, not a tag) are rewritten too, so a queue reorder that moves the base under a branch still finds its `since:` fields. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate.
4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites, plus every `stamp_patterns` template, within `stamp_paths` and `stamp_files`. A quote around X in the first two forms is part of the form and is kept as written: `since: 'X'`, `since: "X"`, `(was 'X')` and `(was "X")` move like the bare ones (the shape of a quoted source row such as gbrain's `{ since: '0.60.468.0', … }`), an opened quote must close right after X (`since: '1.2.3-beta'` and `since: '1.2.3"` are not the form, in the writer and in the tree receipt alike), and `vX` already accepts a quote before the `v`. Which X moves is decided by provenance. The branch's own tentative stamp (its VERSION when it moved past base) and `--was <X>`, when given, are each rewritten on every line of every stamp file. Beyond that, only lines the branch itself added are read, by their position in the file (`git diff --unified=0` from the fork point), so a base line with the same text, or a CRLF checkout whose diff is LF-normalized, is told apart: a stamp-form mention there of a version newer than the version at the branch's fork point is a tentative stamp and moves (this is what finds a quoted row after a lost slot and a merge of the base, since the base's release is newer than the fork point), and when the branch has no stamp of its own because its VERSION equals the base's and its fork point is the base itself (a branch rebased onto the base after it reached X, or one never stamped) a mention of the base version on a line it added is its stale stamp and moves too, while the same version on a line the base already carries is the base's release and stays. The stage detail names both counts per file (`VERSION equals base X, so X moved only on lines this branch added and stayed on the base's own lines (<file>: N moved, M kept)`), so the stamp commit shows which lines moved and why. The rule reads line ownership, not meaning: a branch-added line inside `stamp_paths` that means the base's own release in a stamp form (`Requires vX or later` written on a branch forked at X) moves with it, which the `--dry-run` diff and the per-file counts show before anything is written; write such a reference outside a stamp form, or keep that file out of `stamp_paths`. `--was X` is the opposite tool: it rewrites X everywhere, the base's own lines included, and re-heads the CHANGELOG section `## [X]`. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate.
5. **mirror**: tier 2; declared and validated, reported as not run.
6. **apply**: every changed file is written to the branch only after all stages validated, each against the fingerprinted preimage; a file edited meanwhile is `RESTAMP_CONFLICT` and the applied writes roll back (rollback refuses to overwrite a file whose bytes differ from the recorded postimage).
@@ -794,7 +876,7 @@ manifest (`version` key only), npm and bun lockfiles (root `version` keys
only, where `gstack-version-bump` writes; a dependency entry is `changed`),
the CHANGELOG (a heading move or re-heading, compared as an order-independent
multiset of entries), `stamp_paths` and `release_outputs` (every
removed/added pair must be an exact `(was X)` / `since: X` / `vX` rewrite with
removed/added pair must be an exact `(was X)` / `since: X` / `vX` rewrite (a quote around X in the first two forms included) with
X the gated version 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
+38
View File
@@ -15,6 +15,7 @@ Detailed guides for every gstack skill — philosophy, workflow, and examples.
| [`/design-review`](#design-review) | **Designer Who Codes** | Live-site visual audit + fix loop. 80-item audit, then fixes what it finds. Atomic commits, before/after screenshots. |
| [`/design-shotgun`](#design-shotgun) | **Design Explorer** | Generate multiple AI design variants, open a comparison board in your browser, and iterate until you approve a direction. Taste memory biases toward your preferences. |
| [`/design-html`](#design-html) | **Design Engineer** | Generates production-quality Pretext-native HTML. Works with approved mockups, CEO plans, design reviews, or from scratch. Text reflows on resize, heights adjust to content. Smart API routing per design type. Framework detection for React/Svelte/Vue. Previews render through your Aside browser. |
| [`/design-motion`](#design-motion) | **Motion Designer** | Turns an approved design into a showcase loop: rebuilds the exact frame, loads it from empty, one story per visual, then an MP4 for X, a website GIF and an 800×500 GIF after seam, rest and overflow checks. |
| [`/qa`](#qa) | **QA Lead** | Explore browser and functional behavior (APIs, CLIs, jobs, workers, webhooks), reproduce defects, prove regressions fail before repair, then fix and re-verify. |
| [`/qa-only`](#qa) | **QA Reporter** | Explore the same surfaces and propose regression cases with evidence, without changing product code or tests. |
| [`/scrape`](#browse) | **Browser Data Extractor** | Pull structured data off a web page — tables, lists, prices — in your Aside browser with the page's real logged-in state. Same driver contract as `/browse`. On the fallback browser, a codified browser-skill answers a repeat intent in ~200ms. |
@@ -555,6 +556,43 @@ Claude: Saved to ~/.gstack/projects/myapp/designs/hero-20260330/finalized.html
---
## `/design-motion`
This is my **showcase mode**.
A launch post on X wants the product moving, not a static mockup. Screen-recording a page in real time gives you dropped frames, a loop that jumps at the seam, and a card that drifts out of its container halfway through.
`/design-motion` takes an approved mockup from `/design-shotgun`, a `finalized.html` from `/design-html`, or a URL. It rebuilds the exact frame as one self-contained page, starts it from an empty state, loads it in with a soft rise-and-resolve, and gives each visual one story: a chart gets read, a list receives a new item, a prompt retypes itself. Then it dissolves and repeats.
Nothing is recorded in real time. `bin/gstack-motion-capture.ts` drives the page on Playwright's fake clock in whole-millisecond steps that average 1000/60, so frame N is always the same pixels. `bin/gstack-motion-encode check` then proves the loop with ffmpeg's PSNR: the first frame equals the frame one cycle later (seam), the empty beat equals the frame before the start (no JS state left over), the last frame before the dissolve equals the still design (every story came home), and nothing moves outside the containers (overflow). Only then does `export` write an MP4 for X, a 960 px website GIF and an 800×500 GIF.
On a website it follows gstack's design rules instead: one authored motion moment per page, content visible without script, and no pulsing dots or marquees left running. ffmpeg is optional; without it the skill builds the page and tells you how to install it. A Figma source works when a `FIGMA_TOKEN` is set.
Adapted from Elaya's [design-to-motion](https://github.com/elayadesign/ai-design-skills) (MIT, © 2026 Elaya).
### Example
```
You: /design-motion
Claude: Found approved mockup: variant-B.png (analytics bento, 1440×990)
Context: showcase. Layout: separate cards (4).
Stories: chart scrubber, a new lead lands on the stack, KPI counts up.
Cycle: 12 s.
[Rebuilds the frame, still matches the mockup at 37 dB]
[Captures 744 frames on a fake clock]
MOTION_CHECK: seam pass
MOTION_CHECK: empty-beat pass
MOTION_CHECK: rest pass
MOTION_CHECK: overflow pass
bento-x.mp4 (1744×1200, 60 fps, 12 s), bento-loop.gif, bento-800x500.gif
```
---
## `/review`
This is my **paranoid staff engineer mode**.
+1
View File
@@ -16,6 +16,7 @@ Beyond the slash-command skills, gstack ships standalone CLIs for workflows that
| `gstack-review-log` | **Review-pass receipts** — `--start <skill>` captures the working-tree fingerprint before a diff review; `'<JSON>' --finish <token>` consumes that single-use, repository/branch/skill-scoped receipt. Binding requires matching start/end content and reviewer-reported `completed:true` and `converged:true`; it is not independent proof that a model read the source. `--check-shared-libs <token>` reads a current finding as JSON on stdin and checks prior Skip decisions against the actual branch, capture and eligible raw source blobs. It returns `reusable:false` when proof is missing or unsafe, including older records without logger-versioned coverage. Final review logging computes shared-code fingerprints and coverage rather than trusting supplied proof. |
| `gstack-review-read` | **Review freshness** — emits review records with computed `review_freshness.status` and `reason`: CURRENT, STALE, or UNVERIFIED for diff reviews. `/ship` and `/land-and-deploy` use the same grade; a matching commit alone never certifies a diff review. [Dashboard rules](skills.md#review-readiness-dashboard). |
| `gstack-evidence` | **Verification-evidence ledger** — `run --label <lane> -- <cmd>` transparently wraps any test command (the child's exit code always passes through) and records what ran against which working-tree fingerprint; `check` grades each label FRESH/STALE/MISSING with `--expect-cmd`, `--max-age`, and `--allow-paths` binding. /ship and /land-and-deploy cite fresh evidence instead of re-running suites. Per-run logs are 0600, capped at 2MB, pruned after 30 days; the ledger and logs stay machine-local by design. |
| `gstack-pr-wait` | **PR event watcher** — `gstack-pr-wait <pr> [--repo owner/name] [--until green\|ready\|any-change] [--interval 20] [--timeout 60]` polls one pull request through `gh` and prints one stdout line per real event (a check failed with its link, all checks finished, a comment or review from someone other than the author, a review bot finished on the head, the base moved and its new commits touch this PR's files, BEHIND or DIRTY, merged or closed), with progress on stderr, so a monitor wakes the agent once per event instead of on a timer. `green` gates on every check on the head (an advisory bot's review is never waited for, its checks gate like any other), as `gstack-ci-gate` does; a check the base branch requires (rulesets and classic branch protection) is pending until it appears, never green by omission; two identical polls confirm a check set is complete. Exit 0 when the condition is met, 1 when it needs action or `--timeout` passes (`PR_WAIT_BLOCKED`), 2 on bad input, a second watcher for the same PR or a local error (`PR_WAIT_REFUSED`). State under `<state root>/pr-wait/`, baseline discarded after 6 hours. Review bots are configuration: `gstack-config set pr_wait_gating_bots greptile,macroscope`, `pr_wait_advisory_bots coderabbit` (advisory bots are reported, never waited on). `/land-and-deploy` Step 3 is one call of it. Design ported from tstack's pr-wait (MIT, Theo Browne; NOTICE.md). [Exit codes](troubleshooting.md#gstack-pr-wait). |
| `gstack-issue-guard` | **Tracker-text trust envelope** — fetches GitHub issue/PR text (`issue <n>`, `pr-body`, `pr-comments`, or `--stdin`) and wraps it in a labeled envelope so agents treat it as data: injection-shaped lines get labeled even through fullwidth and invisible-character evasion, and forged envelope banners are defused. Every tracker-text ingress in gstack routes through it, enforced by a CI scanner. |
| `gstack-ios-qa-daemon` | **iOS QA daemon** — Mac-side broker between an agent and a connected iPhone or iPad over USB CoreDevice. Loopback by default; `--tailnet` opens a Tailscale-facing listener with identity-gated capability tiers. Single-instance via flock on `~/.gstack/ios-qa-daemon.pid`. See [docs/howto-ios-testing-with-gstack.md](howto-ios-testing-with-gstack.md). |
| `gstack-ios-qa-mint` | **iOS allowlist manager** — owner-grant CLI for the tailnet allowlist. `grant`/`revoke`/`list` against `~/.gstack/ios-qa-allowlist.json` (mode 0600). Remote agents never auto-allowlist; this is the explicit-intent path. |
+272 -19
View File
@@ -2087,6 +2087,29 @@ override is never cached.
`gstack-gate-diff` records a candidate ending in a tree id instead of
`+nowtree`.
<a id="gate-error-no-base"></a>
### `GATE_ERROR=no_base`
**Meaning.** `gstack-gate-diff` could not resolve the base it diffs against
(`GATE_ERROR=no_base ref=<ref> fix=<git command>` is its only stdout line,
exit 2). The ref is not a commit in this checkout: a shallow clone (CI's
default `fetch-depth: 1`), a remote branch never fetched
(`origin/main` with no `refs/remotes/origin/main`), or a `--commit` whose
first parent is missing from a shallow history. The tool never guesses a
base, because an empty diff against a wrong or absent base would read as "no
gate edits" and pass review. `gstack-diff-scope` prints `SCOPE_ERROR=no_base`
(all flags false, exit 2) for the same cause.
**Fix.** Run the command the line prints. It is built for this checkout:
`git fetch origin +<branch>:refs/remotes/origin/<branch>` for an unfetched
branch, `git fetch --unshallow origin <refspec>` or
`git fetch --deepen=50 origin` when the clone is shallow. In a workflow, check
out with `fetch-depth: 0` (or fetch the base explicitly) before the step that
runs `gstack-gate-diff` or `gstack-diff-scope`.
**Expected result.** `gstack-gate-diff <base>` prints `GATE_SUMMARY:` and
`gstack-diff-scope` prints its `SCOPE_*` flags without an error line.
---
## Memory and gbrain
@@ -2330,6 +2353,60 @@ release of this repository (or the post ran outside the checkout).
**Fix.** Write it as `version: 1.2.3.4`, `"version": "..."` or `v1.2.3.4`.
MEDIUM findings do not block pushes.
<a id="redact-run-id-as-phone"></a>
### `pii.phone.e164` MEDIUM on a receipt token or a GitHub Actions run id
**Meaning.** An all-digit identifier reads as a phone number: a 12-hex receipt
sha that happens to be all digits (a `tree`, `head`, `gated_tree` or
`predecessor` value), or an 11-digit GitHub Actions run id written somewhere
`gstack-post` cannot tell from a number. `gstack-post` allowlists only the
exact tokens it finds in these positions:
- a 12-digit value right after `tree`, `gated_tree`, `head` or `predecessor`
with `=` or a space (`tree=704992822996`, `tree 704992822996`);
- an 11-digit id inside an `/actions/runs/<id>` URL or as that link's text;
- an 11-digit id right after a run word on the same line, with an optional
`:`, `#` or `=` between: `run`, `runs`, `screen`, `screens`, `gate`,
`gates`, `backstop`, `bisect`, `rerun`, `dispatched`, `dispatch`,
`failed`, `passed` (`failed 40269992077 | rerun passed 40287239206`,
`gate 40323989742`, `run #19368712345`); a generic word such as `id`,
`job`, `attempt` or `workflow` exempts nothing;
- every 11-digit id chained after an allowlisted one through `, `, ` and `,
` & `, ` or ` or ` / `, optionally with a `(#1234)` between
(`screen 40269992077 (#3434) and 40275443393 (#3453)`).
A bare 11-digit number with none of that context, a 10-digit national number
and an E.164 `+14155551234` still report, so the finding means the id stands
without a run word (`the id is 40269992077`, `40269992077 failed`) or follows
a word that is not one (`see 40323989742`).
**Fix.** Write the id next to a run word (`run 40269992077`) or as an
`/actions/runs/` link. For anything else, rerun with `--confirm <token>`
using the `TOKEN:` line the refusal printed; MEDIUM findings do not block
pushes.
<a id="gstack-post-pr-body-merge"></a>
### `gstack-post pr-body` kept sections the body file did not contain
**Meaning.** `pr-body` merges the file into the PR's current body by `## `
section instead of overwriting it: a section in the file replaces the
same-titled section (title compared case-insensitively), the
````gstack-ship-receipt```` block is a section of its own so a receipt
rewrite replaces only the receipt, sections the file does not rewrite stay in
place, new sections are appended after them, and a current body with no
`## ` headings is kept as the leading text. One stderr line names what was
replaced, kept and added (`gstack-post: pr-body merged by section: replaced
What changed; kept ship receipt, Why, Approvals; added Tests`). A body file
holding only its first section therefore no longer wipes the rest of the PR.
The redaction scan runs on the merged bytes, so a MEDIUM or HIGH finding can
come from a kept section; the `RULE:` line's `LINE:` is in the merged text.
When the current body cannot be read (`gh pr view --json body` fails) nothing
is posted, exit 3.
**Fix.** Nothing, when the kept sections are wanted. To overwrite the whole
body with the file, rerun with `--replace`. To drop a section, post the body
with `--replace` or edit it on the host.
<a id="redact-prepush-email"></a>
### `MEDIUM pii.email <file>:<line>` from the pre-push hook
@@ -2566,7 +2643,9 @@ or corrupt object) that `git ls-tree` cannot confirm is absent; it never falls
through to the user-owned file. It also refuses when Bun loaded a `.env` file
from the working directory (a bare `bun gstack-pregate`), since a branch could
then pick the user policy: run `gstack-pregate` directly or as
`bun --no-env-file`.
`bun --no-env-file`. `gstack-ship-queue ready` under a train policy fetches
`origin/<base>` first and refuses with this code, quoting git's error, when
that fetch fails.
**Fix.** `git fetch origin <base> --depth=1`, then rerun. To run against the
branch's own file deliberately, pass `--policy-from head --allow-repo-commands`;
@@ -3974,7 +4053,9 @@ time; `gstack-ship-policy validate` prints `MIN_READER:` for exactly that.
coordinator could misread: the working tree has uncommitted tracked changes,
HEAD is not the PR's pushed head (read through `gh pr view`), CI at that
exact head is not green (failed, pending or unreadable), or the pregate result
is missing, failed or recorded on another tree (`PREGATE_STALE`).
is missing, failed or recorded on another tree (`PREGATE_STALE`). Under
`--dispatch-ci` it is also a dispatched workflow run that concluded red (a red
run clears no pregate lane) or a `gh workflow run` that failed.
**Fix.** Commit and push, wait for CI on that head, rerun
`gstack-pregate run --allow-repo-commands` when the tree changed, then rerun
@@ -3988,8 +4069,13 @@ which line is not yet true.
input does not match: the gated head is not a commit here, HEAD differs from
the gated head beyond stamps (the gate never covered this tree), an earlier
stamp with other inputs is unfinished, the stamp journal's head moved, or a
gate-ahead merge could not be recomputed with `git merge-tree`. Nothing was
written unless the `QUEUE_STAMP_REFUSED:` line says `committed=yes`.
gate-ahead merge could not be recomputed with `git merge-tree`. The train
commands use it too: `train forget` on a record that does not exist or on a
train still building, running or merging, `train watch` on an unknown record,
`verify --pr <n> --screen` on a run with no verdict to read (an INCOMPLETE or
unreadable run), and `ready --screen` when the scratch merge branch cannot be
pushed. Nothing was written unless the `QUEUE_STAMP_REFUSED:` line says
`committed=yes`.
**Fix.** Supply the named input (fetch the gated head, pass the current base
sha), or regate the tree you mean to stamp. For an unfinished stamp, rerun the
@@ -4064,7 +4150,8 @@ a `train` block with `min_gstack_version` on the base branch first
**Meaning.** A pinned PR's head repository is not the base repository. The
train branch and its dispatches run with repository secrets, so fork work
never rides a train.
never rides a train. `ready --dispatch-ci` refuses a fork head for the same
reason and dispatches nothing.
**Fix.** Run the printed `train build --prs <list without it>`; the fork PR
stays on the trusted-branch path.
@@ -4128,7 +4215,11 @@ one-PR train.
### `QUEUE_TRAIN_PENDING`
**Meaning.** The train's run (free tests, the full gate or the bisect) has
not finished, so there is no verdict yet.
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:`.
**Fix.** Rerun the printed `NEXT:` command later. Nothing is dispatched again
while a run is pending.
@@ -4266,10 +4357,14 @@ to fix this: the stamp comes only when the slot is called.
<a id="queue-screen-stale-ref"></a>
### `QUEUE_SCREEN_STALE_REF`
**Meaning.** `gstack-ship-queue ready --screen` dispatches the screen against
GitHub's `refs/pull/<n>/merge`, and that ref is missing or still merges an
older head than the PR's pushed head. GitHub stops recomputing the merge ref
while the PR conflicts with the base (a CHANGELOG-only conflict after a stamp
**Meaning.** `ready --screen` refuses with this code when the PR conflicts with
the base beyond the CHANGELOG and the stamp files (the message lists the
conflicting paths) or when it cannot list the remote's refs. A missing or
lagging merge ref alone no longer refuses (see Fix). The rest of this
paragraph is why such a ref cannot be screened directly: `ready --screen`
dispatches the screen against GitHub's `refs/pull/<n>/merge`, and that ref can
be missing or still merge an older head than the PR's pushed head. GitHub
stops recomputing the merge ref while the PR conflicts with the base (a CHANGELOG-only conflict after a stamp
counts), so a screen dispatched there checks out the old merge, sees itself
superseded, plans 0 cases and reports success. GSTA-95 hit this on #3431:
`ready` printed `SCREEN_DISPATCH:` as if a screen ran. `ready` now compares
@@ -4285,16 +4380,113 @@ read (`… paid-plan artifact has expired`, `… manifest.json is unreadable`, a
unreadable run or artifact list, an unreadable repository slug, or a READY
whose run id is not a number) keeps the slot screened and prints
`SCREEN: #<n> <reason>; counted as screened, unverified`; `ready --screen`
follows such a run. When `ready` cannot list the remote's refs or fetch the
merge ref at all, it refuses with the same code and a `could not list …` or
`could not fetch …` message whose NEXT line says to check the remote, not to
resolve a conflict.
follows such a run. When `ready` cannot list the remote's refs, it refuses with
the same code and a `could not list …` message whose NEXT line says to check
the remote, not to resolve a conflict; a merge ref that lists but cannot be
fetched is replaced by the scratch merge described under Fix.
**Fix.** Resolve the conflict with the base branch (or wait for GitHub to
recompute the merge ref), push, and run `gstack-ship-queue ready --pr <n>
--screen` again. Under crew rule 14 a CHANGELOG-only conflict is left to the
train, so a lane that cannot get a fresh merge ref READYs `--no-screen` only
after its own focused paid cases passed (rule 6).
**Fix.** Since the scratch-merge screen landed, a lagging merge ref is no
longer a refusal: `ready --screen` builds the merge of the PR head onto
`origin/<base>` itself with the train's merge rules (version files take the
base's side, the CHANGELOG merges by section), pushes it as
`screen/<pr>-<head12>` and dispatches the screen on that commit, printing
`SCREEN_MERGE: scratch merge …`. The code now means a real conflict: the
message lists the conflicting paths. Resolve those with the base branch,
push, and run `gstack-ship-queue ready --pr <n> --screen` again. A
CHANGELOG-only or stamp-file-only conflict does not refuse, unless the PR's
CHANGELOG drops or renames a released heading (the message then names the
CHANGELOG). `train prune --stale` deletes the scratch branches of merged or
closed PRs.
<a id="queue-screen-red"></a>
### `QUEUE_SCREEN_RED`
**Meaning.** The screen run dispatched for this PR head finished RED. The
verdict is read from the run itself, never from a lane's report: the
`gate-binding-a<attempt>` artifact evals.yml's report job uploads (verdict,
reconcile exit, flaky cases) and the `report-verdict-a<attempt>` artifact's
`collector-outcomes.json` (the panels that failed the lane and the rule-16
flaky lines). A case the binding calls red already failed twice inside the
run (rule 16 reruns a first failure once), so the red is real. `ready`
refuses with this code when the head's recorded screen run is red, whichever
of `--screen` or `--no-screen` it is given, and never dispatches another
screen at the same merge SHA; `train build` refuses to seat the pin and its
`NEXT:` names the build without it; `verify --pr <n> --screen` prints
`SCREEN_VERDICT: #<n> run <id> RED red=<cases> flaky=<cases>` and one
`FLAKY:` line per flaky case. The verdict is recorded in the READY receipt
(`screen.verdict`), locally and in `refs/notes/gstack-trains`.
**Fix.** Read the red cases from the `SCREEN_VERDICT:` line, repair, push a
new head, run /ship and `gstack-ship-queue ready --pr <n> --screen` again
(the new head gets its own screen). A GREEN screen with flaky cases is green:
the flaky cases are listed for the ledger and need no rerun.
<a id="queue-train-stack-red"></a>
### `QUEUE_TRAIN_STACK_RED`
**Meaning.** `gstack-ship-queue train build --dry-run --stack-tests` ran a PR's
new test files (files the PR adds relative to the base whose name ends in
`.test`, `_test`, `.spec` or `_spec` plus a JS or TS extension, bun's own
rule; a helper or fixture under `test/` is not one) on the integrated prefix that includes it,
T<k> (the base plus every slot up to and including that PR), in a scratch
worktree, and they failed. The `STACK_TESTS: #<pr> T<k> <n> new test file(s)
FAIL (<files>): <failing assertion>` line names the files and the runner's
failing lines. Those tests pass on the PR head alone only because nothing
before it in the train had changed what they read (a PR stacked on another
PR's test files or fixtures hit this on 2026-10-26). Nothing was written,
pushed or dispatched; the ordinary dry run (without `--stack-tests`) does not
run them.
**Fix.** Fix the PR's tests against that prefix, or reorder the train with
`--order` so the PR they depend on comes first, then rerun the dry run. Pass
`--stack-tests-command "<cmd>"` when the repo's tests run under something
other than `bun test`; the files are appended to the command.
<a id="queue-post-merge-red"></a>
### `QUEUE_POST_MERGE_RED`
**Meaning.** `gstack-ship-queue verify --post-merge <sha>` (the full 40-character
SHA) watched the check runs attached to a commit the train merged onto the
base branch (the push workflows: free tests, the Windows subset, the main
backstop) and its commit statuses until none was still running, and at least
one check concluded `failure`, `timed_out`, `cancelled` or another non-success,
or a commit status failed (the message then says a commit status, not a check
run). A `neutral` or `skipped` check (the
backstop superseding itself when main moved on) is counted as skipped, never
red. The command keeps no process state: it polls gh until the checks settle
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.
**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
backstop filed one.
<a id="queue-screen-not-run"></a>
### `QUEUE_SCREEN_NOT_RUN`
**Meaning.** The screen run dispatched for this PR concluded `success` but
ran no slice. evals.yml's `route` job plans nothing when
`scripts/eval-readiness.ts wait` finds the PR head superseded (a push landed
between `ready --screen` and the plan) or when the plan selects no case; the
run then uploads no `paid-plan` artifact (or an empty manifest), every slice
job is skipped and GitHub shows a green check. On 2026-10-26, 18 of 41 PR
screens ended this way. The verdict is read from the run (`lib/ship-queue-screen.ts`),
so such a run is `NOT-RUN`: `verify --pr <n> --screen` prints
`SCREEN_VERDICT: #<n> run <id> NOT-RUN (<why>)` and refuses with this code, the
READY receipt records `screen.verdict.outcome: NOT_RUN`, `ready --screen` never
follows the run (its `SCREEN_DISPATCH:` line says `not following … a screen
that planned nothing is not a screen`) and `train build` seats the pin unscreened
with `SCREEN: #<n> run <id> planned no cases …; counted as not-run, the slot
rides unscreened`.
**Fix.** `gstack-ship-queue ready --pr <n> --screen` at the current head
dispatches a fresh screen. A head that moved since the dispatch needs a fresh
READY anyway (`QUEUE_TRAIN_MOVED`); a head that did not move and still plans
nothing has nothing diff-relevant to screen, and `--no-screen` with the
focused cases named in the READY block is the honest choice.
<a id="ready-screen-choice-required"></a>
### `READY_SCREEN_CHOICE_REQUIRED`
@@ -4448,6 +4640,67 @@ conflict-only base merge, a test-only change or release metadata, 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="gstack-pr-wait"></a>
## gstack-pr-wait
`gstack-pr-wait <pr> [--repo owner/name] [--until green|ready|any-change]
[--interval SECONDS] [--timeout MINUTES]` watches one pull request and prints
one stdout line per real event, prefixed `#<pr>`; progress and recoverable
errors go to stderr. `/land-and-deploy` Step 3 runs it with `--until green`.
Exit 0 means the `--until` condition is met or the PR merged; exit 1 and 2
end with one of the two codes below. `green` gates on every check on the
head, as `gstack-ci-gate` does, so the two never disagree; a check the base
branch requires (rulesets and classic branch protection) counts as pending
until it appears. A 403 on the classic
protection read (a token without admin scope) is logged and skipped, and the
rulesets read still names the required checks. The review-bot lists are
configuration: `gstack-config set pr_wait_gating_bots greptile,macroscope`
and `pr_wait_advisory_bots coderabbit` (`none` empties a list); a bot owns
the logins that start with its name and the checks that mention it, and an
advisory bot's review is never waited for while its checks gate like any
other. The loop
is synchronous, so Ctrl-C and `kill` stop it at once; the lock it leaves is
taken over by the next watcher for that PR once the pid is gone.
<a id="pr-wait-blocked"></a>
### `#<pr> stopped: action needed (PR_WAIT_BLOCKED)`
**Meaning.** Exit 1. The condition cannot be met without a change to the PR:
the lines above it say which. A check failed (name and link; `required check
failed` when the base branch requires it); the merge state is DIRTY (the branch
conflicts with its base); the PR was closed without merging; `--until ready` is
blocked by unresolved review threads or a BEHIND branch; GitHub stayed
unreachable for ten polls, or answered with a non-transient error
(`gstack-pr-wait stopped: <gh message>`); or `--timeout` passed (default 60
minutes) with the condition unmet (`gstack-pr-wait stopped: no green after 60
minutes; still pending: <checks>`).
**Fix.** Act on the named line: fix the failing check and push, rebase or
update the branch, resolve the threads, or read the gh message. After a
timeout, decide whether the pending checks are worth waiting for and start
again, with `--timeout 0` to wait without limit. Then start `gstack-pr-wait`
again; its state file remembers what it already reported, so only new events
print. A watcher that stopped on a gh error restarts cleanly once `gh auth
status` passes.
<a id="pr-wait-refused"></a>
### `gstack-pr-wait: the watcher did not start, or a local error stopped it before a verdict — ... (PR_WAIT_REFUSED)`
**Meaning.** Exit 2, never a verdict on the PR. The target is not a PR number
or URL, `--repo` is not `owner/name`, no repository could be read from the
checkout, the PR cannot be read with the current `gh` login, an option is
unknown, another `gstack-pr-wait` process (the line names its pid) already
watches this PR from the same state root, so a second one would print
duplicate events, or a local error (`a local error stopped the watch before a
verdict: <message>`, an unwritable state root for example) ended the process.
Exit 1 is reserved for `PR_WAIT_BLOCKED`, so a local fault never reads as a
PR that needs action.
**Fix.** Pass the number or URL and `--repo owner/name`, check `gh auth
status`, fix the named local error, or let the running watcher finish. If no
events reach you from the named pid, `kill <pid>` and start again; a lock
whose pid is no longer alive is taken over without that step.
## gstack-error-capture
`gstack-error-capture` runs one target CLI for `/devex-review` Step 3 with no