v1.91.374.0 stamp: train gate.train-395971298ca4-0cd2b4a9f944-22db37 S1 (#3611)

This commit is contained in:
garrytan committed 2000-01-01 00:00:00 +00:00
commit 4bc8d9c73f
61 files changed
+3091 -147

No files matched your search

+68 -3
View File
@@ -197,7 +197,15 @@ shards get duration-aware walls (`max(base, predicted × 3, files × 5s)`). The
legacy `--shards N --shard i` path keeps stable hash indices. Required CI uses
one duration-packed `--ci-plan`, 20 ordinary `--ci-run` shards plus a separate
exclusive-fixture shard, and a `--ci-verify` aggregate. CI shards still run on
independent machines without ordering unrelated jobs. The public
independent machines without ordering unrelated jobs. Two workflow-level
constants in `free-tests.yml` set that size for every event: `FREE_SUITE_RUNNER`
(`ubicloud-standard-2`) and `FREE_SUITE_SHARDS` (`'20'`), which `free-plan`
resolves into the shards' runner and shard count. A `workflow_dispatch` run may
override them with `free_runner` (`ubicloud-standard-2`, `-4` or `-8`) and
`free_shards` (1–40) for runner-sizing experiments only; its job summary says
so, and it is never cited as a PR or train Free Tests result. Each shard prints
`RESOURCES: peak_used_mib=… min_free_disk_mib=… samples=…` from a memory and
disk sample every 2 seconds, even when the suite fails. The public
`TREE_MUTATING` map now classifies exclusive host-state fixtures; its sole
entry is `test/bootstrap-retention.test.ts`, whose same-UID nondumpable actors
affect host-wide procfs permission checks. Locally, this file runs only after
@@ -1274,7 +1282,10 @@ re-run keeps the creation time, so it does not wait out the window again). A
head that is no longer the PR's head reports "superseded by <sha>" and plans
nothing. Selection, trials and verdict rules do not change. Version Gate is not
an input, so a stamp push waits only for its own Free Tests and always gets a
gate. Dispatches (`eval:bg`, the census) never wait.
gate. Dispatches (`eval:bg`, the census) never wait. Neither does a
`pr-summary` run (a repository whose policy declares a train): it plans no paid
slices, so `readiness` is skipped, the run ends with the screening summary, and
a red Free Tests shows on Free Tests alone.
**Verdict reuse (R2a restamp-only, R2 main moved; a gate edit).** A PR head
reuses the verdict of the newest executed gate of the same PR instead of
@@ -1424,6 +1435,13 @@ 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`).
Other repositories get the same rule per test with `bin/gstack-flaky run`
([docs/flaky.md](flaky.md)): it reads a JUnit report from any runner, reruns
the command once when tests failed (counting only the runner's full green
verdict), and writes `flaky-pass` and `rerun-fail` lines to its
own ledger (`flake-ledger-junit.jsonl`, or `GSTACK_FLAKE_LEDGER_JUNIT`), kept apart
from this file-level ledger.
The aggregate job of `free-tests.yml` downloads every `flake-ledger-*` artifact
of the run and appends `bun run scripts/test-health-report.ts flake-summary <dir>`
to its job summary. The Windows lane uploads `flake-ledger-windows-<shard>`.
@@ -1504,7 +1522,9 @@ refresh to a maintainer.
- `free-durations-record.yml` (Mondays 04:00 UTC, before test-health, and on
dispatch) records the Linux seed on `ubicloud-standard-8` with
`scripts/ubicloud/setup-free-suite.sh` and the Windows seed through
`windows-free-tests.yml`'s `record_durations` path. When either recording adds
`windows-free-tests.yml`'s `record_durations` path. The recorder stays on
`ubicloud-standard-8` while the free-suite shards run on `FREE_SUITE_RUNNER`
(`ubicloud-standard-2`); both are Ubicloud standard runners. When either recording adds
or drops a test file, it force-updates the `bot/free-duration-seeds` branch,
opens or refreshes one seed PR whose body states the ratchet result, and
dispatches Free Tests, Windows Free Tests and Quality gate on it. Those
@@ -1624,6 +1644,51 @@ lower-case words in path components under `test/fixtures/`; a substring match
separate change with its own touchfile updates; the list is here so that change
can be scoped.
## No-gstack arm
Garry, 2026-10-31: "all of our head to head tests have to test no gstack from
here so that that case is tested obviously Do it for science". A variant that
beats another variant can still lose to the plain model, and only a no-gstack
arm shows that.
**Scope.** Every preregistered comparison where any arm loads gstack or another
skill stack: review variants, preamble loadouts, skill diets scored by behavior
evals, and model panels run under gstack or under another stack. The
preregistration says which kind it is in `## Comparison`: `stack: loaded`, or
`stack: none` with a `why:` line (a scorer calibration, for example, where no
arm loads any skill text). `gstack-eval-plan prereg --check` refuses a loaded
comparison without the arm (`PREREG_NO_GSTACK_ARM`).
**How the arm is built.** One arm per model in the comparison, named
`<model>#no-gstack` in the Arms section and listed on the `arms:` line of
`## No-gstack arm`. Arm identities are `<model>[#<loadout>]` and are priced by
the model, so the pilot reserves the no-gstack arm beside its variants under its
own attempts. The arm gets:
- the same model, settings, inputs, fixture and tools as the variants;
- a plain prompt: the task text only, with no SKILL.md, preamble, hooks,
installed skills or routing lines from gstack or any other stack;
- the same panel: the same `E2E_KINDS` kind and trial count, the same judge and
rubric, and the same blinding labels (the judge reads it as `A` or `B` like
any other arm, per [Candidate-side blinding](#candidate-side-blinding));
- the same budget treatment: it is priced and reserved in the pilot like every
other arm.
**Reporting.** The no-gstack result is one row beside the variants in every
results table and every verdict, including when it wins. A comparison that
drops the row has not reported its result. The prereg check cannot see the
report, so the `report:` line states where the row goes and the reviewer of
the result holds the author to it.
**Precedent and its gaps.** `test/helpers/arm-benchmark-harness.ts` already has
a `without-skill` arm beside `with-skill`, and it is the closest existing
control. It falls short of this rule in three ways: each arm and task runs
once rather than as a panel; the judge (`armJudge` in
`test/helpers/llm-judge.ts`) scores each diff on its own, without `A`/`B`
labels; and the candidate's temporary folder is named after the arm
(`arm-<fixture>-<arm>-*`). Existing paid cases are not retrofitted by the
change that added this rule; an audit of them is tracked separately.
## Running evals as an agent: detach
Moved verbatim from CLAUDE.md (#2096 size limit). The short rule stays in
+137
View File
@@ -0,0 +1,137 @@
# Flaky tests: rerun once and record it (`gstack-flaky`)
`gstack-flaky run` runs your test command, reads the JUnit report it writes,
and when tests failed, runs the command once more. A test that passes on its
rerun is a **flaky pass**: the job stays green and the flake is recorded on a ledger, so
the daily flaky pass can fix it at the source. A test that fails twice is red.
This is CREW rule 16 ("run the failed test again and note that it is flaky")
for any runner that writes JUnit: Playwright, `bun test`, jest, vitest and
pytest. gstack's own free suite keeps its file-level retry
(`GSTACK_FREE_RETRY_FLAKY`, [Flake ledger](TESTING_INTERNALS.md#flake-ledger)).
## Get started in GitHub Actions
```yaml
- uses: oven-sh/setup-bun@v2
with: { bun-version: 1.4.4 }
- name: Install gstack-flaky (pinned)
env: { GSTACK_REF: <a gstack commit sha you have tested> }
run: |
git clone --quiet --filter=blob:none https://github.com/garrytan/gstack.git "$RUNNER_TEMP/gstack"
git -C "$RUNNER_TEMP/gstack" checkout --quiet "$GSTACK_REF"
- name: E2E
env:
PLAYWRIGHT_JUNIT_OUTPUT_NAME: results/junit.xml
GSTACK_FLAKE_LEDGER_JUNIT: ${{ runner.temp }}/flaky-junit-ledger.jsonl
run: |
"$RUNNER_TEMP/gstack/bin/gstack-flaky" run --junit results/junit.xml -- npx playwright test --reporter=line,junit
- if: always()
uses: actions/upload-artifact@v4
with: { name: flaky-junit-ledger-e2e, path: "${{ runner.temp }}/flaky-junit-ledger.jsonl", if-no-files-found: ignore, retention-days: 90 }
```
Replace `GSTACK_REF` with a gstack commit you have tested; the bin needs only
Bun and git. In Actions it also writes a step-summary table of the flaky
passes and one `::warning` annotation per flaky test, so they are visible on
the run before anyone reads the ledger.
## Runners
| Runner | Command after `--` | Notes |
|---|---|---|
| Playwright | `npx playwright test --reporter=line,junit` with `PLAYWRIGHT_JUNIT_OUTPUT_NAME=<file>` | `gstack-flaky` sets `PLAYWRIGHT_JUNIT_INCLUDE_RETRIES=1` so Playwright's own `retries` are reported; with retries configured, those attempts stand and nothing is rerun again. |
| bun | `bun test --reporter=junit --reporter-outfile=<file>` | |
| jest, vitest, pytest (`--runner generic`) | your command, writing JUnit | Always reruns the whole command, even with `--rerun-scope test`. |
The command after `--` runs without a shell. When it needs one, wrap it as
`bash -c '<script> "$@"' _` so a targeted rerun's selection arguments reach
the runner.
## Rerun scope
**Default: the whole command.** The rerun repeats your command exactly, and
counts only on the runner's own full green verdict: it exits 0, no test fails,
every test of the first run is in its report, and the runner counts no failure
outside the report. That verdict covers every reason a run can fail, including
the ones a narrowed rerun cannot see: a test the first run never reached
(`--bail`, `--max-failures`, `-x`), a coverage threshold (however coverage is
configured, `bunfig.toml` and lcov included), a fail-on-flaky policy, a
global setup error. A test that failed in the first run and passes once in this
rerun is a flaky pass. It costs one more run of the suite, which is still far
cheaper than rerunning the CI job by hand.
**Opt-in: `--rerun-scope test`.** Reruns the failed test alone (Playwright:
`<file> --grep <title> --project <project>`; bun: `<file> --test-name-pattern <title>`)
and clears it when that rerun exits 0. Use it only when the first run's exit
status can come from failed tests alone: no coverage threshold in any form, no
`--fail-on-flaky-tests`, no other step inside the command that can fail. As a
safety net, `gstack-flaky` still reruns the whole command
(`FLAKY_RERUN: scope=command (<why>)`) when the runner exited anything but
1, its summary is missing, it stopped early (bun `Bailed out`, Playwright
`did not run`, `interrupted`, `Testing stopped early`, a global timeout), it
printed a coverage table or got `--coverage`, or it retried flaky tests itself.
It cannot see a coverage threshold configured without console output (for
example `coverageReporter = ["lcov"]` in `bunfig.toml`), which is why this
scope is opt-in.
## Rules
- **One rerun, small failures only.** A failed test gets one rerun when the
runner made no retries and at most `--max-reruns` (3) tests failed. More
failures than that is a real red, and nothing is rerun. When the run is
already red for a test that cannot be rerun, a whole-command rerun is
skipped.
- **A rerun clears a test only cleanly.** The rerun's command must exit 0 and
the test must run once and pass on its first attempt; a rerun the runner
retried again does not count.
- **Never green by accident.** The report is deleted before every attempt, and
a report that attempt did not write counts as missing. A missing, empty,
zero-test or malformed report fails, as does a non-zero exit with no failed
test (a crash or a non-test step) and a runner killed by a signal. Cancelling
`gstack-flaky` (SIGINT, SIGTERM) is passed to the runner and latched: nothing
is rerun after it, whatever exit the runner chooses. When the runner's
summary counts failures its JUnit omits (a bun file that fails to load, a
Playwright error outside any test), the run is red and nothing is rerun.
- **Reports.** After the run, `--junit` holds the first run's report, so a
later upload or test reporter sees what failed. Each rerun's report is kept
beside it as `<name>.rerun-<n>.xml`. Other reporters' outputs (Playwright's
`test-results/` and HTML report) are rewritten by a rerun; upload the JUnit
report as the record. An existing `--junit` file that is not a JUnit report,
or cannot be read, is refused (usage, exit 2), never deleted.
- **Ids.** A test is `<file>::<project>::<describe > title>`, with the project
empty outside Playwright, so a test in two browser projects is two tests.
`gstack-flaky ids --junit <file>` prints them.
- **Ledger.** One JSON line per flaky pass (`kind: flaky-pass`) and per test
that failed its rerun (`rerun-fail`), with the tree, commit, branch, run URL,
attempts, the rerun's scope (`test` or `command`) and the first lines of
the error. Ids, paths and failure text go through gstack's redaction engine
first; when the scan cannot run, they are withheld. The path is `--ledger`,
else `GSTACK_FLAKE_LEDGER_JUNIT`, else
`<state root>/flake-ledger-junit.jsonl`, and `FLAKY_RESULT` names it. It is
kept apart from the free suite's file-level ledger (`GSTACK_FLAKE_LEDGER`).
A ledger write error is a warning and never changes the verdict.
## Output and exit codes
One `FLAKY: <flaky-pass|fail|rerun-fail> <id> attempts=<…> (<reason>)` line per
test that failed at least once, `FLAKY_RERUN: scope=command (<why>)` when the
rerun repeated the whole command, then
`FLAKY_RESULT: tests=<n> failed=<n> flaky=<n> reruns=<n> command_exit=<n> verdict=pass|fail ledger=<file|none>`.
| Exit | Meaning |
|---|---|
| 0 | Every test passed, flaky passes included |
| 1 | A test failed (after its rerun, or with no rerun), including `FLAKY_UNREPORTED_FAILURES` |
| 2 | Usage error, including a `--junit` path that is not a `.xml` file, or an existing file there that is not a readable JUnit report |
| 3 | No usable report, a crash, or a killed runner (a code below on stderr) |
| Code | Exit | Cause | Fix |
|---|---|---|---|
| `FLAKY_NO_RESULTS` | 3 | No report at `--junit`, an empty one, or one with no testcase | Make the command write JUnit to that path |
| `FLAKY_JUNIT_INVALID` | 3 | The report is not well-formed XML | Make sure one process writes it and nothing appends to it |
| `FLAKY_RETRIES_HIDDEN` | 3 | Playwright retried but its report omits the retries | Set `PLAYWRIGHT_JUNIT_INCLUDE_RETRIES=1` where the report is written |
| `FLAKY_COMMAND_FAILED` | 3 | Non-zero exit with no failed test | Read the command output: a real red |
| `FLAKY_COMMAND_KILLED` | 3 | The runner was killed by a signal (a cancelled job, an OOM kill) | Nothing was rerun; rerun the job |
| `FLAKY_COMMAND_NOT_STARTED` | 3 | The command could not be started | Check the argv after `--` |
| `FLAKY_UNREPORTED_FAILURES` | 1 | The runner counted failures or errors its JUnit omits | Fix the load error or the error outside any test: a real red |
| `FLAKY_LEDGER_WRITE_FAILED` | — | The ledger could not be appended (warning) | Check the path; the verdict stands |
+9 -3
View File
@@ -85,7 +85,7 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`).
| `serial_patterns` | test-file globs the `lanes` check runs one process per file, apart from the unit files (default `**/*.serial.test.*`) |
| `sealed` | sealed or eval output that must never reach a public destination; read by the pre-push hook and `gstack-pregate sealed` (see [sealed output](#sealed)) |
| `price_file` | repo-relative path of the paid gate's call-plan data file (`GSTACK_EVAL_CALLS v1`); `gstack-eval-plan price-gate` prices from it and never executes anything ([docs/eval-calls.md](eval-calls.md)) |
| `stall_timeout_s` | seconds a test command may print nothing before its process group is killed and the check is `incomplete` (`PREGATE_STALLED`); default 300, `0` disables; `--stall-timeout` overrides (see [supervision](#supervision)) |
| `stall_timeout_s` | seconds a test command may print nothing before its process group is killed and the check is `incomplete` (`PREGATE_STALLED`); default 300, `0` disables; `--stall-timeout` overrides; the `--remote` gate is exempt (see [supervision](#supervision)) |
| `timeout_s` | seconds each runner process may take (the unit-file group, each serial file, each guard without its own `timeout_s`, and the remote gate's deadline) when `--timeout` is not passed; 1 to 3600, default 600. `gstack-ship-preflight` and the `finish` fallback run pregate without `--timeout`, so this is how a repo with long lanes sets theirs |
| `remote_gate` | the repo's remote test gate command; under 8 cores the local test tier becomes a `requires-remote` obligation naming it, and `--remote` runs it (see [remote gate](#remote-gate)) |
| `allowlist` | `[{ finding, paths?, pattern?, reason }]`: benign findings the `secrets` check accepts and names as allowlisted, by gstack-redact finding id or non-secret category, path globs and a pattern anchored at the finding; never `env.value` (see [allowlist](#allowlist)) |
@@ -411,7 +411,9 @@ test-tier command:
- is killed as **stalled** when its log does not grow for the stall timeout
(`--stall-timeout <s>`, else `stall_timeout_s`, else 300; `0` disables): the
check is `incomplete` with `PREGATE_STALLED` and names the last file
started; the next serial file still runs;
started; the next serial file still runs. The `--remote` gate is exempt: a
gate that waits on CI is quiet by design, so only its timeout bounds it, and
its heartbeats add `; last output: <the gate's last line>`;
- is killed as **timed out** when that runner process reaches its timeout
(`--timeout <s>`, else `timeout_s`, else 600; per runner process, so many
serial files that each fit still pass): the check is `incomplete`, its detail
@@ -452,7 +454,11 @@ the supervision above. Close the obligation one of two ways:
`PREGATE_REMOTE: tree=<tree> tests=<n> failed=<n> [spent=<usd>]`, reporting the
tree it actually tested. The obligation clears only when that tree is
`PREGATE_TREE`, `tests` > 0, `failed` = 0 and the gate exits 0; anything else
leaves it uncleared and the check `incomplete`. The cap defaults to
leaves it uncleared and the check `incomplete`. The gate runs until it exits
or reaches the timeout (`--timeout`, else `timeout_s`, else 600 s); the stall
timer does not apply, and each 60 s heartbeat names the gate's last output
line, so a gate that prints what it is waiting on (a CI run id and its
status) shows it. The cap defaults to
`$GSTACK_SPEND_CAP_USD`; there is no default spend. Spend prints as
`spent, reserved, unknown`: a gate that prints no `spent=` is `unknown`,
counted at the cap.
+16 -5
View File
@@ -473,7 +473,14 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
second line for the same field starts another: `Severity` raises its label,
`Blocks: yes` makes it CRITICAL and `Blocks: no` never lowers it; an inline
`Blocks: yes` is CRITICAL too, and with no finding above it is one. The native
adversarial voice's FIXABLE collects as CRITICAL (normalizer v2, GSTA-167). A phantom finding costs a fold; a lost one costs the gate.
adversarial voice's FIXABLE collects as CRITICAL (normalizer v2, GSTA-167). An unnumbered, unlabeled bullet with no file:line
under a paragraph finding whose Severity or Blocks line has not come yet is that finding's detail, so the field line after it
lands on the finding; a numbered item is its own finding.
A phantom finding costs a fold; a lost one costs the gate.
A reply with no finding counts as a review only with the clean marker its own brief asks for: `NO FINDINGS` alone on a
line for the specialists, the batched specialist, the targeted reader, the Red Team and fix-check, the `Recommendation:`
line for the outside voice, either for the native adversarial voice. Without one (a refusal, an empty reply, a reader
that never saw the diff) collect records `review: false`, prints `COLLECT_NOT_REVIEWED` and exits 1.
4. Lead judgment writes `judged.jsonl`; `judge` refuses a set that drops a merged
finding, lowers its severity, folds it into a less severe duplicate, or adds or
promotes one without a reason (`JUDGMENT_REJECTED`). A merged file missing any id
@@ -481,10 +488,14 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
5. `verify --ledger <f>` replays all of it from the run's ledger: rendered against
dispatched hashes per render (each render is one dispatch of its reader, so a
two-pass ledger checks pass-1 sends against pass-1 briefs; a later render prints as
`<reader>@2`), a collected output for every sent reader (`NOT_COLLECTED`
otherwise; a reader that failed is collected from its failure text, where any
label word becomes a finding the judge folds, and its coverage is logged
`unavailable` in the review-log row). A mismatched send re-sent
`<reader>@2`), a collected output that is a review for every sent reader (`NOT_COLLECTED`
otherwise, and the output row `NOT_REVIEWED`; the marker is checked again in the output file when it is unchanged, so an
older ledger meets the same rule; a non-review the same dispatch later replaced with a review, by re-sending the brief, is `VOIDED`
and counted in `voided=N`). The outside voice may fail without blocking: `collect --unavailable '<reason>'` records its failure text
as missing coverage (`COLLECT_UNAVAILABLE`); an empty reason or an output that holds findings is refused (exit 2), so a partial
answer is collected normally and loses nothing. Verify prints its brief and output `UNAVAILABLE` and `unavailable=N` on the
verdict line, and its coverage is logged `unavailable` in the review-log row; any other reader is refused (exit 2), because its
missing coverage leaves the review incomplete. A mismatched send re-sent
byte-exact before anything was collected is void and counted (`voided=N` on the
verdict line); one that produced an output stays `MISMATCH`. Then output hashes
against the merged lines they produced, and the judgment rerun against the
+1
View File
@@ -18,6 +18,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 (tracked and untracked files that are not gitignored, minus an untracked `.bun/` folder at the repo root, which Bun can create during a run); a run whose files changed while it ran is recorded without a fingerprint and with up to five of the changed paths, which `check` names in its STALE reason. `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-flaky` | **Flaky-test rerun for any JUnit runner** — `run --junit <file.xml> -- <command>` runs your test command (Playwright, `bun test`, jest, vitest, pytest) and, when at most `--max-reruns` (3) tests failed and the runner made no retries of its own, reruns the whole command once (`--rerun-scope test` opts in to rerunning only the failed tests). A test that fails and then passes on that rerun is a flaky pass: the run stays green and one redacted line per flaky pass or failed rerun goes on a ledger (`GSTACK_FLAKE_LEDGER_JUNIT`, default `<state root>/flake-ledger-junit.jsonl`). A missing or malformed report, a crash or a killed runner never passes. `ids --junit <file>` prints a report's test ids. Exit 0 pass, 1 a test failed, 2 usage, 3 no usable report. [Guide](flaky.md). |
| `gstack-pr-wait` | **PR event watcher** — `gstack-pr-wait <pr> [--repo owner/name] [--until green\|ready\|any-change] [--interval 20] [--timeout 60] [--exclude CHECK,...]` 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; `--exclude` leaves a named non-required check out of the verdict as `gstack-ci-gate --exclude` does (a required name is refused and gates). 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). |
+46 -12
View File
@@ -2522,12 +2522,15 @@ 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, and new sections are appended after them. The text above the first
`## ` heading belongs to the file whenever the file has a `## ` section: the
current body's text there (the opening lines, and any lines between the receipt
and the first heading, or the whole body when it has no `## ` heading) is
replaced by the file's own text above its first heading, or dropped when the
file has none. A file with no `## ` section (a receipt-only or text-only post)
leaves that text alone. One stderr line names what was replaced, kept, added
`## ` heading belongs to the file when the file is a full post: a
receipt block above its first `## ` section, as /ship composes it. Then the current body's text there (the opening lines, and
any lines between the receipt and the first heading, or the whole body when it
has no `## ` heading) is replaced by the file's own text above its first
heading, or dropped when the file has none. A partial post (`## ` sections
with no receipt above them, such as a lane posting `## Screenshots`) and a
file with no `## ` section (a receipt-only or text-only post) leave that text alone, so a human
intro or a `Review-loop exception:` line under the receipt survives; only
opening lines the file brings of its own replace the PR's opening lines. One stderr line names what was replaced, kept, added
and dropped, a dropped piece with its first line, or with that line withheld
when the redaction scan flags it (`gstack-post: pr-body merged
by section: replaced What changed; kept ship receipt, Why, Approvals; added
@@ -3464,8 +3467,7 @@ the lane log, then rerun or split the selection. A tool missing from PATH is
<a id="pregate-stalled"></a>
### `PREGATE_STALLED`
**Meaning.** A test command the lanes check (or a guard, or the `--remote`
gate) launched wrote nothing to its log for the stall timeout (default 300 s,
**Meaning.** A test command the lanes check (or a guard) launched wrote nothing to its log for the stall timeout (default 300 s,
`stall_timeout_s` in the base branch's `.gstack/pregate.json` or the user-owned file, or
`--stall-timeout`). pregate killed its process group and names the last test
file started. The check is incomplete, never pass.
@@ -3474,7 +3476,9 @@ file started. The check is incomplete, never pass.
child, a server that never exits, a synchronous wait), fix it and rerun
`gstack-pregate --stage tests`. A test that is legitimately quiet for longer
needs a larger `--stall-timeout`; `0` disables the stall kill and leaves only
`--timeout`.
`--timeout`. The `--remote` gate never stalls: it is bounded by `--timeout`
(else `timeout_s`), so a gate that waits on CI needs a timeout long enough for
the CI run.
<a id="pregate-receipt-invalid"></a>
### `PREGATE_RECEIPT_INVALID`
@@ -3640,14 +3644,44 @@ parentheses; grep the code.
### `PREREG_INCOMPLETE`
**Meaning.** `gstack-eval-plan prereg --check <file>` found a required
preregistration section missing or empty: `## Bars`, `## Arms`, `## Decoys`,
`## Stop rules` or `## Held-out exposure` (every held-out set the plan
touches, its prior exposure and the reservation on it).
preregistration section missing or empty: `## Bars`, `## Arms`,
`## Comparison`, `## Decoys`, `## Stop rules` or `## Held-out exposure`
(every held-out set the plan touches, its prior exposure and the reservation
on it). It also fires when `## Comparison` has no `stack:` line, a value other
than `loaded` or `none`, or `stack: none` without a `why:` line, and when a
`## Diagnostics` declaration is malformed.
**Fix.** Fill the named sections before any priced call. A plan that cannot
name its stop rule or its held-out exposure is not preregistered; the pilot
refuses to start without a passing check.
<a id="prereg-no-gstack-arm"></a>
### `PREREG_NO_GSTACK_ARM`
**Meaning.** The preregistration says `stack: loaded` (some arm loads gstack or
another skill stack) and its `## No-gstack arm` section is missing, lacks one
of `arms:`, `inputs:`, `prompt:`, `panel:` or `report:`, lists an arm that
is not a `<model>#no-gstack` Arms line, or leaves a model in the comparison
without its own no-gstack arm. Arms match on whole identities, so
`claude-opus-5#no-gstack` does not cover `claude-opus-5-5`.
**Fix.** Add one `<model>#no-gstack` line per model under `## Arms`, list them
on the `arms:` line, and fill the other four keys: the same inputs, a plain
prompt with no skill text, the same panel and judge, and where the result row
goes beside the variants. When no arm loads a skill stack, write
`stack: none` with a `why:` line instead. The rule is in
[docs/TESTING_INTERNALS.md](TESTING_INTERNALS.md#no-gstack-arm).
<a id="eval-arm-duplicate"></a>
### `EVAL_ARM_DUPLICATE`
**Meaning.** `gstack-eval-plan pilot start --arms` named the same arm identity
twice, so two arms would share one set of pilot attempts.
**Fix.** Give each arm its own identity: `<model>` or `<model>#<loadout>`, for
example `claude-opus-5-5` and `claude-opus-5-5#no-gstack`. Nothing was
reserved.
<a id="eval-price-missing"></a>
### `EVAL_PRICE_MISSING`