mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 15:27:56 +01:00
Merge commit '6f1dfd5b7cd0507d29e62d09a31e77f7e787e947' into gsta249/gst-942-rm-recursive
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> # Conflicts: # test/review-packets-removed.test.ts
This commit is contained in:
commit
c37bb2bedd
244 files changed
+8030
-1009
No files matched your search
+46
-42
@@ -1,7 +1,9 @@
|
||||
# CHANGELOG + VERSION style
|
||||
|
||||
Moved verbatim from CLAUDE.md (token-load reduction). Read this BEFORE
|
||||
writing any `## [X.Y.Z]` CHANGELOG entry.
|
||||
writing a CHANGELOG entry. In gstack the entry is a fragment,
|
||||
`changelog.d/<branch-slug>.md`, and the merge step gives it its `## [X.Y.Z.W]`
|
||||
heading ([changelog fragments](ship-policy.md#changelog-fragments)).
|
||||
|
||||
## Versioning and branch-scoped entries
|
||||
|
||||
@@ -9,15 +11,15 @@ Moved verbatim from CLAUDE.md (#2096 size limit). The autonomous version
|
||||
policy (choose versions without asking; default to PATCH) stays in CLAUDE.md
|
||||
as the single source.
|
||||
|
||||
**Versioning invariant (workspace-aware ship).** VERSION is a monotonic ordered
|
||||
release identifier, not a strict semver commitment. The bump level
|
||||
(major/minor/patch/micro) expresses intent at ship time. Queue-advancing past a
|
||||
claimed version within the same bump level is explicitly permitted — if branch A
|
||||
claims v1.7.0.0 as a MINOR and branch B is also a MINOR, B lands at v1.8.0.0
|
||||
(still a MINOR relative to main). Downstream consumers must NOT rely on
|
||||
"MINOR = feature-only, PATCH = fix-only" as a strict contract. This is why
|
||||
`bin/gstack-next-version` advances within the chosen bump level rather than
|
||||
repicking the level when collisions happen.
|
||||
**Versioning invariant.** VERSION is a monotonic ordered release identifier,
|
||||
not a strict semver commitment. The bump level (major/minor/patch/micro)
|
||||
expresses intent. Versions are assigned at merge, in merge order: each stamp
|
||||
takes the version before it plus the effective bump, which is the policy's
|
||||
PATCH raised by any folded fragment's `bump:` front matter. If PR A and PR B
|
||||
both ask for MINOR and land back to back on v1.6.0.0, A lands at v1.7.0.0 and
|
||||
B at v1.8.0.0 (still a MINOR relative to the version before it). Downstream
|
||||
consumers must NOT rely on "MINOR = feature-only, PATCH = fix-only" as a
|
||||
strict contract.
|
||||
|
||||
**package.json carries the npm-valid translation, not VERSION verbatim.**
|
||||
VERSION stays the 4-digit source of truth (e.g. `1.67.0.0`); package.json and
|
||||
@@ -26,18 +28,25 @@ translation (`1.67.0`), and lockfile `version` fields sync only when the
|
||||
lockfile already exists. `bin/gstack-version-bump` (via `lib/version-source.ts`)
|
||||
owns the translation and judges drift on translated forms — do NOT "fix" the
|
||||
apparent mismatch by hand, and do not write a 4-digit version into
|
||||
package.json (npm rejects it). Rationale and translation rules live in the
|
||||
package.json (npm rejects it). A branch never writes either one: the merge
|
||||
stamp does, through `bin/gstack-version-bump`. Rationale and translation rules live in the
|
||||
`lib/version-source.ts` header; `test/gstack-version-bump.test.ts` pins the
|
||||
contract.
|
||||
|
||||
**VERSION and CHANGELOG are branch-scoped.** Every feature branch that ships gets its
|
||||
own version bump and CHANGELOG entry. The entry describes what THIS branch adds —
|
||||
not what was already on main.
|
||||
**The CHANGELOG entry is branch-scoped; the version is not.** Every feature
|
||||
branch that ships writes one entry as `changelog.d/<branch-slug>.md`: the entry
|
||||
text only, with no `## [x]` heading, and `bump: minor` or `bump: major` front
|
||||
matter only when the change warrants more than a PATCH. A branch never edits
|
||||
VERSION, package.json's version or a released CHANGELOG.md section. At merge,
|
||||
`train land` (or `gstack-ship-queue land`) assigns the version, folds the
|
||||
fragment into one `## [<version>] - <date>` section at the top of CHANGELOG.md
|
||||
and deletes the file. The entry describes what THIS branch adds — not what was
|
||||
already on main.
|
||||
|
||||
**The CHANGELOG entry is the diff between main and the shipping branch — what users
|
||||
get when they upgrade. NOT how the branch got there.** A reader landing on the entry
|
||||
should learn what they can do now that they couldn't before; they should not learn
|
||||
about the branch's internal version bumps, the bugs we caught and fixed mid-branch,
|
||||
about versions the branch carried during development, the bugs we caught and fixed mid-branch,
|
||||
the plan reviews we ran, or the commits we squashed. That is branch development
|
||||
narrative. It belongs in PR descriptions and commit messages, not CHANGELOG.
|
||||
|
||||
@@ -62,29 +71,28 @@ the user gets; the path to that system is invisible to them.
|
||||
**When to write the CHANGELOG entry:**
|
||||
- At `/ship` time (Step 13), not during development or mid-branch.
|
||||
- The entry covers ALL commits on this branch vs the base branch.
|
||||
- `/ship` lints the lines the branch added with `bin/gstack-prose-lint CHANGELOG.md --base origin/<base> --gate`,
|
||||
and the Quality gate workflow gates the `## [Unreleased]` block the same way. Rewrite each `PROSE:` line
|
||||
it names ([docs/troubleshooting.md](troubleshooting.md#prose-lint-found)).
|
||||
- Never fold new work into an existing CHANGELOG entry from a prior version that
|
||||
already landed on main. If main has v0.10.0.0 and your branch adds features,
|
||||
bump to v0.10.1.0 with a new entry — don't edit the v0.10.0.0 entry.
|
||||
- `/ship` lints the fragment with `bin/gstack-prose-lint changelog.d/<branch-slug>.md --gate`.
|
||||
Rewrite each `PROSE:` line it names ([docs/troubleshooting.md](troubleshooting.md#prose-lint-found)).
|
||||
- Never fold new work into a CHANGELOG.md section that already landed on main.
|
||||
New work goes in this branch's fragment, and the merge stamp gives it its own
|
||||
section at the next version.
|
||||
|
||||
**Key questions before writing:**
|
||||
1. What branch am I on? What did THIS branch change?
|
||||
2. Is the base branch version already released? (If yes, bump and create new entry.)
|
||||
3. Does an existing entry on this branch already cover earlier work? (If yes, replace
|
||||
it with one unified entry for the final version.)
|
||||
2. Does this change warrant more than a PATCH? (If yes, open the fragment with
|
||||
`bump: minor` or `bump: major` front matter; otherwise add none.)
|
||||
3. Does this branch's fragment already cover earlier work? (If yes, rewrite it as
|
||||
one unified entry.)
|
||||
|
||||
**Merging main does NOT mean adopting main's version.** When you merge origin/main into
|
||||
a feature branch, main may bring new CHANGELOG entries and a higher VERSION. Your branch
|
||||
still needs its OWN version bump on top. If main is at v0.13.8.0 and your branch adds
|
||||
features, bump to v0.13.9.0 with a new entry. Never jam your changes into an entry that
|
||||
already landed on main. Your entry goes on top because your branch lands next.
|
||||
**Merging main never gives your branch a version.** When you merge origin/main into
|
||||
a feature branch, main may bring new CHANGELOG sections and a higher VERSION. Take them
|
||||
exactly as main has them. Your branch still carries only its fragment, and never jams its
|
||||
changes into a section that already landed on main; the merge stamp puts its section on top.
|
||||
|
||||
**After merging main, always check:**
|
||||
- Does CHANGELOG have your branch's own entry separate from main's entries?
|
||||
- Is VERSION higher than main's VERSION?
|
||||
- Is your entry the topmost entry in CHANGELOG (above main's latest)?
|
||||
- Is your fragment still in `changelog.d/`, with its own text, separate from main's sections?
|
||||
- Are VERSION, package.json's version and every released CHANGELOG.md section exactly
|
||||
main's? (The Version Gate check requires this of a head the merge step did not stamp.)
|
||||
If any answer is no, fix it before continuing.
|
||||
|
||||
**After any CHANGELOG edit that moves, adds, or removes entries,** immediately run
|
||||
@@ -93,13 +101,9 @@ order. Gaps between version numbers are fine. A branch that ships at v1.6.4.0 wi
|
||||
a prior v1.5.2.0 or v1.5.3.0 entry on main is correct — those were branch-internal
|
||||
version numbers that never landed. Do not back-fill gaps with placeholder entries.
|
||||
|
||||
**Never orphan branch-internal versions.** If your branch bumped VERSION several times
|
||||
during development (v1.5.1.0 → v1.5.2.0 → v1.6.4.0, say) and those earlier entries were
|
||||
never released to main, the final ship consolidates ALL of them into a single entry at
|
||||
the final version (v1.6.4.0). Collapse them — delete the old entries and move their
|
||||
content into the final entry, re-version table columns accordingly. Readers see one
|
||||
release, not a branch diary. Gaps are fine (v1.6.3.0 → v1.6.4.0 with no v1.5.x
|
||||
in between on main is correct).
|
||||
**Keep one entry per branch.** When the branch's work grows after its fragment was
|
||||
written, rewrite that fragment as one unified entry instead of adding a second file
|
||||
beside it. Readers see one release, not a branch diary.
|
||||
|
||||
CHANGELOG.md is **for users**, not contributors. Write it like product release notes:
|
||||
|
||||
@@ -122,9 +126,9 @@ we got here. Keep out of the CHANGELOG, always:
|
||||
documents what DID ship, not what MIGHT ship.
|
||||
- Version-bump housekeeping when no user-facing work actually landed.
|
||||
|
||||
If the diff between the base branch version and this version has no user-facing change
|
||||
(only merges, only CHANGELOG edits, only placeholder work), the honest entry is one
|
||||
sentence: "Version bump for branch-ahead discipline. No user-facing changes yet." Stop
|
||||
If the diff between the base branch and this branch has no user-facing change
|
||||
(only merges, only CHANGELOG edits, only placeholder work), the honest fragment is one
|
||||
sentence: "No user-facing changes yet." Stop
|
||||
there. Do not pad. Do not explain the plan that will ship eventually. Do not narrate
|
||||
the branch's history. When real work lands, the entry will replace this at /ship time.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -206,7 +206,16 @@ GSTA-203 chose them by a 3-run A/B against 20 × `ubicloud-standard-4` at k = 1
|
||||
(the PR that set them has the table). A `workflow_dispatch` run may
|
||||
override them with `free_runner` (`ubicloud-standard-2`, `-4` or `-8`),
|
||||
`free_shards` (1–40) and `free_jobs` (1–8) 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
|
||||
its job summary says so, and it is never cited as a PR or train Free Tests result.
|
||||
Each `free-suite` job's GitHub timeout comes from the plan, not a fixed number
|
||||
(#3850): `--ci-plan --job-setup-seconds $FREE_SUITE_SETUP_SECONDS` emits every
|
||||
shard's `timeout_minutes` as the shard's runner wall (the packed wall above),
|
||||
plus the flaky-retry pass's wall (at most 5 files, so the 6-minute base), plus
|
||||
the job's setup and teardown (`ciShardJobTimeoutMinutes`). So GitHub never kills
|
||||
a hung file before the runner names it and retries it. A shard that would need
|
||||
more than 90 minutes fails the plan with `FREE_JOB_TIMEOUT_OVER_CAP`; plan more
|
||||
shards or a higher k instead. The plan step prints the emitted timeouts and adds
|
||||
them to its job summary. 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
|
||||
|
||||
+8
-5
@@ -188,11 +188,14 @@ digest's first line shows its gstack version; re-copy it after upgrading.
|
||||
For Codex, setup reads the top-level `model` from
|
||||
`${CODEX_HOME:-~/.codex}/config.toml` and generates the matching behavioral
|
||||
profile, falling back to `gpt-6-astra` when no usable model is configured.
|
||||
`gpt-5.6-sol` automatically receives bounded-scope instructions that
|
||||
finish the requested lake without expanding into adjacent cleanup or speculative
|
||||
hardening. The Sol profile is exact-match only: dated snapshots and other 5.6
|
||||
variants get the generic GPT profile, and setup warns on near-misses like
|
||||
`gpt-5.6-sol-2026-08-01`. Override detection with `./setup --host codex --model <id>` — the
|
||||
The current OpenAI tiers are `gpt-6-astra` (frontier), `gpt-6.1-sol` (smart, the
|
||||
default eval tier) and `gpt-6-luna` (lowest cost); `gpt-6.1-sol`
|
||||
and `gpt-6-luna` get the generic GPT profile. Only `gpt-5.6-sol` receives the
|
||||
bounded-scope instructions that finish the requested lake without expanding into
|
||||
adjacent cleanup or speculative hardening, because that patch was measured on 5.6
|
||||
Sol; extending it to `gpt-6.1-sol` waits for its own scope eval. The Sol profile is
|
||||
exact-match only: dated snapshots and other 5.6 variants get the generic GPT
|
||||
profile, and setup warns on near-misses like `gpt-5.6-sol-2026-08-01`. Override detection with `./setup --host codex --model <id>` — the
|
||||
override applies to that run only; set `model` in your Codex `config.toml` to
|
||||
make it stick across upgrades. After changing your Codex model, rerun
|
||||
`./setup --host codex` to regenerate the skills.
|
||||
|
||||
+69
-13
@@ -74,7 +74,7 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`).
|
||||
|
||||
| Key | Meaning |
|
||||
|---|---|
|
||||
| `runner` | the local test command with `{files}` (gstack: `bun test --timeout=30000 {files}`, the 30 s per-test timeout CI uses); unset, it is detected (bun, vitest, jest, pytest, `node --test`, and `flutter test`/`dart test` per Dart package root, through fvm when that package or an ancestor pins it with `.fvmrc` or `.fvm/fvm_config.json` and fvm is on PATH; `cargo test -p <name>` per Rust crate, run from the root of the workspace the crate is a member of (Cargo's `members` globs, `exclude` and `package.workspace`), else the crate directory; a JS/TS test whose nearest package.json is a nested package declaring vitest or jest (a dependency or its `test` script) runs from that package, with package-relative paths, through the package manager its `packageManager` field or the nearest lockfile up to the repo root names: `pnpm exec`, `yarn`, `bun x`, else `npx`; every other JS/TS test keeps the root detection); nothing detected, a Dart SDK, cargo or package manager missing from PATH, a `.dart` file with no `pubspec.yaml` above it, a `.rs` file outside any crate, or one in a crate whose name Cargo would reject (anything but letters, digits, `_` and `-`), is `incomplete` (the runnable groups still run, so a real failure still fails) |
|
||||
| `runner` | the local test command with `{files}` (gstack: `bun test --timeout=30000 {files}`, the 30 s per-test timeout CI uses); unset, it is detected (bun, vitest or jest for JS/TS files, pytest when python3 can import it (else `pytest` or `python3` is named missing), `node --test`, and `flutter test`/`dart test` per Dart package root, through fvm when that package or an ancestor pins it with `.fvmrc` or `.fvm/fvm_config.json` and fvm is on PATH; `cargo test -p <name>` per Rust crate, run from the root of the workspace the crate is a member of (Cargo's `members` globs, `exclude` and `package.workspace`), else the crate directory; a JS/TS test whose nearest package.json is a nested package declaring vitest or jest (a dependency or its `test` script) runs from that package, with package-relative paths, through the package manager its `packageManager` field or the nearest lockfile up to the repo root names: `pnpm exec`, `yarn`, `bun x`, else `npx`; a JS/TS test file that imports `@playwright/test` at the start of a line runs as `playwright test --reporter=list` from its nearest package through that same package manager, whatever the unit runner is, a type-only import (`import type …`, or one whose every specifier is `type X`) does not count, file arguments are passed as escaped regexes anchored to the path (Playwright reads them as regexes), and a run where every test fails and Playwright says its browser is not installed is `PREGATE_ENVIRONMENT` (a partial install, where some tests pass, stays the diff's failure); every other test keeps the root detection, split by language when no one runner fits them all, so `.mjs` and `.py` root tests each get their own runner); nothing detected, a Dart SDK, cargo or package manager missing from PATH, a `.dart` file with no `pubspec.yaml` above it, a `.rs` file outside any crate, or one in a crate whose name Cargo would reject (anything but letters, digits, `_` and `-`), is `incomplete` (the runnable groups still run, so a real failure still fails) |
|
||||
| `dependencies` | glob → test files for subprocess targets, runtime reads and generated inputs the import graph cannot see; `[]` declares "no lane needed" (docs, licences). A `[]` that matches a deleted test does not skip it: the deleted test still runs its owning suite, and a file naming it that `[]` covers (a durations list) counts as covered. With this key present, even empty, the policy [declares lanes](#lane-requirements) |
|
||||
| `empty_lanes` | extra globs for the docs/no-tests empty lane. Any Markdown file already qualifies with no pin file when no test depends on it (the same import graph as other files: a quoted path, a `join()` of path segments, an import, or a `dependencies` entry). reStructuredText and AsciiDoc count as Markdown here. README, CHANGELOG, CONTRIBUTING, LICENSE, AGENTS and CLAUDE qualify even without `.md`, and so does licence text under any vendored name (`roboto-LICENSE.txt`, `LICENSE-MIT`, `licenses/*.txt`). Source code and files under a test directory never qualify. A test that reads the file still selects that test |
|
||||
| `selection` | `workflow.yml/job` → a command printing the lane's file list, for matrices a planner computes (gstack: `scripts/test-free-shards.ts --list`) |
|
||||
@@ -85,11 +85,11 @@ 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)) |
|
||||
| `remote_gate_runs` | `{ profiles, inventory }`: the run profiles that together clear `pregate.json/remote_gate`, and the command that prints the remote gate's test files, one per line, which their runs must cover (see [run evidence](#run-evidence)) |
|
||||
| `remote_gate_runs` | `{ profiles, inventory, skipped? }`: the run profiles that together clear `pregate.json/remote_gate`, the command that prints the remote gate's test files, one per line, which their runs must cover, and an optional command that prints which selected files the gate itself never runs (see [run evidence](#run-evidence)) |
|
||||
| `allowlist` | `[{ finding, paths?, pattern?, reason }]`: benign findings the `secrets` check accepts and names as allowlisted, by gstack-redact finding id or non-secret category, path globs and a pattern anchored at the finding; never `env.value` (see [allowlist](#allowlist)) |
|
||||
|
||||
Only the `origin/<base>` copy or the user-owned file below is honoured, so a
|
||||
@@ -220,6 +220,26 @@ Two stages with separate clocks, both printed:
|
||||
other line holding the old value still fail. Only the row itself is exempt:
|
||||
the old value after its closing bracket on the same line still fails. Rows in object form (`{ v: 45 }`) or spread over several
|
||||
lines are not recognized as history rows.
|
||||
A one-character old value (`2`) counts only in a statement that names the
|
||||
constant: on the name's line, or on up to three lines that continue it (the
|
||||
line before ends with `(`, `[`, `{`, `,`, `.`, `=` or another operator, or the
|
||||
line starts with `.`, `)`, `]`, `}` or an operator), never on a comment line, plus the history rows
|
||||
above. A `$2` in a shell stub, a comment, or `export default 2` in a fixture
|
||||
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
|
||||
@@ -437,12 +457,19 @@ test-tier command:
|
||||
Per-file lines come from a progress adapter where the runner's reporter
|
||||
provides them: `bun test` (its `<file>:` headers) is the only adapter today;
|
||||
a one-file run (each serial file) reports its file from the launch for any
|
||||
runner. Other runners (node --test, vitest, jest, pytest, flutter test, dart test, cargo test) get heartbeats
|
||||
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
|
||||
@@ -734,6 +761,16 @@ Then, for the lane:
|
||||
while the working tree is pregate.json's tree (checked before and after it
|
||||
runs); an empty or failing inventory refuses, and it runs under `pipefail`,
|
||||
so a pipeline whose first command fails refuses too.
|
||||
- When `remote_gate_runs` declares `skipped`, that command runs the same way,
|
||||
with `PREGATE_FILES_FILE` naming a file of the obligation's selected test
|
||||
files, one per line, and prints the ones the remote gate itself never runs
|
||||
(gstack's gate leaves paid test files to the paid PR gate). Those files need
|
||||
no run, and the receipt counts them as `skipped=<n>`. A file the inventory
|
||||
prints stays required even when `skipped` names it, and a failing `skipped`
|
||||
command refuses (`skipped_failed`). When `skipped` names every selected file,
|
||||
no selected test ran, so the clear refuses (`paid_only`), just as the gate's
|
||||
own `tests=0` report never clears. Without `skipped`, every selected file
|
||||
must appear in a run's `files`.
|
||||
- Every run is read once more after the inventory: a run that moved to a new
|
||||
attempt or stopped being a success meanwhile refuses (`run_changed`).
|
||||
|
||||
@@ -745,13 +782,14 @@ verification is named with its own reason (`fork_run`, `event_mismatch`,
|
||||
`os_mismatch`, `api_error`, …); the clear-level codes are `base_mismatch`,
|
||||
`no_verified_run`, `tree_mismatch` (runs on different trees),
|
||||
`tree_not_stamp_only`, `no_tests`, `inventory_failed`, `inventory_empty`,
|
||||
`inventory_uncovered` (the remote gate) and `tests_uncovered` (a lane's own
|
||||
`skipped_failed`, `paid_only`, `inventory_uncovered` (the remote gate) and `tests_uncovered` (a lane's own
|
||||
selected tests). A wrong run count, a repeated run or a lane with no
|
||||
profile is a usage error (exit 2), not a REUSE refusal.
|
||||
|
||||
Accepted, `clear` prints `REUSE: accepted <lane> <receipt>`, and the receipt
|
||||
reads `remote-gate tree=<t12> tests=<n> runs=<id>/<attempt>,… profiles=<p>,…
|
||||
heads=<h12> verified=<time>` (with `accepted-for=` and the substitution for a
|
||||
heads=<h12> verified=<time>` (with `skipped=<n>` after `tests=` when the
|
||||
`skipped` command excused selected files, and `accepted-for=` and the substitution for a
|
||||
stamp-only predecessor). A lane with its own profile gets `run tree=… runs=…`
|
||||
the same way; for such a lane `--run-url` is verified as `--run` is, and
|
||||
`--bundle-lane` is refused. A lane with no profile still clears by
|
||||
@@ -796,7 +834,21 @@ every non-matrix job and the `free-tests` aggregate, `os` Linux) and
|
||||
aggregate, `os` Windows). Each accepts `pull_request` and `workflow_dispatch`
|
||||
runs and reads the `tested-tree` artifact, which both aggregates upload only
|
||||
after their strict verification passes. The main-push reuse job and
|
||||
`--from-run` read them. No lane has a profile, so every macOS and Windows lane
|
||||
`--from-run` read them, and `remote_gate_runs` names `ci-reuse/free-tests`,
|
||||
so a green Free Tests run at pregate.json's tree clears
|
||||
`pregate.json/remote_gate` without a Ubicloud VM:
|
||||
|
||||
```bash
|
||||
gstack-pregate clear --remote pregate.json/remote_gate --run <free-tests run id or url> --allow-repo-commands
|
||||
```
|
||||
|
||||
Its `inventory` is `scripts/test-free-shards.ts --list` (the complete free
|
||||
suite, the same set the Free Tests plan shards), and its `skipped` command
|
||||
prints the selected files `test/helpers/paid-test-set.ts` marks paid, the
|
||||
same split `scripts/ubicloud/pregate-remote.sh` makes before it runs. A
|
||||
`pull_request` run counts only when the branch already contains its base
|
||||
(the merge commit's tree is the head's), so a branch behind main still needs
|
||||
the gate or a fresh run. No lane has a profile, so every macOS and Windows lane
|
||||
still clears by `--run-url`, checked for its workflow file and a green
|
||||
conclusion. On main the free tier's Windows smoke lane is
|
||||
`windows-smoke.yml/windows-smoke`, and a `windows-smoke.yml` run clears it.
|
||||
@@ -994,7 +1046,7 @@ failure the branch caused carries `cause: "diff"` (`PREGATE_LANE_FAILED`,
|
||||
|
||||
| Kind | Evidence |
|
||||
|---|---|
|
||||
| `missing-binary` | exit 127, `command not found`, a spawn `ENOENT`, a runner tool `not on PATH` (dart, flutter, cargo, a package manager) |
|
||||
| `missing-binary` | exit 127, `command not found`, a spawn `ENOENT`, a runner tool `not on PATH` (dart, flutter, cargo, a package manager, `python3`, or `pytest` when python3 cannot import it), Playwright's browsers not installed |
|
||||
| `network` | `ENOTFOUND`, `ECONNREFUSED`, `ECONNRESET`, `ETIMEDOUT`, `EAI_AGAIN`, `Could not resolve host`, `fetch failed`, a TLS handshake or certificate failure |
|
||||
| `quota` | `quota`, `429 Too Many Requests`, `rate limit`, `insufficient_quota` (a vCPU quota on the remote backend) |
|
||||
| `disk` | `ENOSPC`, `No space left on device`, `EDQUOT` |
|
||||
@@ -1005,7 +1057,11 @@ The classifier is conservative. A local lane that exited non-zero is read from
|
||||
its output **only when its runner reported no test summary** (nothing ran) and
|
||||
the runner was not seen starting a test file; a run whose summary counts `N
|
||||
ran, M failed` is the diff's failure whatever its log says, so a test that
|
||||
failed on `ECONNREFUSED` is still a red test. A remote gate that exited
|
||||
failed on `ECONNREFUSED` is still a red test. One exception: when every
|
||||
failed run is a Playwright run in which every test failed and Playwright says
|
||||
its browsers are not installed, the lane is `missing-binary` with `proof:
|
||||
"output"` (a partial install, where some tests pass, stays the diff's
|
||||
failure). A remote gate that exited
|
||||
without a `PREGATE_REMOTE:` line is read from its **preamble only** (its first
|
||||
40 lines, before any test output could stream): a VM refused for quota or an
|
||||
unreachable API there never ran the tests, so it is environment and the
|
||||
|
||||
@@ -263,7 +263,7 @@ line whose input could not be read says `unavailable (<reason>)` rather than 0.
|
||||
gstack-factory-scorecard --repo owner/name --since-days 7 \
|
||||
[--crew login,login] [--gate-workflow evals.yml] [--free-workflow free-tests.yml] \
|
||||
[--train-prefix train/] [--ready-marker 'READY #\d+|SHIP_RECEIPT:'] [--pass-marker '"passes": (\d+)|passes=(\d+)'] \
|
||||
[--pass-cap 3] [--out-dir .context/factory-scorecard] [--now <iso>]
|
||||
[--pass-cap 2] [--out-dir .context/factory-scorecard] [--now <iso>]
|
||||
```
|
||||
|
||||
`--repo` defaults to the origin remote's owner/name on github.com (or
|
||||
@@ -280,7 +280,7 @@ measured later does not count anything that happened after that week.
|
||||
| Merged PRs | `merged_at` in the window, base = default branch; crew by `--crew` logins, else author association OWNER/MEMBER/COLLABORATOR | several merges a day, every day; outside contributions land in the same week |
|
||||
| Lead time, created → merged | PR `created_at` to `merged_at`, median and p90 | median under 24h, p90 under 3 days (DORA "elite" change lead time is under a day) |
|
||||
| Lead time, READY → merged | first PR comment matching `--ready-marker` at or before `merged_at`; PRs with no such comment are left out and counted | under 2h; every merged PR carries a READY comment |
|
||||
| Review passes per PR | the largest count `--pass-marker` captures in the PR body (the ship receipt's `"passes": N` once /ship writes it) or a PR comment (`passes=N`, `REVIEW_PASSES: N`), median and p90, plus how many PRs sat at `--pass-cap` (3: pass 1 reviews the whole diff, later passes only the delta, a pass that finds nothing new is the exit) | median 2 passes; none at the cap |
|
||||
| Review passes per PR | the largest count `--pass-marker` captures in the PR body (the ship receipt's `"passes": N` once /ship writes it) or a PR comment (`passes=N`, `REVIEW_PASSES: N`), median and p90, plus how many PRs reached `--pass-cap` (2, the [review policy](review-policy.md)'s cap of two rounds, a fix-check included: pass 1 reviews the whole diff, pass 2 only the range since, and a pass that finds nothing blocking is the exit) | median 1 pass; few at the cap |
|
||||
| Gate runs green / red / cancelled | completed runs of the gate workflow (`workflow_dispatch` by default; `--gate-event any`); cost from the run's `report-verdict-a*` artifact (`collector-outcomes.json` `.verdict.costUsd`), else the `**$N** rule/judge cost` or `cost=$N` line of the PR's verdict comment | green on 95%+ of runs, cancelled near 0, cost known for every run |
|
||||
| Known gate cost per merged PR | known cost / merged PRs; unknown-cost runs make it a floor | known, stable or falling week over week |
|
||||
| Red-train rate | runs whose head branch starts with `--train-prefix`, cancelled excluded; red = conclusion other than success; red backstops and screens shown beside it | under 5% (each red train is a bisect plus a rebuild, about two gates) |
|
||||
|
||||
+49
-7
@@ -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
|
||||
@@ -326,7 +333,8 @@ fix-checks run at `full` effort (the lane's reasoning); specialists run at
|
||||
batched specialist, which carries the Red Team prompt and stays `full`. The
|
||||
outside voice and the native adversarial pass carry the floor sentence and end
|
||||
each finding with `Severity:` and `Blocks: yes|no`; a finding is CRITICAL when
|
||||
its label rule says so (native FIXABLE, outside Critical or High) or it says
|
||||
its label rule says so (native FIXABLE, except as the collect step below
|
||||
relaxes it on a runtime-only code or docs plan; outside Critical or High) or it says
|
||||
`Blocks: yes`, and `Blocks: no` never lowers one (`gstack-review-dispatch collect`). Work that cannot block (the Step 7 coverage audit, the Step 8
|
||||
plan-completion audit, the Step 14.5 docs audit, decision-trail reviewers,
|
||||
watchers) runs at `light`: the same model at a lower reasoning effort, never a
|
||||
@@ -399,8 +407,19 @@ The brief states the blocking floor in one sentence, and its money block cites
|
||||
the checklist's Forced-CRITICAL rule on uncapped spend, so a money finding can
|
||||
block without new floor words. The reader is read-only, and `NO FINDINGS` is a
|
||||
result. `gstack-review-dispatch render --role targeted --classes <classes>`
|
||||
renders its brief; `verify --targeted-out <file>` writes the record that
|
||||
`gstack-ship-receipt write --review-targeted @<file>` puts in the receipt.
|
||||
renders its brief; `verify --targeted-out .gstack/tmp/targeted.json` writes the record that
|
||||
`gstack-ship-receipt write --review-targeted @.gstack/tmp/targeted.json` puts in the receipt
|
||||
(Step 19, the handoff and the driver pass it when the file exists). The record is `done` when
|
||||
the reader got its brief verbatim and its reply was collected on a committed diff, whatever
|
||||
happened to HEAD after, and it names the commit it read (`head`). `write` binds it only while
|
||||
that commit is an ancestor of HEAD, its product patch is the receipt's, and nothing changed
|
||||
after it carries a targeted class, with the gate scan run over that range: any product change
|
||||
after the read refuses with `RECEIPT_INVALID`, docs and TODOS.md commits included, and so
|
||||
does a test edit the gate scan tags (a skipped case). Every verify
|
||||
removes `--targeted-out` before it writes a record, so an earlier run's record never reaches
|
||||
Step 19. `write` and `carry` (which rescans the gate at the new head) also
|
||||
refuse a receipt whose head needs a class no `done` record covers (`RECEIPT_INVALID`,
|
||||
`RECEIPT_CARRY_REFUSED`). The queue refuses the same receipts at `ready` (#3859).
|
||||
`gstack-ship-queue ready`, `train build` and `land` print
|
||||
`REVIEW_TARGETED: required=<classes|none> receipt=<classes|none|legacy>
|
||||
verdict=ok|missing|failed` and refuse
|
||||
@@ -412,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
|
||||
@@ -429,11 +466,11 @@ puts in the receipt:
|
||||
| `review_loop.noted` | below-floor findings listed in the PR body |
|
||||
| `review_loop.fixed` | findings fixed in this PR |
|
||||
| `review_loop.scope` | the `review_scope` the loop ran under |
|
||||
| `review_loop.fix_checks` | fix-checks this ship ran (never passes) |
|
||||
| `review_loop.fix_checks` | fix-checks this ship ran (never passes): the larger of the `review-fix-check` rows and the ledgers holding a fix-check reader's review. When fix-checks ran without their rows, a blocker no row disposes counts as fixed once a later fix-check ledger followed it (decision 3.5 runs one only after the fixes) |
|
||||
| `review_loop.rounds` | passes plus fix-checks (R51 caps it at 2; the queue's `REVIEW_DEPTH:` line prints it) |
|
||||
| `review_loop.dispatches` | reviewer dispatches by role: `specialist`, `red-team`, `adversarial-native`, `outside-voice`, `fix-check`, `targeted` |
|
||||
| `review_loop.est_input_tokens` | prompt plus diff bytes the dispatches read, divided by 4: a relative estimate for comparing runs, not a bill |
|
||||
| `review_targeted` | the [targeted pass](#targeted-pass-on-money-gate-auth-and-secret-code): the `classes` it covered, `rules`, `brief_sha`, the receipt's own `product_patch_id`, `status` (`done` only with its output collected and the committed diff it was rendered on still the one at verify, else `failed`; absent when write got no record, which the queue reports as `legacy`), `findings` and `raised_by`; a carry keeps it while the product patch is unchanged |
|
||||
| `review_targeted` | the [targeted pass](#targeted-pass-on-money-gate-auth-and-secret-code): the `classes` it covered, `rules`, `brief_sha`, the receipt's own `product_patch_id`, `status` (`done` only with its brief sent verbatim and its output collected on a committed diff, else `failed`; `write` refuses a record when a change after its read commit carries a targeted class or the read commit is not an ancestor, and refuses to write without a record when the head needs a class), `findings` and `raised_by`; a carry keeps it while the product patch is unchanged |
|
||||
|
||||
Each review row may carry `dispatches: [{role, effort, prompt_bytes,
|
||||
diff_bytes, status}]`, and each finding `raised_by` (the reviewer that raised it,
|
||||
@@ -564,7 +601,11 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
second line for the same field starts another: `Severity` sets its label (a label leading the finding stays its floor),
|
||||
`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). An unnumbered, unlabeled bullet with no file:line
|
||||
adversarial voice's FIXABLE collects as CRITICAL (normalizer v2, GSTA-167) unless collect `--manifest` names this
|
||||
ledger's pass and its plan is the code or docs class with only `runtime=<n>` depth reasons (no `risk:`, `error:`,
|
||||
`history:` or `floor:` reason). On such a plan a native finding's own `Severity` field decides and FIXABLE beside it
|
||||
raises nothing once it also states `Blocks: no`; a native FIXABLE finding without `Blocks: no`, with no `Severity` field,
|
||||
or whose field names only FIXABLE or INVESTIGATE still collects as CRITICAL (GST-987). An unnumbered, unlabeled bullet with no file:line
|
||||
under a paragraph finding whose Severity or Blocks line has not come yet is that finding's detail, so the field line after it
|
||||
lands on the finding; a numbered item is its own finding.
|
||||
A negated severity word still collects at its label ("found nothing CRITICAL", "No P0/P1 issues"), so a reader's
|
||||
@@ -658,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)).
|
||||
|
||||
+153
-14
@@ -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`. |
|
||||
@@ -112,10 +113,16 @@ refuses the file by name (`POLICY_GSTACK_TOO_OLD`).
|
||||
`.gstack/ship-policy.example.json` shows every key set the way a serial-merge
|
||||
repo with a release script, docs mentions and a mirror would set them.
|
||||
|
||||
### Queue-side procedure for gstack
|
||||
<a id="queue-side-procedure-for-gstack"></a>
|
||||
### Queue-side procedure under `stamp-at-merge` without fragments
|
||||
|
||||
Who does what under `stamp-at-merge`, for the PR author and for the thread
|
||||
running the merge queue:
|
||||
gstack itself no longer follows these steps. Since #3654 (v1.91.342.0) its
|
||||
policy sets [`changelog_fragments`](#changelog-fragments), so a gstack PR adds
|
||||
a fragment and never a version, and trains merge through
|
||||
[`train land`](#train-land). The steps
|
||||
below are for a repo on `stamp-at-merge` whose policy has no
|
||||
`changelog_fragments`. Who does what there, for the PR author and for the
|
||||
thread running the merge queue:
|
||||
|
||||
1. **Open.** The branch carries a tentative stamp of the base's version plus
|
||||
PATCH (`gstack-version-bump write --version <base+patch> --regen-digest`)
|
||||
@@ -600,13 +607,20 @@ gstack-ship-queue wave settle --wave <nonce> [--dry-run] # after the wave
|
||||
|
||||
- **When.** `wave plan` counts the live READY PRs (open, head equal to their
|
||||
READY receipt, CI green at that head; a READY head whose CI is red, still
|
||||
running or absent prints a `NOT_GREEN` line and stays out of the plan). A
|
||||
READY PR whose CI, head or diff cannot be read fails the whole plan with
|
||||
`QUEUE_PROOF_MISSING`. Over the
|
||||
running or absent prints a `NOT_GREEN` line and stays out of the plan; a
|
||||
READY head that does not merge onto main with the train's merge rules prints
|
||||
`NOT_MERGEABLE` with the conflicting files and stays out too, while a
|
||||
conflict only in the CHANGELOG, which the train merges, does not count). A
|
||||
READY PR whose CI, head, diff or merge onto main cannot be read fails the
|
||||
whole plan with `QUEUE_PROOF_MISSING`. Over the
|
||||
trigger (6) it prints `WAVE_ADVISED` and suggested groups of 3 to 6: PRs
|
||||
whose changes to a common code file conflict (`git merge-tree` cannot merge
|
||||
the two heads, or the check itself fails) always share a group, so that
|
||||
conflict is resolved once. Hub
|
||||
conflict is resolved once. A PR that renames a file and one that edits the
|
||||
old path are linked when the edit conflicts at the new path. A group of
|
||||
linked PRs is never split across waves: one larger than the wave maximum
|
||||
prints `WAVE_OVERSIZE` with no build command (land part of it as single
|
||||
slots, then plan again). Hub
|
||||
files never link PRs: anything under `docs/`, Markdown (generated skill
|
||||
renders included; their `.tmpl` sources still link) and every `.json` or
|
||||
`.jsonl` file (ledgers and registries). Most PRs append to them, and
|
||||
@@ -1226,15 +1240,140 @@ both values; prose that quotes no counter is not a claim. The `## Approvals`
|
||||
block `gstack-approvals` renders (its heading, the Date | Asked | Exact words
|
||||
table and the `Source: gstack-approvals record` line) quotes the owner's
|
||||
words, not this PR's numbers, so it is skipped: "one Step 11 pass" in that table
|
||||
is not a `pass` claim. Text you write under the heading after that block, and
|
||||
a `## Approvals` example inside a code fence, are still checked. The
|
||||
`Review-loop exception: …` line the receipt's own exception requires is
|
||||
skipped the same way (GST-977): it quotes the owner's reason, so "outside 1 and
|
||||
2 skip detector" is not a `2 skip` claim. Only a line that matches the rendered
|
||||
line exactly is skipped, inside a code fence too, as the receipt check accepts
|
||||
a fenced copy; the same words inside a longer line are still checked. Data PRs (evals,
|
||||
is not a `pass` claim. Text you write under the heading after that block is
|
||||
still checked. The `Review-loop exception: …` line (GST-977) and the
|
||||
`Documentation exception: …` line (GST-983) the receipt's own exceptions
|
||||
require are skipped the same way: they quote the owner's reason, so "outside 1
|
||||
and 2 skip detector" is not a `2 skip` claim. Only a line that matches the
|
||||
rendered line exactly is skipped; the same words inside a longer line are still
|
||||
checked. A closed code fence is skipped too (GST-984): pasted test output
|
||||
("113 pass, 0 fail") and quoted code are evidence, not this PR's counters, so
|
||||
put a count you mean as a claim outside the fence. A line like ```` ```npm test``` ran ````
|
||||
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
@@ -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
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
+3
-1
@@ -105,7 +105,9 @@ lives in `projects/<slug>/`. `bin/gstack-slug` picks the slug from the git
|
||||
- Without a remote, the project root's directory name is used.
|
||||
|
||||
A bucket can also hold a user-owned `pregate.json` policy, for a repo that keeps
|
||||
`.gstack/` out of git (see [pregate v1](pregate.md#pregate-v1)).
|
||||
`.gstack/` out of git (see [pregate v1](pregate.md#pregate-v1)), and
|
||||
`pregate/runs/<run id>/`, the scratch each pregate run writes (lane and guard
|
||||
logs, the remote gate's file list); old runs are pruned when a new one starts.
|
||||
|
||||
The first slug gstack resolves in a directory sticks (it is cached under
|
||||
`slug-cache/`), so a project keeps its bucket when it adds a remote later.
|
||||
|
||||
+127
-11
@@ -570,6 +570,9 @@ the base has moved.
|
||||
**Meaning.** The working tree changed between the pass's `gstack-review-log --start` and
|
||||
its logged row: an edit, commit or merge landed while reviewers were still reading. The
|
||||
reviewers may not have seen that change, so the row's `review_binding.state` is `changed`.
|
||||
Review records fingerprint with `gstack-wtree --review`, which skips untracked root dot-folders
|
||||
that hold no tracked file (never `.github/` or `.gstack/`), so a test lane's scratch folder alone
|
||||
does not move the tree.
|
||||
Step 11.5 cannot bind it and inserts `9 → 10 → 11 → 11.5`. Logging the same results again
|
||||
cannot repair it.
|
||||
|
||||
@@ -2431,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
|
||||
@@ -3314,10 +3320,85 @@ for an older PR, write it once by hand with the same command.
|
||||
**Meaning.** The block exists but does not validate against the `ship-receipt`
|
||||
schema (`gstack-artifact schema ship-receipt`). `gstack-ship-receipt write`
|
||||
also refuses with this code, and writes nothing, when `--head` is not the
|
||||
checked-out HEAD: the receipt binds the tree the review read.
|
||||
checked-out HEAD: the receipt binds the tree the review read. It refuses the
|
||||
same way when the head touches money, gate, auth or secret code and no done
|
||||
targeted record covers those classes (`REVIEW_TARGETED: … verdict=missing|failed`),
|
||||
or when `--review-targeted` names a read commit whose product patch is not
|
||||
the receipt's and a change after it carries a targeted class (money, gate,
|
||||
auth or secret code changed after the targeted reader read it, a test edit the
|
||||
gate scan tags included), or that is not an ancestor of HEAD.
|
||||
|
||||
**Fix.** Regenerate it with `gstack-ship-receipt write`; never hand-edit the
|
||||
block. For the `--head` refusal, check out that commit and write there.
|
||||
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`
|
||||
|
||||
@@ -3695,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`
|
||||
@@ -3831,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
|
||||
@@ -3932,7 +4018,10 @@ and the table prints `environment: <kind> — <evidence>`. A test that failed an
|
||||
whose log also names a connection error is **not** this code: the runner's own
|
||||
summary counted a failure, so it is `PREGATE_LANE_FAILED` with `cause:
|
||||
"diff"`; nor is a runner, guard or gate script whose own binary is present but
|
||||
exits 127. `proof: "launch"` is certain (the command could not start, its
|
||||
exits 127. One exception: a Playwright lane where every test failed and
|
||||
Playwright says its browsers are not installed is `missing-binary` with
|
||||
`proof: "output"` (`npx playwright install`); a partial install stays the
|
||||
diff's failure. `proof: "launch"` is certain (the command could not start, its
|
||||
binary is off PATH, the runtime or a gstack tool refused); `proof: "output"`
|
||||
means a rule matched the command's text before any test was counted, which the
|
||||
diff's own output could have written, so the line says `(inferred from output)`
|
||||
@@ -5468,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`
|
||||
|
||||
@@ -5812,7 +5924,10 @@ working tree's status. It also refuses when an owner exception the receipt recor
|
||||
no longer stands, and, for a receipt from a gstack that records review
|
||||
depth, when HEAD needs a deeper review than the receipt records, `--head` is
|
||||
not the checked-out HEAD, or the receipt records `gstack_version` without
|
||||
`review_depth` ([ship-policy.md](ship-policy.md#ship-receipt)). The receipt in
|
||||
`review_depth` ([ship-policy.md](ship-policy.md#ship-receipt)). It refuses, too, when
|
||||
HEAD needs a targeted class (money, gate, auth, secret) that the receipt's
|
||||
`review_targeted` does not cover with a done pass, since the queue would refuse
|
||||
that receipt. The receipt in
|
||||
the body is unchanged and still names the old head, so `gstack-ship-queue
|
||||
ready` refuses `QUEUE_RECEIPT_STALE`.
|
||||
|
||||
@@ -5820,7 +5935,7 @@ ready` refuses `QUEUE_RECEIPT_STALE`.
|
||||
`gstack-ship-receipt carry --pr <n>` at the new head. When the history is
|
||||
already rewritten and the receipt predates `head_tree`, run /ship on the new
|
||||
head; next time merge the base in instead of rebasing. For a retargeted stacked
|
||||
PR whose older receipt lost its parent branch, or a depth or exception
|
||||
PR whose older receipt lost its parent branch, or a depth, targeted or exception
|
||||
refusal, run /ship on the new head. When `--head` is not the checked-out HEAD,
|
||||
check it out and carry there.
|
||||
|
||||
@@ -5915,8 +6030,9 @@ body that names a counter the receipt records (`passes=3` against
|
||||
against `gate.passed: 41`) with a different value. The receipt is written from
|
||||
the run; the prose was typed, so a reader of the body would learn a number the
|
||||
run did not produce. The `PROSE:` line names each field with both values. The
|
||||
`## Approvals` table `gstack-approvals` renders, and the `Review-loop exception: …`
|
||||
line the receipt's exception requires, are never read as claims.
|
||||
`## Approvals` table `gstack-approvals` renders, the `Review-loop exception: …`
|
||||
and `Documentation exception: …` lines the receipt's exceptions require, and
|
||||
the inside of a closed code fence are never read as claims.
|
||||
|
||||
**Fix.** Correct the body to the receipt's number. When the receipt is the
|
||||
stale one (the run was repeated), rerun `/ship` so the receipt is rewritten
|
||||
|
||||
+41
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user