v1.93.37.0 stamp: merge main 5192908de4 into #3732 (land)

This commit is contained in:
garrytan committed 2026-11-03 02:09:10 +00:00
commit 377de6fcc2
192 files changed
+6013 -760

No files matched your search

+2 -2
View File
@@ -131,8 +131,8 @@ determined leaker (a CHANGELOG line that does would fail a hostile screenshotter
→ glab → unknown(=public-strict). Public repos get STERNER per-finding
confirmation (no batch-acknowledge, no silent-proceed); MEDIUM is never
auto-promoted to HIGH.
- **Tool-attributed fences:** wrap Codex/Greptile/eval output in ` ```codex-review `
/ ` ```greptile ` fences so example credentials those tools quote WARN-degrade
- **Tool-attributed fences:** wrap Codex/review-bot/Greptile/eval output in ` ```codex-review `
/ ` ```review-bot ` / ` ```greptile ` fences so example credentials those tools quote WARN-degrade
instead of blocking. A live-format credential inside the fence still blocks.
- **Config keys:** `redact_repo_visibility` (public|private|unknown, local-only
override for repos gh/glab can't read), `redact_prepush_hook` (true|false).
+24 -5
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; the `--remote` gate is exempt (see [supervision](#supervision)) |
| `stall_timeout_s` | seconds a test command may print nothing while using under a tenth of one CPU before its process group is killed and the check is `incomplete` (`PREGATE_STALLED`); default 300, `0` disables; `--stall-timeout` overrides; the `--remote` gate is exempt (see [supervision](#supervision)) |
| `timeout_s` | seconds each runner process may take (the unit-file group, each serial file, each guard without its own `timeout_s`, and the remote gate's deadline) when `--timeout` is not passed; 1 to 3600, default 600. `gstack-ship-preflight` and the `finish` fallback run pregate without `--timeout`, so this is how a repo with long lanes sets theirs |
| `remote_gate` | the repo's remote test gate command; under 8 cores the local test tier becomes a `requires-remote` obligation naming it, and `--remote` runs it (see [remote gate](#remote-gate)) |
| `run_profiles` | name → `{ workflow, events, jobs, artifact, max_age_hours?, os? }`: the GitHub Actions runs `clear --run` verifies as evidence on the identical tree; a profile named after a lane (`free-tests.yml/macos-named-regressions`) clears that lane (see [run evidence](#run-evidence)). `--from-run` and the main-push reuse job verify runs against them too ([from-run](#from-run), [CI reuse](#ci-reuse)) |
@@ -228,6 +228,18 @@ Two stages with separate clocks, both printed:
elsewhere in the file is not a consumer. A call-site name such as `attempts`
or `limit` is an ordinary word, so for it the file must also name the changed
module.
A test that keeps the old value on purpose, such as the refused input after a
minimum version goes up, marks that one line with a comment on it or on the
comment line directly above it:
`// pregate-literal: MIN_RUNTIME='1.4.0' refused input below the minimum`
(`#` and `/*` comments work too). The marker waives only that line, and only
when it names the changed constant and its old value and gives a reason (a
reason cannot start with `*`, so a bare `*/`, `**/` or `* */` closer is not
one); text inside a string is not a marker. Every other line in the file is still
checked. The check lists each waived line as
`waived: <file>:<line>: <NAME> <old> (<reason>)`, adding `marker added on
this branch` when the merge-base file lacks the marker. The policy has no
waiver key, so neither a base nor a pushed `pregate.json` can widen it.
- `timing`: added or changed test lines that compare an elapsed wall-clock or
CPU time against a bound, or sleep and then assert, print
@@ -447,10 +459,17 @@ test-tier command:
a one-file run (each serial file) reports its file from the launch for any
runner. Other runners (node --test, vitest, jest, pytest, playwright test, flutter test, dart test, cargo test) get heartbeats
only. A heartbeat follows each 60 s with no progress line;
- 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. The `--remote` gate is exempt: a
- is killed as **stalled** when its log does not grow and its process group
stays under a tenth of one CPU 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. The group counts as busy when its CPU time, sampled each second,
grew by at least a tenth of the wall time, so a quiet suite that is working
(FOUN-63: a parallel `check.sh` silent for 9 minutes) is not killed, while
a hang that waits on nothing is. A hang that keeps burning CPU is left to
the timeout. Linux reads `/proc` (exited children's time included once
reaped); macOS and other BSDs read `ps -S`; a failed read skips that
sample. Windows reads no CPU time here and uses log growth alone. 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
+27 -1
View File
@@ -207,7 +207,14 @@ function but a same-file `url(#id)`, which closes `@import`, `image-set()`,
entity declaration, and no DOCTYPE but a bare `<!DOCTYPE svg>` or W3C's own SVG DTD, read after character references are decoded; anything else, a
script or `foreignObject` included, keeps it code. More than
2,000 data paths keep every one code; a `git grep` failure is `consumer_scan_failed`
and fails the depth closed. Only `classifyDiff` applies the rule (`lib/review-collect.ts`
and fails the depth closed. The scan first lists the files that contain a candidate's
extension and then searches only those, docs files left out, so a large data tree
costs one cheap pass. Each scan gets one 120-second deadline, set in milliseconds with
`GSTACK_CONSUMER_SCAN_TIMEOUT_MS`; a scan that runs past it, or whose matches pass
256 MB, is also `consumer_scan_failed`, and its detail says what stopped it and names
the deadline. One scan fits a minute inside the 180 seconds /ship gives
`gstack-review-plan`; a plan that also scans a fix range runs two scans, and if they
pass that limit the plan fails, which also leads to a full review. Only `classifyDiff` applies the rule (`lib/review-collect.ts`
finds the consumers, `gstack-ship-preflight`'s `RUNTIME_LINES` reads the same
function); every path-only caller (the receipt's gate-scan decision, the Step 16
post-review route, /autoplan's docs-only scope) keeps data files code. A numeric claim
@@ -424,6 +431,24 @@ Over the last 100 non-release commits on main (8663c3d2..8be7f213), money hit
which hit 43 of 100; those paths already get full depth, so the gate class was
narrowed to the gate scan's tagged hunks.
<a id="review-exclude-paths"></a>
## What readers read under `review_exclude_paths`
A repository can list content folders its reviewers leave out, on its base
branch only ([ship-policy.md](ship-policy.md#review-exclude-paths)). Under it,
every reader (specialists, the Red Team, the native and outside voices, the
targeted reader) reads the same selection: its `DIFF_CMD:` with one
`:(top,exclude,glob)` item per applied pattern, the fixture command with the
same items, and the `DIFF_KEEP:` command for the kept files under those
patterns. The outside voice's embedded diff is that selection. Each brief says
how many files and bytes were left out and that the reader must not report on
their absence. Excluded paths count as docs for the depth and never trigger the
numeric-claim scan or a targeted class; kept files keep their own class.
Independently of the key, no reader is handed more than 2,000,000 bytes of
diff: `gstack-review-dispatch` stops with `REVIEW_DIFF_TOO_LARGE` before it writes
anything, instead of a review of the part a reader happened to see.
## What the receipt records
`gstack-review-scope summary --since <REVIEW_SINCE> --run <run dir>` reads this invocation's
@@ -674,6 +699,7 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
- `scripts/resolvers/review-army.ts`: the floor and `NO FINDINGS` in every specialist and Red Team prompt (rendered into `review/sections/review-army.md` and `ship/sections/review-army.md`), and the lead judgment's "On record" line that turns a [ruled class](#rulings-below-the-floor) into a note; in `/ship` every specialist and Red Team prompt it renders carries its diff base as a literal sha the parent writes in (`DIFF_BASE=<diff-base>`: the merge base on pass 1, `RANGE_BASE` from pass 2 on) beside its plan's `DIFF_CMD` excludes, because a reviewer is its own process and never sees the parent's shell variables; the fan-out batches above.
- `ship/sections/review-army.md` Step 9: the plan's `REVIEW_DEPTH`, `GATE_SCAN` and `--depth-floor` on every run, the depth fields of the review-log row, passes, `RANGE_BASE`, classify, defer, render, the cap check before any edit, the Step 11 late-defer row, scratch files under `.gstack/tmp/`, the cap decisions; `ship/sections/adversarial.md` Step 11: the outside voice reads the same range and scopes queued fixes the same way.
- `lib/review-exclude.ts`: the base policy's `review_exclude_paths` (matched set, kept reasons, the kept-file argument bound, the digest the receipt, carry and the queue compare) and the 2,000,000-byte reader ceiling (`test/review-exclude.test.ts`).
- `lib/review-depth.ts`, `bin/gstack-review-plan`: the diff class, the depth (`depthFor`, `DEPTH_RULES`, `depthOfCommitInput`), each dispatch's `exclude` pathspecs and `DIFF_CMD`, the transition table, the numeric-claim scan and each role's effort tier (`test/review-depth.test.ts`, `test/review-plan-cli.test.ts`).
- `lib/review-collect.ts`: `collectCommit` (git objects only, from any checkout) and `collectLive` (the working tree), per-path lines and the base commit's registry (`test/review-collect.test.ts`).
- `lib/review-scope.ts`, `bin/gstack-review-scope`: `classify`, `defer`, `render`, `summary` (with `fix_checks`, `dispatches`, `est_input_tokens`, `unrecorded`, `ledgers_read`); refusals are `REVIEW_SCOPE_INPUT`, `REVIEW_SCOPE_RANGE` and `REVIEW_SCOPE_LEDGER` ([troubleshooting.md](troubleshooting.md#review-scope-input)).
+122
View File
@@ -58,6 +58,7 @@ refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
| `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`. |
| `full_e2e` | `true` \| `false` | `false` | `true`: every merge needs a passing full-gate result bound to a tree equal to the merging one modulo stamps; `gstack-ship-queue ready`, `stamp` and `verify` refuse without one (`QUEUE_GATE_MISSING`): a gate-ahead record, or the lane's `--gate-exit 0 --gate-log <file>` from its run on the gated head. Needs `min_gstack_version`. |
| `review_scope` | `frozen` \| `diff-plus-blocking` \| `all` | `frozen` | The finding scope of `/ship`'s review loop; see [review_scope](#review-scope). `frozen` fixes the diff on pass 1, files blocking findings in unchanged code as issues, and later passes read only the range since the previous pass. `diff-plus-blocking` is the same but fixes a blocking finding in unchanged code in this PR instead of filing it. `all` is the earlier loop: every finding is fixed on every pass. Needs `min_gstack_version` when not `frozen`. |
| `review_exclude_paths` | git glob pathspecs | `[]` | Content files (data, docs, binary, `.log`) `/ship`'s reviewers leave out; read from the base only, with code, gate, config, executable and named-by-code files kept. See [review_exclude_paths](#review-exclude-paths). Needs `min_gstack_version`. |
| `min_gstack_version` | version or `null` | `null` | The oldest gstack that may read the file; see the compatibility rule above. |
| `docs` | `{ caps: [{ path, max_bytes }] }` or `null` | `null` | Documentation files with a byte cap. `/ship` Step 14.5 runs `gstack-ship-receipt docs-caps --base <base>` (the caps come from `origin/<base>`'s policy, never the branch's copy; `--policy <file>` is for a repository bootstrapping its policy) before the verification gate and prints one `DOCS_CAP: <path> <bytes>/<max> ok\|over` line per cap; any `over` (an absent file counts) exits 1 and blocks the ship until the file is trimmed or the cap raised on the base branch (`DOCS_AUDIT_BLOCKED`). At most 64 caps, each path contained. See [the ship receipt](#ship-receipt). Needs `min_gstack_version`. |
| `train` | `{ gate_workflow, free_workflow, full_gate_check, record_artifact, max_size, fill_hours, screen_selection, trial_reservation_usd, land? }`, `{ gates, land, max_size, fill_hours }` or `null` | `null` | Turns on [merge trains](#run-a-merge-train), read from the base revision only. Every field is required and an unknown field is `POLICY_INVALID`: `max_size` 1 to 6, `fill_hours` above 0 and at most 24, `screen_selection` `gstack-paid` \| `none`, `trial_reservation_usd` above 0 and at most 100. A block with `gates` ([any repository](#train-gates)) takes `max_size` 1 to 5 and needs `min_gstack_version` at least `TRAIN_GATES_KEY_VERSION`. `null` keeps the per-PR queue exactly as it is. Needs `min_gstack_version`. |
@@ -1252,6 +1253,127 @@ opens no fence, and a fence that never closes stays prose, along with the rest
of the body. Data PRs (evals,
measurements, sweeps) run it before READY so the body's numbers are the run's.
<a id="review-exclude-paths"></a>
## `review_exclude_paths`: what reviewers leave out
Since the release that added it (`REVIEW_EXCLUDE_KEY_VERSION` in
`lib/ship-policy.ts`). A results PR can carry thousands of receipt files that
no reviewer needs to read. `review_exclude_paths` lists git glob pathspecs
whose content files `/ship`'s reviewers leave out:
```json
{ "review_exclude_paths": ["receipts/**"], "min_gstack_version": "<the release that introduced review_exclude_paths>" }
```
**Read from the base only.** `gstack-review-plan` reads the policy committed on
the base commit it already resolved (`origin/<base>`, or `<base>`), never
the branch's copy, so a branch cannot exclude its own files. A branch that adds
the key gets `REVIEW_EXCLUDE: none (head sets review_exclude_paths; it applies
after it lands on <base>)` and a normal review. A base policy that does not
validate stops the plan (`POLICY_INVALID`), and a base git cannot read stops it
(`POLICY_SOURCE_UNAVAILABLE`); neither turns exclusion off quietly.
**Git's glob dialect.** Each pattern is a `:(top,glob)` pathspec: root-anchored
and case-sensitive. The matched set comes from git itself (`git diff --raw -z`),
so the plan, the receipt and what readers see are one set. Untracked files are never part of it: no reader's `git diff` shows them, and the receipt reads the committed head.
| Pattern | gitignore would match | `review_exclude_paths` matches |
|---|---|---|
| `receipts/**` | everything under `receipts/` at any depth | everything under the top-level `receipts/` |
| `receipts/*` | files directly in `receipts/` | files directly in `receipts/` only (`*` never crosses `/`) |
| `data/` | any `data/` directory at any depth | nothing: write `data/**` (and `**/data/**` for any depth) |
A pattern that matches no changed file is listed as `unmatched=` on the plan line.
**What is excluded, and what is kept.** A matched file is excluded only when it
is content: a data file (JSON, JSONL, CSV, TSV, images), a docs-class file, a
`.log` file, or a file git reports as binary (`-\t-` in `--numstat`; readers would only see `Binary files differ`) whose bytes hold a NUL in their first 8,000, so a branch's `.gitattributes` cannot make a text file binary content. A binary file is content even under
`config/` or in a skill directory; only `policy-dir`, `gate-path`, `executable`, `link`, `gate-tagged`, or a source or script extension (`code`) keep it.
Everything else stays in every reader's diff with one reason code:
| Reason | Kept because |
|---|---|
| `policy-dir` | it is under `.gstack/` or `.github/` |
| `gate-path` | it is a gate path (lib/review-depth.ts) |
| `executable` | its old or new mode is executable |
| `link` | its old or new mode is a symlink or a submodule commit |
| `agent-prose` | it is a skill, section, resolver, template or rule file |
| `config` | it is a data-shaped config file (`package.json`, `tsconfig*.json`, `config/`, dotfiles) |
| `gate-tagged` | the gate scan tagged one of its hunks |
| `code` | it is code or a test, not content; a binary file with a source or script extension (`.js`, `.py`, `.sh`, `.wasm`) counts as code |
| `secret-line` | an added line reads a credential (the targeted pass's secret rule) |
| `named-by-code` | a code or test file that stays in review (not docs, a data file, a `.log` or binary, and not itself excluded) names its full repository path, at the head or the merge base |
| `consumer-scan-unavailable` | the `git grep` for those names failed, so every candidate is kept; also a live binary file whose bytes could not be read |
| `gate-scan-unavailable` | the gate scan failed or did not run at the head, so no hunk's tag is known and every candidate is kept |
Only a full path counts as a name. A basename such as `result.json` would keep
every receipt in the tree, and a directory reference such as `receipts/` (the
harness that writes the receipts names it) keeps nothing: the plan prints an
informational `REVIEW_EXCLUDE_REFERENCE:` line naming the code that reads the
folder. A data file never keeps another file: a results manifest such as
`docs/receipts-manifest.json` lists every receipt by full path, and it is
content, not a program that reads them. The name scan is one uncapped `git grep -F -f` over the candidates'
paths, not the collector's capped consumer scan.
**What readers run.** Every reader's `DIFF_CMD:` gains one
`':(top,exclude,glob)<pattern>'` item per applied pattern, and the fixture
command the security specialist, the Red Team and the native adversarial pass
read through gets the same items. A git exclude beats any positive pathspec, so
kept files under an applied pattern come back through a second command,
`DIFF_KEEP: <reader> git diff <diff-base> -- ':(top,literal)<path>' …`, which every
brief names. That command is bounded by its argument bytes (24,000 across all
patterns, under Windows' command-line limit): a pattern whose kept files would
pass the bound is not applied, and the plan line says so as
`skipped=<pattern>:kept=<n>:bytes=<b>`. Every brief also gets one line:
`Excluded by base policy <ref>@<sha12> (selection <digest>): <n> files, <bytes> bytes
on this branch matching <patterns>. They are not in your diff; do not report on
their absence.`
**Depth, numbers and the targeted pass.** Excluded paths count as docs for the
review depth everywhere it is computed (`gstack-review-plan`, the receipt
writer, carry and the queue), and their lines never trigger the numeric-claim
scan or the targeted money, gate, auth and secret classes. Defects inside a
correctly excluded data file are coverage the owner gives up by policy.
**The plan line.** `REVIEW_EXCLUDE: patterns=<applied>/<declared> excluded=<files>
bytes=<b> kept=<k> content=<kind>:<n>,… source=<ref>@<sha12> digest=<sha256>`
(`content=` counts the excluded files by kind: `binary`, `data`, `docs`, `log`), then up to 20
`REVIEW_EXCLUDE_KEPT: <reason> <path>` lines (`--json` lists every one). A
repository with no key anywhere prints none of these lines, and its plan, briefs
and diff commands are byte for byte what they were.
**The receipt, carry and the queue.** Each rendered brief records the digest it
names in the review ledger, and `gstack-review-dispatch verify` prints
`REVIEW_EXCLUDE_RECORD: digest=<d>` when every reader's newest brief names the
same one (a mixed set makes it `VERIFY: dirty`). `gstack-ship-receipt write
--review-exclude <d>`, with that digest, records `review_exclude` (source, patterns, counts, the gate-tagged kept
paths and three hashes). The `digest` hashes the declared patterns and the
excluded and kept lists; `source` is provenance only. Write, carry and the queue
each recompute the digest at the current base and compare it, so a base merge
or a moved base that leaves the patterns and the lists unchanged still carries
and binds. A different digest means the reviewers read another selection:
write refuses with `RECEIPT_INVALID field=review_exclude`, carry with
`RECEIPT_CARRY_REFUSED review_exclude changed`, and the queue with
`RECEIPT_INVALID field=review_exclude expected=<digest> recorded=<digest>`. The
fix is to rerun the review, never a receipt write alone.
**Adopting it.** (1) Open a PR on the base that adds the patterns and
`min_gstack_version` at least `REVIEW_EXCLUDE_KEY_VERSION`, and ship it like any
policy change. (2) Once it merges, bring the base into the results branch (a
crew needs its coordinator's word for that merge) and rerun `/ship`; the plan
prints `REVIEW_EXCLUDE:`. (3) Check the kept list: a file you expected to be
excluded names its reason.
<a id="review-diff-ceiling"></a>
**The review ceiling.** Independently of the key, `gstack-review-dispatch` reads
each reviewer's diff once before it writes a manifest or a brief (`pass`, `stage`
and `render`). A diff over 2,000,000 bytes (about 500K tokens) stops with
[`REVIEW_DIFF_TOO_LARGE`](troubleshooting.md#review-diff-too-large), exit 3, and a
fix chosen by where the policy stands; a `git diff` that fails or runs past
120 seconds stops with
[`REVIEW_DIFF_UNREADABLE`](troubleshooting.md#review-diff-unreadable), exit 1.
There is no override flag: the recovery is a policy PR on the base, or a split.
<a id="review-scope"></a>
## `review_scope`: how the review loop converges
+8 -2
View File
@@ -631,6 +631,8 @@ One exception: a shortcut you took deliberately and logged. A `gstack-shortcut(d
**Design pass.** When the diff touches frontend files, the Design specialist reads `review/design-checklist.md`, which is generated from `lib/design-catalog.ts`, so `/review`, `/ship`, and `/design-review` flag the same patterns under the same rule ids. If you have [impeccable](https://impeccable.style) installed, its engine scans the changed frontend files first (`.dart` files get the checklist pass only): its rows bucket by tier (auto-fix, ask, possible), a detector hit and a checklist hit at the same file:line collapse into one row, and your repo's `.impeccable/config*.json` ignores are read as settled decisions. Without it, the checklist pass runs alone.
**PR type.** The checklist starts by naming the kind of PR from the changed paths: docs-only, CI/infra, scripts/tooling, application or mixed, and the review's first line says which (`PR type: <type>`). Application and mixed diffs run every category as before. A CI/infra, scripts/tooling or docs-only diff always gets Gate Integrity, Shell Injection, Completeness Gaps, Distribution & CI/CD Pipeline and Repo Conventions & Platform Fit; every other category with nothing to check is listed once, in one line, as not applicable, and never counts as passed. Repo Conventions & Platform Fit flags new files that don't match the languages and tools the repo already uses or the platforms its CI runs on, such as a `.bat` build script in a bash-and-Makefile repo.
### Example
Suppose the smart listing flow is implemented and the tests are green.
@@ -1538,7 +1540,7 @@ Install Greptile on your GitHub repo at [greptile.com](https://greptile.com) —
The problem with any automated reviewer is triage. Greptile is good, but not every comment is a real issue. Some are false positives. Some flag things you already fixed three commits ago. Without a triage layer, the comments pile up and you start ignoring them — which defeats the purpose.
gstack solves this. `/review` and `/ship` are now Greptile-aware. They read Greptile's comments, classify each one, and take action:
gstack solves this. `/review` and `/ship` read your review bots' comments (Greptile, CodeRabbit, Macroscope, or any bot you name), classify each one, and take action:
- **Valid issues** get added to the critical findings and fixed before shipping
- **Already-fixed issues** get an auto-reply acknowledging the catch
@@ -1548,7 +1550,11 @@ The result is a two-layer review: Greptile catches things asynchronously on the
### Learning from history
Every false positive you confirm gets saved to `~/.gstack/greptile-history.md`. Future runs auto-skip known FP patterns for your codebase. And `/retro` tracks Greptile's batting average over time — so you can see whether the signal-to-noise ratio is improving.
Every false positive you confirm gets saved to `~/.gstack/review-bot-history.md`, tagged with the bot that made it. Future runs auto-skip that bot's known FP patterns for your codebase. And `/retro` tracks each bot's batting average over time — so you can see whether the signal-to-noise ratio is improving.
### Choosing your bots
The bots are the two lists `gstack-pr-wait` uses: `gstack-config set pr_wait_gating_bots greptile,macroscope` (waited for, then triaged) and `gstack-config set pr_wait_advisory_bots coderabbit,copilot` (triaged, never waited for). Set both to `none` to turn bot triage off. Greptile's early review (`/ship` opening the PR early) runs only while `greptile` is in one of the lists.
### Example
+1
View File
@@ -21,6 +21,7 @@ Beyond the slash-command skills, gstack ships standalone CLIs for workflows that
| `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. `quarantine add|renew|release|check|report` keeps `.gstack/quarantine.json`: a quarantined test still runs, gets up to 3 attempts of the whole command, passes only on a full green attempt and is never counted as a pass (`run --base <ref>` reads the registry from the base branch). `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-review-bots` | **Review-bot comments** — `fetch [--pr <n>]` reads every root line comment, conversation comment and newest review body from each bot in `pr_wait_gating_bots` and `pr_wait_advisory_bots` (Bot accounts only, all pages, through `gh`) into a private directory with per-bot counts, or prints `REVIEW_BOTS: skip` or `REVIEW_BOTS: error` (never a false zero). `record --dir <dir> --id <comment id> --type fix\|fp\|already-fixed --category <category>` appends one triage outcome to the project and global `review-bot-history.md`, taking the bot and file from the fetched comment. `/review` Step 2.5 and `/ship` Step 10 run it. [Triage reference](../review/greptile-triage.md). |
| `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. |
+101 -4
View File
@@ -2434,7 +2434,10 @@ default `fetch-depth: 1`), a remote branch never fetched
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.
(all flags false, exit 2) for the same cause, and also when no base was given
and none resolves because `origin/HEAD` names the checked-out branch (or its
upstream, or HEAD's own commit) and no PR base, live remote HEAD, `origin/main`
or `origin/master` resolves (its comment line says so).
**Fix.** Run the command the line prints. It is built for this checkout:
`git fetch origin +<branch>:refs/remotes/origin/<branch>` for an unfetched
@@ -3330,6 +3333,72 @@ block. For the `--head` refusal, check out that commit and write there. For a
targeted refusal, run the targeted reader on HEAD, `gstack-review-dispatch verify
--targeted-out .gstack/tmp/targeted.json`, and write again with
`--review-targeted @.gstack/tmp/targeted.json`.
**`field=review_exclude`.** `gstack-ship-receipt write` and the queue's receipt check also
use this code when the receipt's review_exclude digest (or the
`--review-exclude <digest>` write was given) is not the digest HEAD has under the
base policy's [`review_exclude_paths`](ship-policy.md#review-exclude-paths). The line
names `expected=` (now), `recorded=` (what the review read, `none` when it read
without exclusion) and `source=` (the base commit). The reviewers read another
selection than the one the head now has, so a receipt write alone cannot clear
it: rerun `/ship`'s review on the head (`gstack-review-dispatch pass --base <base>`),
run `gstack-review-dispatch verify --ledger <ledger>`, which prints
`REVIEW_EXCLUDE_RECORD: digest=<d>` from the briefs the readers got, then
`gstack-ship-receipt write --pr <n> --review-exclude <d> --body <body>`.
<a id="review-diff-too-large"></a>
### `REVIEW_DIFF_TOO_LARGE`
**Meaning.** One reviewer's diff is over the 2,000,000-byte review ceiling
(about 500K tokens). `gstack-review-dispatch pass`, `stage` and `render` read
each reader's diff once before writing anything, and stop (exit 3, nothing
written) rather than hand a reader more than it can read. The line names the
reader, its bytes, the five largest top-level folders (`top=<dir>:<bytes>`) and
the base policy commit.
**Fix.** The line prints the fix for where the base policy stands:
- **No key:** copy the printed JSON (`{"review_exclude_paths":[…],"min_gstack_version":"…"}`)
into `.gstack/ship-policy.json` on the base as its own PR. A lane cannot land on
the base itself: ask the repo owner or the coordinator. Once it merges, bring
the base into the branch and rerun once; if it is still over, split the PR.
- **The branch declares the key but the base does not:** only the base copy
applies; land that hunk on the base first, as above.
- **A pattern was not applied** (`skipped=` on the `REVIEW_EXCLUDE:` line): its kept
files need more than 24,000 argument bytes. Narrow the pattern, move the kept
files out of it, or split the PR.
- **Still over after exclusion:** add the printed folders to the base policy, or
split the PR.
There is no override flag. See [the review ceiling](ship-policy.md#review-diff-ceiling).
<a id="review-diff-unreadable"></a>
### `REVIEW_DIFF_UNREADABLE`
**Meaning.** git could not read a reviewer's diff, so the pass stopped (exit 1)
before writing a manifest or a brief. `reason=timeout` means `git diff` ran past
`timeout_ms` (120,000 ms; fixed, no flag); `reason=git-exit` gives git's exit code.
The line carries the diff base, git's first stderr line with remote URLs
replaced by `<url>`, and `log=`, a file under the run directory holding git's whole
stderr, redacted the same way. It is also printed when git cannot list the
files `review_exclude_paths` matches.
**Fix.** By reason: a timeout, rerun once unchanged, then split the PR or
exclude its largest data folders on the base; git exit 128 (a missing object or
an unfetched base), `git fetch origin <base>` and rerun once; anything else, stop
and ask a person with the log file.
<a id="review-exclude-reference"></a>
### `REVIEW_EXCLUDE_REFERENCE`
**Meaning.** Informational, never a stop. A kept code file names a folder that
`review_exclude_paths` excludes (for example the harness that writes
`receipts/`). A folder reference does not keep files; only a full file path
does, so readers see that code without the files it reads.
**Fix.** Nothing for the review itself. If the code's correctness depends on the
excluded files, narrow the pattern on the base so they stay in the diff.
<a id="execution-unknown"></a>
### `EXECUTION_UNKNOWN`
@@ -3707,7 +3776,12 @@ directory, or under a scratch name (`zz-*`, `tmp*`, `scratch*`).
appears with its old value in a test file the branch did not update — nightly-
and Heavy-only files included.
**Fix.** Update every listed `file:line`, or keep the old value.
**Fix.** Update every listed `file:line`, or keep the old value. When a test
keeps the old value on purpose (the refused input after a minimum version goes
up), mark that line: `// pregate-literal: NAME=<old value> <reason>` on the line
or on the comment line directly above it. The marker waives only that line, and
the check lists it as `waived:`. Don't use `warn: ["literals"]` for this, since
that turns the check off for every constant.
<a id="pregate-no-lane"></a>
### `PREGATE_NO_LANE`
@@ -3843,14 +3917,14 @@ 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) 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, and its process group used under a tenth of one CPU (Windows: log growth alone), 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.
**Fix.** Open the printed log, find the hang in the named file (a blocked
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
`gstack-pregate --stage tests`. A test that legitimately waits quietly for longer
needs a larger `--stall-timeout`; `0` disables the stall kill and leaves only
`--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
@@ -5483,6 +5557,29 @@ wait`'s `--timeout`.
**Fix.** Run `wait` again (the pause stays valid until it expires), or find who
owns the base's red.
<a id="base-contains-head"></a>
### `BASE_CONTAINS_HEAD`
**Meaning.** A base-relative step (`gstack-ship-queue ready`, `land` and
`train` receipt binding, `gstack-review-plan`, `gstack-pregate`,
`gstack-gate-diff`, `gstack-diff-scope`, `gstack-docs-candidate snapshot`)
was given a base that is the head or already contains it, while the branch
has commits its real base lacks. The comparison would read the branch against
itself and report no changes, a false green. The usual cause is a checkout
made by a PR flow: `refs/remotes/origin/HEAD` names the PR branch and there is
no local `main` (GST-774). The detail names the base, the head, and the real
base (the PR's base from `gh`, else the remote's live HEAD from
`git ls-remote --symref`, else `origin/main`, else `origin/master`). For
a pull request's head the queue refuses whenever the base contains it. On the
base branch itself, on a detached HEAD, and on a fresh branch with no commits
past its base, the step runs as before. `gstack-diff-scope` reports it as
`SCOPE_ERROR=base_contains_head` (all flags false, exit 2) with the reason on a
comment line.
**Fix.** Pass the real base (`--base main`, or the PR's base branch). When
`origin/HEAD` names the checked-out branch, `git remote set-head origin main`
repairs every default at once.
<a id="queue-receipt-depth-raised"></a>
### `QUEUE_RECEIPT_DEPTH_RAISED`
+41 -1
View File
@@ -337,7 +337,7 @@ in about a hundred lines, generated from the code.
```bash
AP=bin/gstack-autoplan
$AP next --out <dir> --plan <plan.md> [--ui|--no-ui] [--developer-tool] [--no-dx] [--light] [--spend-cap <usd>]
$AP next --out <dir> --plan <plan.md> --full [--ui|--no-ui] [--developer-tool] [--no-dx] [--light] [--spend-cap <usd>]
# PHASE: ceo snapshot=<sha> order=ceo,design,dx,eng
# SCOPE: ui=yes (...) dx=yes (detected: ...) source=ui:detected,dx:detected
# DRIVER: <root>/autoplan/DRIVER.md
@@ -353,6 +353,46 @@ and, when CLI flags differ from the plan's marker, `SCOPE_PIN: add <!--
gstack-autoplan-scope: … --> as the first line of <file>`; gstack never writes
the plan.
### Kinds: fix, feature and full (R65)
The example above is the full run. The first `next` also takes `--kind`:
- **`--kind fix`**: a bug fix gets no plan review. `gstack-autoplan next --kind fix`
prints `AUTOPLAN: not needed (bug fix; R65)`, a `PROOF:` line and
`GSTACK_RESULT: skill=autoplan status=skipped run=-`, exits 0 and writes
nothing (no `--out` or `--plan` needed; `--json` prints one object with
`status: "skipped"`). `run --kind fix` does the same before any paid call.
An `--out` that already holds a run is refused. The PR still owes the bug's
proof: a repro on current main or a failing test written first.
- **`--kind feature`**, the default for an unattended run on Capy
(`GSTACK_CAPY=1`) that names no kind, is the lean run. The run is the `eng`
phase alone with one native reviewer, whose prompt merges the CEO and Eng
lenses (Design and DX only when scope detects them) and always asks "Any
outside terms, licences or rights involved?". The reviewer reads the draft
code with the plan: `git diff` from the merge base with `--base <ref>`
(default `origin/HEAD`), untracked files included and the run directory
left out, written into the phase's snapshot directory as `draft.diff` and
hashed into `run.json` (`phases.eng.draft`) and the `phase_opened` journal
row. With no code yet, the run says so: `DRAFT: none (<why>)`. A git
command that fails is `DRAFT: none (draft capture failed: …)`, never a
partial draft. An untracked file whose name looks like a secret (`.env*`,
`*.pem`, `*.key`, `id_rsa`, `credentials*`, `*secret*`) is left out with a
note, and the `DRAFT:` line names the checkout it diffed (`root=`). The prompt inlines the diff in a fence longer than any tilde
run inside it, cut at 100,000 bytes (`shown=` on the `DRAFT:` line) with a
note asking the reviewer to name what it didn't see. The reviewer's
line after INPUT names the draft (`DRAFT: <sha256>` or `DRAFT: none`), or
submit refuses the result with `RESULT_RECEIPT_MISSING`. A hand-run record
bound with `--attest` has no receipt at all, so its deviation says the
draft was not bound. `run`, which pays for both voices, stays the full run
unless `--kind feature` asks for the lean one. There is no outside
voice, `--max-rounds` defaults to 1, carry-forward is off, and the gate has
one revision: `revise --incorporates` applies the owner's accepted changes
without re-review (`APPLIED: … (lean run: one gate revision; …)`) and
approves. The gate and `gstack-owner-brief render --captain` relay work as
in the full run. A paid-eval prereg counts as a feature.
- **`--full`** restores every phase and both voices. Off Capy, and in an
interactive session, the default stays full; `--ceo-only` stays its own run.
1. **Dispatch each `ATTEMPT:`.** The native voice is a subagent on the parent's
own model given the prompt file; the outside voice is a subagent on another
model family, or `bin/gstack-outside-voice run --runner codex-cli|api