mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 10:40:32 +01:00
merge origin/main (pre-READY base merge, v1.94.0.0; keep both review_exclude_paths and release_gate)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> # Conflicts: # lib/ship-policy.ts
This commit is contained in:
commit
62bbf6de0f
198 files changed
+6858
-770
No files matched your search
@@ -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).
|
||||
|
||||
+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.
|
||||
|
||||
+129
-7
@@ -85,11 +85,12 @@ 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, 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)) |
|
||||
| `ship_lanes_from` | maps each exact /ship lane command to `{ profiles, inventory }`: the run profiles whose verified runs at HEAD's clean tree carry that lane instead of running it, and the command listing its test files, which their runs must cover (see [CI carry](#ci-carry)); read from the base branch only |
|
||||
| `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
|
||||
@@ -173,7 +174,7 @@ The list lives in its own file, not in `pregate.json`, because older pregate rea
|
||||
|
||||
```
|
||||
gstack-pregate [--stage preflight|tests|all] [--only <check,..>] [--allow-repo-commands] [--policy-from base|head]
|
||||
[--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--from-run <id|url>] [--explain] [--json]
|
||||
[--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--from-run <id|url>] [--no-ci-carry] [--explain] [--json]
|
||||
gstack-pregate verify | clear --remote <workflow.yml/job> --run-url <url> | lanes | validate | init
|
||||
```
|
||||
|
||||
@@ -459,10 +460,17 @@ test-tier command:
|
||||
a one-file run (each serial file) reports its file from the launch for any
|
||||
runner. Other runners (node --test, vitest, jest, pytest, playwright test, flutter test, dart test, cargo test) get heartbeats
|
||||
only. A heartbeat follows each 60 s with no progress line;
|
||||
- is killed as **stalled** when its log does not grow for the stall timeout
|
||||
(`--stall-timeout <s>`, else `stall_timeout_s`, else 300; `0` disables): the
|
||||
check is `incomplete` with `PREGATE_STALLED` and names the last file
|
||||
started; the next serial file still runs. The `--remote` gate is exempt: a
|
||||
- is killed as **stalled** when its log does not grow and its process group
|
||||
stays under a tenth of one CPU for the stall timeout (`--stall-timeout <s>`, else
|
||||
`stall_timeout_s`, else 300; `0` disables): the check is `incomplete` with
|
||||
`PREGATE_STALLED` and names the last file started; the next serial file
|
||||
still runs. The group counts as busy when its CPU time, sampled each second,
|
||||
grew by at least a tenth of the wall time, so a quiet suite that is working
|
||||
(FOUN-63: a parallel `check.sh` silent for 9 minutes) is not killed, while
|
||||
a hang that waits on nothing is. A hang that keeps burning CPU is left to
|
||||
the timeout. Linux reads `/proc` (exited children's time included once
|
||||
reaped); macOS and other BSDs read `ps -S`; a failed read skips that
|
||||
sample. Windows reads no CPU time here and uses log growth alone. The `--remote` gate is exempt: a
|
||||
gate that waits on CI is quiet by design, so only its timeout bounds it, and
|
||||
its heartbeats add `; last output: <the gate's last line>`;
|
||||
- is killed as **timed out** when that runner process reaches its timeout
|
||||
@@ -486,8 +494,15 @@ the selected local files as a `requires-remote` obligation with the command to
|
||||
run, `pregate.json/remote_gate` (the declared command) or
|
||||
`test_backend/<backend>` (gstack's `scripts/ubicloud/ubi-runner.sh run --src
|
||||
<repo> -- '<runner> <files>'`). Without either, the tier runs locally under
|
||||
the supervision above. Close the obligation one of three ways:
|
||||
the supervision above. Close the obligation one of four ways:
|
||||
|
||||
- by default, when the base declares `remote_gate_runs`, let CI clear it: the
|
||||
tests stage clears it on its own from a verified run at this exact tree
|
||||
([CI carry](#ci-carry)), and the uncleared obligation's detail names that
|
||||
route first (`push, then gstack-ci-carry wait --base <base> and rerun
|
||||
gstack-pregate --allow-repo-commands`, the whole pregate, so the record keeps
|
||||
its preflight checks) and the VM only as the opt-in (under `--no-ci-carry` it
|
||||
names the VM route); or
|
||||
- after running it, `gstack-pregate clear --remote pregate.json/remote_gate
|
||||
--receipt <file>` with the gate's output, validated as `--remote` validates it
|
||||
(a run URL is refused, `PREGATE_RECEIPT_INVALID` for a bad receipt; a receipt
|
||||
@@ -900,6 +915,113 @@ as a usage error. It changes nothing in the preflight stage: with `--stage all`
|
||||
the preflight checks, the secrets scan included, run as before, and
|
||||
`--stage tests` runs no preflight check in that invocation, with or without it.
|
||||
|
||||
<a id="ci-carry"></a>
|
||||
### CI carry: pregate and /ship use CI's run instead of running again (GSTA-258)
|
||||
|
||||
CI is the one full test run. When a verified run already passed at the exact
|
||||
tree being tested, pregate's tests stage and /ship's test lanes use it instead
|
||||
of running the suite a second time. `lib/ci-carry.ts` `findCarry` decides, and
|
||||
it accepts a run only when all of these hold:
|
||||
|
||||
- The working tree is HEAD's committed tree (the `gstack-wtree` fingerprint
|
||||
equals `HEAD^{tree}`), read before the lookup and again after it. A dirty
|
||||
tree, an untracked file or an edit made during the lookup never carries.
|
||||
- The newest of HEAD's runs (up to 10) under each named profile passes
|
||||
`verifyRun` above: same-repository head (a fork run never counts), the
|
||||
profile's workflow, event and jobs, the newest attempt, no newer red or
|
||||
running run on HEAD, within `max_age_hours`, and a `tree.json` naming HEAD's
|
||||
tree. A `pull_request` run counts only when the pull request already
|
||||
contained its base.
|
||||
- Every run executed tests, and with an inventory command, the runs'
|
||||
`tree.json` files cover every file it prints. Each run is read once more
|
||||
afterward (`run_changed`).
|
||||
|
||||
Security and advisory scans, the sealed guard, guards and every preflight-stage
|
||||
check always run; only test lanes carry.
|
||||
|
||||
**The tests stage.** Without `--from-run`, when the base branch declares
|
||||
`run_profiles`, `gstack-pregate` looks up the newest verified run at HEAD under
|
||||
the profiles `--from-run` would use and applies it as `--from-run` does: the
|
||||
selected local files its `tree.json` lists count as passed, and only the rest
|
||||
run. On a small machine a selection the run covers records no remote gate
|
||||
obligation. It prints `CI_CARRY: accepted <receipt>` or `CI_CARRY: none
|
||||
(<reason>)` on stderr; `--no-ci-carry` skips the lookup.
|
||||
|
||||
When files are left over, the remote gate obligation still clears from the
|
||||
same run when the base declares `remote_gate_runs` naming that run's profile:
|
||||
the run must pass every `clear --run` check, with the carried files counted
|
||||
as selected files. So a remainder of only files the gate's `skipped` command
|
||||
names (gstack's paid evals, which the gate itself reports as
|
||||
`PREGATE_REMOTE_PAID_ONLY` with `tests=0`) clears with `skipped=<n>` in the
|
||||
receipt instead of becoming an obligation nothing can clear. The line is
|
||||
`CI_CARRY: cleared pregate.json/remote_gate <receipt>`, or `CI_CARRY:
|
||||
pregate.json/remote_gate stays uncleared (<reason>)` when a check refuses, for
|
||||
example a gate inventory file missing from the run's `tree.json`.
|
||||
|
||||
**gstack's own gate.** gstack's `.gstack/pregate.json` declares
|
||||
`remote_gate_runs` with the `ci-reuse/free-tests` profile, so on gstack a green
|
||||
Free Tests run at the same tree is how the remote gate obligation clears, and
|
||||
the Ubicloud VM (`gstack-pregate --stage tests --remote`) is an opt-in for when
|
||||
no such run exists. The order after a push: wait for Free Tests
|
||||
(`gstack-ci-carry wait --base main`), then `gstack-pregate
|
||||
--allow-repo-commands`. A `pull_request` run tests GitHub's merge commit, so on
|
||||
a branch that does not contain the base tip it tested another tree and
|
||||
`CI_CARRY` says `none (no_verified_run: … tree_mismatch)`: dispatch Free Tests
|
||||
on the branch
|
||||
(`gh workflow run free-tests.yml --ref <branch>`), which tests HEAD's own tree,
|
||||
and wait again. The preflight stage (generated-file freshness, secrets, strays
|
||||
and the other mechanical checks) and the sealed guard are unchanged and always
|
||||
run here. The macOS and Windows lane
|
||||
obligations still clear with `gstack-pregate clear --remote <lane> --run-url
|
||||
<url>` from their CI runs.
|
||||
|
||||
**/ship lanes: `ship_lanes_from`.** A lane carries only when the base branch's
|
||||
`.gstack/pregate.json` maps its exact command (the evidence ledger binds the
|
||||
command's sha256; a label alone never carries) to the profiles whose runs carry
|
||||
it and the command that lists its test files:
|
||||
|
||||
```json
|
||||
"ship_lanes_from": {
|
||||
"bun run test": { "profiles": ["ci-reuse/free-tests"], "inventory": "bun --no-env-file --config=/dev/null run scripts/test-free-shards.ts --list | sed -n 's/^ //p'" }
|
||||
}
|
||||
```
|
||||
|
||||
A carried lane prints `LANE: <label> carried run=<id>/<attempt> tests_ran=<n>`
|
||||
and writes an explicit CI-sourced ledger row (`gstack-evidence record-ci
|
||||
--label <label> --base <base> -- <command>`, which finds and verifies the run
|
||||
itself, so no caller hands it run ids or a test count: `source: "ci"`, its runs
|
||||
and profiles, the run's finish time as `ts`, bound to the clean tree), never a
|
||||
fictitious local run. `gstack-evidence check` grades it
|
||||
like any row and names `source=ci runs=…`, so `finish`'s FRESH check and the
|
||||
24-hour age limit apply to the CI run's own time. A mapped lane that cannot
|
||||
carry prints `CI_CARRY: none label=<label> (<reason>)` and runs here. Every
|
||||
other lane runs through `gstack-evidence run --reuse`: the newest row for its
|
||||
label, command and working tree is reused when it passed with a counted run
|
||||
under the same runtime (bun and node versions, OS, architecture, runner image)
|
||||
within 24 hours; a newer failure on that tree vetoes an older pass, and the
|
||||
reused copy keeps the original timestamp. The cache is this machine's ledger
|
||||
only; across machines, CI is the trust boundary.
|
||||
|
||||
**Finish, push and accept.** `gstack-ship-release finish` commits the release
|
||||
metadata, so the stamped tree has no CI run yet. A mapped lane that is not
|
||||
already FRESH is deferred (`EVIDENCE: DEFERRED`), and the pregate runs only its
|
||||
preflight stage on the stamped tree, written to its own file so the full
|
||||
`.gstack/tmp/pregate.json` stays in place until `accept` replaces it. `gstack-ship-release push --early` (the
|
||||
Phase 2 push that lets CI run beside the review) runs the secrets scan and the
|
||||
sealed guard before it pushes. After the final push,
|
||||
`gstack-ship-release accept --base <base> --lane …` waits (bounded by `--wait`)
|
||||
for the run at the pushed HEAD, stops on a red or still-running run, carries
|
||||
each mapped lane from the green run at that exact tree, reuses or runs the rest,
|
||||
checks every lane FRESH and reruns the pregate there, whose tests stage carries
|
||||
the same run. `gstack-ship-receipt write` records the carried runs as
|
||||
`ci_carried: [{ lane, run, attempt, tree }]`, and the `SHIP_RECEIPT:` line names
|
||||
them (`ci_carried=<lane>:<run>/<attempt>`).
|
||||
|
||||
**`gstack-ci-carry`.** `find [--profile <name>]… [--command <exact lane command>
|
||||
--allow-repo-commands]` prints the verdict for HEAD; `wait [--timeout <s>]`
|
||||
polls the newest run at HEAD until it completes (run it in the background) and
|
||||
prints `CI_WAIT: green|red|running|none`, then `find`'s verdict on green.
|
||||
|
||||
<a id="ci-reuse"></a>
|
||||
### Main pushes reuse the merged pull request's run
|
||||
|
||||
|
||||
@@ -139,7 +139,9 @@ harness-generic; only the thread and the PR tool are Capy.
|
||||
default branch.
|
||||
4. **Run `/ship` unattended** with the `GSTACK_LAUNCH` assignments
|
||||
exported: it runs the pregate (on a machine under 8 cores the test tier
|
||||
needs `gstack-pregate --stage tests --remote`, about $0.06 on a Ubicloud VM
|
||||
clears from a verified CI run at the pushed tree when the base declares
|
||||
`remote_gate_runs` ([CI carry](pregate.md#ci-carry)); otherwise it needs
|
||||
`gstack-pregate --stage tests --remote`, about $0.06 on a Ubicloud VM
|
||||
or free locally for small selections), both reviews, the gate-integrity
|
||||
list, the docs audit, and writes the ship receipt into the PR body
|
||||
(`gstack-ship-receipt write --pr <n>`). With `queue_mode:
|
||||
|
||||
+45
-10
@@ -207,7 +207,14 @@ function but a same-file `url(#id)`, which closes `@import`, `image-set()`,
|
||||
entity declaration, and no DOCTYPE but a bare `<!DOCTYPE svg>` or W3C's own SVG DTD, read after character references are decoded; anything else, a
|
||||
script or `foreignObject` included, keeps it code. More than
|
||||
2,000 data paths keep every one code; a `git grep` failure is `consumer_scan_failed`
|
||||
and fails the depth closed. Only `classifyDiff` applies the rule (`lib/review-collect.ts`
|
||||
and fails the depth closed. The scan first lists the files that contain a candidate's
|
||||
extension and then searches only those, docs files left out, so a large data tree
|
||||
costs one cheap pass. Each scan gets one 120-second deadline, set in milliseconds with
|
||||
`GSTACK_CONSUMER_SCAN_TIMEOUT_MS`; a scan that runs past it, or whose matches pass
|
||||
256 MB, is also `consumer_scan_failed`, and its detail says what stopped it and names
|
||||
the deadline. One scan fits a minute inside the 180 seconds /ship gives
|
||||
`gstack-review-plan`; a plan that also scans a fix range runs two scans, and if they
|
||||
pass that limit the plan fails, which also leads to a full review. Only `classifyDiff` applies the rule (`lib/review-collect.ts`
|
||||
finds the consumers, `gstack-ship-preflight`'s `RUNTIME_LINES` reads the same
|
||||
function); every path-only caller (the receipt's gate-scan decision, the Step 16
|
||||
post-review route, /autoplan's docs-only scope) keeps data files code. A numeric claim
|
||||
@@ -424,6 +431,24 @@ Over the last 100 non-release commits on main (8663c3d2..8be7f213), money hit
|
||||
which hit 43 of 100; those paths already get full depth, so the gate class was
|
||||
narrowed to the gate scan's tagged hunks.
|
||||
|
||||
<a id="review-exclude-paths"></a>
|
||||
## What readers read under `review_exclude_paths`
|
||||
|
||||
A repository can list content folders its reviewers leave out, on its base
|
||||
branch only ([ship-policy.md](ship-policy.md#review-exclude-paths)). Under it,
|
||||
every reader (specialists, the Red Team, the native and outside voices, the
|
||||
targeted reader) reads the same selection: its `DIFF_CMD:` with one
|
||||
`:(top,exclude,glob)` item per applied pattern, the fixture command with the
|
||||
same items, and the `DIFF_KEEP:` command for the kept files under those
|
||||
patterns. The outside voice's embedded diff is that selection. Each brief says
|
||||
how many files and bytes were left out and that the reader must not report on
|
||||
their absence. Excluded paths count as docs for the depth and never trigger the
|
||||
numeric-claim scan or a targeted class; kept files keep their own class.
|
||||
|
||||
Independently of the key, no reader is handed more than 2,000,000 bytes of
|
||||
diff: `gstack-review-dispatch` stops with `REVIEW_DIFF_TOO_LARGE` before it writes
|
||||
anything, instead of a review of the part a reader happened to see.
|
||||
|
||||
## What the receipt records
|
||||
|
||||
`gstack-review-scope summary --since <REVIEW_SINCE> --run <run dir>` reads this invocation's
|
||||
@@ -576,22 +601,30 @@ 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) 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
|
||||
adversarial voice's FIXABLE collects as CRITICAL (normalizer v2, GSTA-167), except that on every class a native
|
||||
finding that states `Blocks: no` and one `Severity` field collects at that field (GST-1031 ruling A); a native FIXABLE
|
||||
finding without `Blocks: no`, with no `Severity` field, or whose field names only FIXABLE or INVESTIGATE still collects
|
||||
as CRITICAL. On a code or docs plan with only `runtime=<n>` depth reasons (collect `--manifest` naming this ledger's
|
||||
pass; GST-987) FIXABLE also raises nothing through a label heading or a leading tag. A numbered finding heading keeps
|
||||
every unlabeled line under it (located or numbered bullets, a bold subheading, a located paragraph) until its
|
||||
`Severity` or `Blocks` line comes, so those fields never split off as a second record (GST-1031). 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
|
||||
closing summary is a known phantom: it stays at that label in the judged set, lead judgment can fold it only into a
|
||||
finding at least as severe, and it counts toward what blocks. GST-766 skipped negated labels and lost
|
||||
closing summary is a known phantom: it collects at that label. When it is a summary candidate (no lead label, no
|
||||
`Severity` or `Blocks` field, and no file:line or exception clause in any sentence naming a label: "so nothing is
|
||||
CRITICAL", "the test covers the CRITICAL fix"), collect marks it `summary_candidate` and lead judgment may lower it
|
||||
with a reason, which judge records as `dismissed` (GST-1031); otherwise lead judgment can fold it only into a finding
|
||||
at least as severe, and it counts toward what blocks. GST-766 skipped negated labels and lost
|
||||
real blockers named in an exception clause ("No CRITICAL issues except the token leak in log.ts:3"); GSTA-167
|
||||
reverted that. Each prose finding's line also
|
||||
records the Severity and Blocks its own text states as `stated`, beside the collected label and never in its place;
|
||||
a classified finding that carries `stated` shows it in its deferred-review issue next to the label ("CRITICAL (the
|
||||
reviewer wrote Severity: Low, Blocks: no)"). /ship's Step 9.4 does not copy `stated` into its findings file yet: that
|
||||
instruction lives in a reviewer brief that changes only after a no-loss replay. What blocks is unchanged.
|
||||
instruction lives in a reviewer brief that changes only after a no-loss replay. A CRITICAL whose own text writes the
|
||||
field `Blocks: no` (colon required, and no `Blocks: yes`) is not counted as a blocker by the ledger count, nor by the
|
||||
review-log count or classify when the finding carries `stated` (GST-1031); lead judgment that raises such a line above
|
||||
its label counts it again.
|
||||
A phantom finding costs a fold; a lost one costs the gate.
|
||||
A reply with no finding counts as a review only with the clean marker its own brief asks for: `NO FINDINGS` alone on a
|
||||
line for the specialists, the batched specialist, the targeted reader, the Red Team and fix-check, the `Recommendation:`
|
||||
@@ -665,7 +698,8 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
working directory) and stops when that is another checkout or cannot be read: run /ship in the thread's own checkout.
|
||||
A start directory outside any checkout, such as a multi-repo workspace root, holds no tree to review by mistake and
|
||||
dispatches. Step 11's voices take the `adversarial-review` start
|
||||
token by default, and a `logStart` list without it stops before anything runs.
|
||||
token by default, and a `logStart` list without it stops before anything runs. Decision 3.5's fix-check pass turns a
|
||||
`review` start into `review-fix-check`, so its row is never a pass (GST-1031).
|
||||
|
||||
## Where this is enforced
|
||||
|
||||
@@ -674,6 +708,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)).
|
||||
|
||||
+124
-1
@@ -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`. |
|
||||
@@ -1333,6 +1334,127 @@ opens no fence, and a fence that never closes stays prose, along with the rest
|
||||
of the body. Data PRs (evals,
|
||||
measurements, sweeps) run it before READY so the body's numbers are the run's.
|
||||
|
||||
<a id="review-exclude-paths"></a>
|
||||
## `review_exclude_paths`: what reviewers leave out
|
||||
|
||||
Since the release that added it (`REVIEW_EXCLUDE_KEY_VERSION` in
|
||||
`lib/ship-policy.ts`). A results PR can carry thousands of receipt files that
|
||||
no reviewer needs to read. `review_exclude_paths` lists git glob pathspecs
|
||||
whose content files `/ship`'s reviewers leave out:
|
||||
|
||||
```json
|
||||
{ "review_exclude_paths": ["receipts/**"], "min_gstack_version": "<the release that introduced review_exclude_paths>" }
|
||||
```
|
||||
|
||||
**Read from the base only.** `gstack-review-plan` reads the policy committed on
|
||||
the base commit it already resolved (`origin/<base>`, or `<base>`), never
|
||||
the branch's copy, so a branch cannot exclude its own files. A branch that adds
|
||||
the key gets `REVIEW_EXCLUDE: none (head sets review_exclude_paths; it applies
|
||||
after it lands on <base>)` and a normal review. A base policy that does not
|
||||
validate stops the plan (`POLICY_INVALID`), and a base git cannot read stops it
|
||||
(`POLICY_SOURCE_UNAVAILABLE`); neither turns exclusion off quietly.
|
||||
|
||||
**Git's glob dialect.** Each pattern is a `:(top,glob)` pathspec: root-anchored
|
||||
and case-sensitive. The matched set comes from git itself (`git diff --raw -z`),
|
||||
so the plan, the receipt and what readers see are one set. Untracked files are never part of it: no reader's `git diff` shows them, and the receipt reads the committed head.
|
||||
|
||||
| Pattern | gitignore would match | `review_exclude_paths` matches |
|
||||
|---|---|---|
|
||||
| `receipts/**` | everything under `receipts/` at any depth | everything under the top-level `receipts/` |
|
||||
| `receipts/*` | files directly in `receipts/` | files directly in `receipts/` only (`*` never crosses `/`) |
|
||||
| `data/` | any `data/` directory at any depth | nothing: write `data/**` (and `**/data/**` for any depth) |
|
||||
|
||||
A pattern that matches no changed file is listed as `unmatched=` on the plan line.
|
||||
|
||||
**What is excluded, and what is kept.** A matched file is excluded only when it
|
||||
is content: a data file (JSON, JSONL, CSV, TSV, images), a docs-class file, a
|
||||
`.log` file, or a file git reports as binary (`-\t-` in `--numstat`; readers would only see `Binary files differ`) whose bytes hold a NUL in their first 8,000, so a branch's `.gitattributes` cannot make a text file binary content. A binary file is content even under
|
||||
`config/` or in a skill directory; only `policy-dir`, `gate-path`, `executable`, `link`, `gate-tagged`, or a source or script extension (`code`) keep it.
|
||||
Everything else stays in every reader's diff with one reason code:
|
||||
|
||||
| Reason | Kept because |
|
||||
|---|---|
|
||||
| `policy-dir` | it is under `.gstack/` or `.github/` |
|
||||
| `gate-path` | it is a gate path (lib/review-depth.ts) |
|
||||
| `executable` | its old or new mode is executable |
|
||||
| `link` | its old or new mode is a symlink or a submodule commit |
|
||||
| `agent-prose` | it is a skill, section, resolver, template or rule file |
|
||||
| `config` | it is a data-shaped config file (`package.json`, `tsconfig*.json`, `config/`, dotfiles) |
|
||||
| `gate-tagged` | the gate scan tagged one of its hunks |
|
||||
| `code` | it is code or a test, not content; a binary file with a source or script extension (`.js`, `.py`, `.sh`, `.wasm`) counts as code |
|
||||
| `secret-line` | an added line reads a credential (the targeted pass's secret rule) |
|
||||
| `named-by-code` | a code or test file that stays in review (not docs, a data file, a `.log` or binary, and not itself excluded) names its full repository path, at the head or the merge base |
|
||||
| `consumer-scan-unavailable` | the `git grep` for those names failed, so every candidate is kept; also a live binary file whose bytes could not be read |
|
||||
| `gate-scan-unavailable` | the gate scan failed or did not run at the head, so no hunk's tag is known and every candidate is kept |
|
||||
|
||||
Only a full path counts as a name. A basename such as `result.json` would keep
|
||||
every receipt in the tree, and a directory reference such as `receipts/` (the
|
||||
harness that writes the receipts names it) keeps nothing: the plan prints an
|
||||
informational `REVIEW_EXCLUDE_REFERENCE:` line naming the code that reads the
|
||||
folder. A data file never keeps another file: a results manifest such as
|
||||
`docs/receipts-manifest.json` lists every receipt by full path, and it is
|
||||
content, not a program that reads them. The name scan is one uncapped `git grep -F -f` over the candidates'
|
||||
paths, not the collector's capped consumer scan.
|
||||
|
||||
**What readers run.** Every reader's `DIFF_CMD:` gains one
|
||||
`':(top,exclude,glob)<pattern>'` item per applied pattern, and the fixture
|
||||
command the security specialist, the Red Team and the native adversarial pass
|
||||
read through gets the same items. A git exclude beats any positive pathspec, so
|
||||
kept files under an applied pattern come back through a second command,
|
||||
`DIFF_KEEP: <reader> git diff <diff-base> -- ':(top,literal)<path>' …`, which every
|
||||
brief names. That command is bounded by its argument bytes (24,000 across all
|
||||
patterns, under Windows' command-line limit): a pattern whose kept files would
|
||||
pass the bound is not applied, and the plan line says so as
|
||||
`skipped=<pattern>:kept=<n>:bytes=<b>`. Every brief also gets one line:
|
||||
`Excluded by base policy <ref>@<sha12> (selection <digest>): <n> files, <bytes> bytes
|
||||
on this branch matching <patterns>. They are not in your diff; do not report on
|
||||
their absence.`
|
||||
|
||||
**Depth, numbers and the targeted pass.** Excluded paths count as docs for the
|
||||
review depth everywhere it is computed (`gstack-review-plan`, the receipt
|
||||
writer, carry and the queue), and their lines never trigger the numeric-claim
|
||||
scan or the targeted money, gate, auth and secret classes. Defects inside a
|
||||
correctly excluded data file are coverage the owner gives up by policy.
|
||||
|
||||
**The plan line.** `REVIEW_EXCLUDE: patterns=<applied>/<declared> excluded=<files>
|
||||
bytes=<b> kept=<k> content=<kind>:<n>,… source=<ref>@<sha12> digest=<sha256>`
|
||||
(`content=` counts the excluded files by kind: `binary`, `data`, `docs`, `log`), then up to 20
|
||||
`REVIEW_EXCLUDE_KEPT: <reason> <path>` lines (`--json` lists every one). A
|
||||
repository with no key anywhere prints none of these lines, and its plan, briefs
|
||||
and diff commands are byte for byte what they were.
|
||||
|
||||
**The receipt, carry and the queue.** Each rendered brief records the digest it
|
||||
names in the review ledger, and `gstack-review-dispatch verify` prints
|
||||
`REVIEW_EXCLUDE_RECORD: digest=<d>` when every reader's newest brief names the
|
||||
same one (a mixed set makes it `VERIFY: dirty`). `gstack-ship-receipt write
|
||||
--review-exclude <d>`, with that digest, records `review_exclude` (source, patterns, counts, the gate-tagged kept
|
||||
paths and three hashes). The `digest` hashes the declared patterns and the
|
||||
excluded and kept lists; `source` is provenance only. Write, carry and the queue
|
||||
each recompute the digest at the current base and compare it, so a base merge
|
||||
or a moved base that leaves the patterns and the lists unchanged still carries
|
||||
and binds. A different digest means the reviewers read another selection:
|
||||
write refuses with `RECEIPT_INVALID field=review_exclude`, carry with
|
||||
`RECEIPT_CARRY_REFUSED review_exclude changed`, and the queue with
|
||||
`RECEIPT_INVALID field=review_exclude expected=<digest> recorded=<digest>`. The
|
||||
fix is to rerun the review, never a receipt write alone.
|
||||
|
||||
**Adopting it.** (1) Open a PR on the base that adds the patterns and
|
||||
`min_gstack_version` at least `REVIEW_EXCLUDE_KEY_VERSION`, and ship it like any
|
||||
policy change. (2) Once it merges, bring the base into the results branch (a
|
||||
crew needs its coordinator's word for that merge) and rerun `/ship`; the plan
|
||||
prints `REVIEW_EXCLUDE:`. (3) Check the kept list: a file you expected to be
|
||||
excluded names its reason.
|
||||
|
||||
<a id="review-diff-ceiling"></a>
|
||||
**The review ceiling.** Independently of the key, `gstack-review-dispatch` reads
|
||||
each reviewer's diff once before it writes a manifest or a brief (`pass`, `stage`
|
||||
and `render`). A diff over 2,000,000 bytes (about 500K tokens) stops with
|
||||
[`REVIEW_DIFF_TOO_LARGE`](troubleshooting.md#review-diff-too-large), exit 3, and a
|
||||
fix chosen by where the policy stands; a `git diff` that fails or runs past
|
||||
120 seconds stops with
|
||||
[`REVIEW_DIFF_UNREADABLE`](troubleshooting.md#review-diff-unreadable), exit 1.
|
||||
There is no override flag: the recovery is a policy PR on the base, or a split.
|
||||
|
||||
<a id="review-scope"></a>
|
||||
## `review_scope`: how the review loop converges
|
||||
|
||||
@@ -1462,6 +1584,7 @@ receipt when the base moves:
|
||||
| a rebase or force-push onto a tree the receipt covers, product diff against the new base unchanged | carries (`carry.ancestry: rewritten`) |
|
||||
| any other rebase or force-push | a fresh /ship (`RECEIPT_CARRY_TREE_MISMATCH`) |
|
||||
| a stacked PR whose parent landed (squash or merge), retargeted to the base with the base merged in, HEAD's product diff exactly the one reviewed against the parent branch | carries (reason `stack parent <branch> landed`) |
|
||||
| a stamp on the branch's own rows in a stamp file the branch also changed as product: every changed line at a branch-added position, the file's mode unchanged, the base not touching it, the rest of the product diff unchanged | carries (reason `a stamp on the branch's own rows (<file>)`) |
|
||||
|
||||
**Stacked PRs (#3570).** A child PR's receipt is written against its parent
|
||||
branch, so its `base` names that branch and its `product_patch_id` is the
|
||||
@@ -1632,7 +1755,7 @@ Stages, each journaled in `.gstack/tmp/restamp-journal.json`:
|
||||
1. **version**: `gstack-version-bump write --version <X>` in the staging worktree (VERSION, the manifest, npm lockfiles); skipped when `restamp_command` owns the version. The manifest gets the npm-valid three-part form, except when the policy lists it in `stamp_paths` or `stamp_files` and its copy at the base holds four parts (gbrain's convention): then it keeps the full version (`--manifest-form full`) and the stage detail says so.
|
||||
2. **release_tool**: the policy command, under the contract below.
|
||||
3. **changelog**: the PR's `[Unreleased]` or old-version section moves to the top under `<X>`, above its predecessor's entry. A duplicate heading the PR introduces (`CHANGELOG_DUPLICATE_HEADING`: the headings are counted against the base's CHANGELOG at `origin/<base>` and at the fork point, so a version the base already heads twice passes as long as the PR adds no further copy) and a top entry that is not `<X>` (`CHANGELOG_TOP_MISMATCH`) are errors; the section being re-headed itself may exist only once.
|
||||
4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites, plus every `stamp_patterns` template, within `stamp_paths` and `stamp_files`. A quote around X in the first two forms is part of the form and is kept as written: `since: 'X'`, `since: "X"`, `(was 'X')` and `(was "X")` move like the bare ones (the shape of a quoted source row such as gbrain's `{ since: '0.60.468.0', … }`), an opened quote must close right after X (`since: '1.2.3-beta'` and `since: '1.2.3"` are not the form, in the writer and in the tree receipt alike), and `vX` already accepts a quote before the `v`. Which X moves is decided by provenance. The branch's own tentative stamp (its VERSION when it moved past base) and `--was <X>`, when given, are each rewritten on every line of every stamp file. Beyond that, only lines the branch itself added are read, by their position in the file (`git diff --unified=0` from the fork point), so a base line with the same text, or a CRLF checkout whose diff is LF-normalized, is told apart: a stamp-form mention there of a version newer than the version at the branch's fork point is a tentative stamp and moves (this is what finds a quoted row after a lost slot and a merge of the base, since the base's release is newer than the fork point), and so does one of the fork-point version itself (a row the branch wrote at the version it forked from instead of a next version is its own stale stamp; a branch-added mention older than the fork point stays), and when the branch has no stamp of its own because its VERSION equals the base's and its fork point is the base itself (a branch rebased onto the base after it reached X, or one never stamped) a mention of the base version on a line it added is its stale stamp and moves too, while the same version on a line the base already carries is the base's release and stays. The stage detail names both counts per file (`VERSION equals base X, so X moved only on lines this branch added and stayed on the base's own lines (<file>: N moved, M kept)`), so the stamp commit shows which lines moved and why. A line the base added since the fork point is the base's even when that diff shows it (a merge of the base on the branch, committed or still staged, brings every line the base added since the fork into the diff from the fork point, main's own release keys included): it is left out, the detail says `N line(s) the base added since the fork point are the base's, not this branch's, and stay (<file>: N)`, and a base diff git cannot run refuses the stage (`POLICY_SOURCE_UNAVAILABLE`, `cannot diff <since>..<base> for the stamp_paths stage`) rather than reading as an empty set that would move the base's lines again. The rule reads line ownership, not meaning: a branch-added line inside `stamp_paths` that means the base's own release in a stamp form (`Requires vX or later` written on a branch forked at X) moves with it, which the `--dry-run` diff and the per-file counts show before anything is written; write such a reference outside a stamp form, or keep that file out of `stamp_paths`. `--was X` is the opposite tool: it rewrites X everywhere, the base's own lines included, and re-heads the CHANGELOG section `## [X]`. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate. The checks that accept a stamp commit read the same bound as the writer (`stampOnlyLine` with the branch's side in `lib/stamp-only.ts`): the tree receipt behind `stamp`, `verify` and the gate-ahead replay, the merge train's stamp and verify checks (each reading the slot's predecessor, the gated commit's first parent, as the base), and finish's route each take a moved version on a line the branch added at or above the fork-point version, and on any other line at or above the gated head's VERSION. A base line whose text duplicates a line the branch added gets the branch's bound too, because the checks match branch lines by text, not position (GST-979). The /ship receipt carry can still refuse the move (`RECEIPT_CARRY_PRODUCT_CHANGED`), and when the branch keeps its VERSION claim, carry and finish still route the move as behavior (GST-980).
|
||||
4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites, plus every `stamp_patterns` template, within `stamp_paths` and `stamp_files`. A quote around X in the first two forms is part of the form and is kept as written: `since: 'X'`, `since: "X"`, `(was 'X')` and `(was "X")` move like the bare ones (the shape of a quoted source row such as gbrain's `{ since: '0.60.468.0', … }`), an opened quote must close right after X (`since: '1.2.3-beta'` and `since: '1.2.3"` are not the form, in the writer and in the tree receipt alike), and `vX` already accepts a quote before the `v`. Which X moves is decided by provenance. The branch's own tentative stamp (its VERSION when it moved past base) and `--was <X>`, when given, are each rewritten on every line of every stamp file. Beyond that, only lines the branch itself added are read, by their position in the file (`git diff --unified=0` from the fork point), so a base line with the same text, or a CRLF checkout whose diff is LF-normalized, is told apart: a stamp-form mention there of a version newer than the version at the branch's fork point is a tentative stamp and moves (this is what finds a quoted row after a lost slot and a merge of the base, since the base's release is newer than the fork point), and so does one of the fork-point version itself (a row the branch wrote at the version it forked from instead of a next version is its own stale stamp; a branch-added mention older than the fork point stays), and when the branch has no stamp of its own because its VERSION equals the base's and its fork point is the base itself (a branch rebased onto the base after it reached X, or one never stamped) a mention of the base version on a line it added is its stale stamp and moves too, while the same version on a line the base already carries is the base's release and stays. The stage detail names both counts per file (`VERSION equals base X, so X moved only on lines this branch added and stayed on the base's own lines (<file>: N moved, M kept)`), so the stamp commit shows which lines moved and why. A line the base added since the fork point is the base's even when that diff shows it (a merge of the base on the branch, committed or still staged, brings every line the base added since the fork into the diff from the fork point, main's own release keys included): it is left out, the detail says `N line(s) the base added since the fork point are the base's, not this branch's, and stay (<file>: N)`, and a base diff git cannot run refuses the stage (`POLICY_SOURCE_UNAVAILABLE`, `cannot diff <since>..<base> for the stamp_paths stage`) rather than reading as an empty set that would move the base's lines again. The rule reads line ownership, not meaning: a branch-added line inside `stamp_paths` that means the base's own release in a stamp form (`Requires vX or later` written on a branch forked at X) moves with it, which the `--dry-run` diff and the per-file counts show before anything is written; write such a reference outside a stamp form, or keep that file out of `stamp_paths`. `--was X` is the opposite tool: it rewrites X everywhere, the base's own lines included, and re-heads the CHANGELOG section `## [X]`. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate. The checks that accept a stamp commit read the same bound as the writer (`stampOnlyLine` with the branch's side in `lib/stamp-only.ts`): the tree receipt behind `stamp`, `verify` and the gate-ahead replay, the merge train's stamp and verify checks (each reading the slot's predecessor, the gated commit's first parent, as the base), the /ship receipt carry, and finish's route each take a moved version on a line the branch added at or above the fork-point version, and on any other line at or above the gated head's VERSION. A branch line is named by its position in the gated file, so a base line whose text duplicates a line the branch added keeps the gated head's bound (GST-979). When the stamp keeps the VERSION (a slot that keeps its claim), only the branch's own lines may move, so a base line moved by hand is refused. The carry follows a stamp on the branch's own rows in a stamp file the branch also changed as product when every changed line of that file sits at a branch-added position, the file keeps its mode, the base did not touch it and the rest of the product diff is unchanged (`a stamp on the branch's own rows` in the carry reason, GST-980).
|
||||
5. **mirror**: tier 2; declared and validated, reported as not run.
|
||||
6. **apply**: every changed file is written to the branch only after all stages validated, each against the fingerprinted preimage; a file edited meanwhile is `RESTAMP_CONFLICT` and the applied writes roll back (rollback refuses to overwrite a file whose bytes differ from the recorded postimage).
|
||||
|
||||
|
||||
+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. |
|
||||
|
||||
+102
-6
@@ -146,7 +146,9 @@ record. Another successful fetch alone cannot certify corrupted history.
|
||||
|
||||
Outside reviews send your diff, plan or question to a second AI provider. Their
|
||||
verdict comes from `lib/outside-review-result.ts`, which prints
|
||||
`VERDICT: clean|findings|unverified|unavailable`. A `findings` verdict with a
|
||||
`VERDICT: clean|findings|unverified|unavailable`. When given an events file, it
|
||||
also prints `OUTSIDE_USAGE:` with the token counters Codex reported, or
|
||||
`OUTSIDE_USAGE: unknown` when it reported none. A `findings` verdict with a
|
||||
P0 or P1 finding blocks exactly like a native P0/P1. `unverified` and
|
||||
`unavailable` are missing coverage: /ship and /review continue, show the gap in
|
||||
the readiness dashboard and the PR body, and never count it as a pass.
|
||||
@@ -2434,7 +2436,10 @@ default `fetch-depth: 1`), a remote branch never fetched
|
||||
first parent is missing from a shallow history. The tool never guesses a
|
||||
base, because an empty diff against a wrong or absent base would read as "no
|
||||
gate edits" and pass review. `gstack-diff-scope` prints `SCOPE_ERROR=no_base`
|
||||
(all flags false, exit 2) for the same cause.
|
||||
(all flags false, exit 2) for the same cause, and also when no base was given
|
||||
and none resolves because `origin/HEAD` names the checked-out branch (or its
|
||||
upstream, or HEAD's own commit) and no PR base, live remote HEAD, `origin/main`
|
||||
or `origin/master` resolves (its comment line says so).
|
||||
|
||||
**Fix.** Run the command the line prints. It is built for this checkout:
|
||||
`git fetch origin +<branch>:refs/remotes/origin/<branch>` for an unfetched
|
||||
@@ -3330,6 +3335,72 @@ block. For the `--head` refusal, check out that commit and write there. For a
|
||||
targeted refusal, run the targeted reader on HEAD, `gstack-review-dispatch verify
|
||||
--targeted-out .gstack/tmp/targeted.json`, and write again with
|
||||
`--review-targeted @.gstack/tmp/targeted.json`.
|
||||
|
||||
**`field=review_exclude`.** `gstack-ship-receipt write` and the queue's receipt check also
|
||||
use this code when the receipt's review_exclude digest (or the
|
||||
`--review-exclude <digest>` write was given) is not the digest HEAD has under the
|
||||
base policy's [`review_exclude_paths`](ship-policy.md#review-exclude-paths). The line
|
||||
names `expected=` (now), `recorded=` (what the review read, `none` when it read
|
||||
without exclusion) and `source=` (the base commit). The reviewers read another
|
||||
selection than the one the head now has, so a receipt write alone cannot clear
|
||||
it: rerun `/ship`'s review on the head (`gstack-review-dispatch pass --base <base>`),
|
||||
run `gstack-review-dispatch verify --ledger <ledger>`, which prints
|
||||
`REVIEW_EXCLUDE_RECORD: digest=<d>` from the briefs the readers got, then
|
||||
`gstack-ship-receipt write --pr <n> --review-exclude <d> --body <body>`.
|
||||
|
||||
<a id="review-diff-too-large"></a>
|
||||
### `REVIEW_DIFF_TOO_LARGE`
|
||||
|
||||
**Meaning.** One reviewer's diff is over the 2,000,000-byte review ceiling
|
||||
(about 500K tokens). `gstack-review-dispatch pass`, `stage` and `render` read
|
||||
each reader's diff once before writing anything, and stop (exit 3, nothing
|
||||
written) rather than hand a reader more than it can read. The line names the
|
||||
reader, its bytes, the five largest top-level folders (`top=<dir>:<bytes>`) and
|
||||
the base policy commit.
|
||||
|
||||
**Fix.** The line prints the fix for where the base policy stands:
|
||||
|
||||
- **No key:** copy the printed JSON (`{"review_exclude_paths":[…],"min_gstack_version":"…"}`)
|
||||
into `.gstack/ship-policy.json` on the base as its own PR. A lane cannot land on
|
||||
the base itself: ask the repo owner or the coordinator. Once it merges, bring
|
||||
the base into the branch and rerun once; if it is still over, split the PR.
|
||||
- **The branch declares the key but the base does not:** only the base copy
|
||||
applies; land that hunk on the base first, as above.
|
||||
- **A pattern was not applied** (`skipped=` on the `REVIEW_EXCLUDE:` line): its kept
|
||||
files need more than 24,000 argument bytes. Narrow the pattern, move the kept
|
||||
files out of it, or split the PR.
|
||||
- **Still over after exclusion:** add the printed folders to the base policy, or
|
||||
split the PR.
|
||||
|
||||
There is no override flag. See [the review ceiling](ship-policy.md#review-diff-ceiling).
|
||||
|
||||
<a id="review-diff-unreadable"></a>
|
||||
### `REVIEW_DIFF_UNREADABLE`
|
||||
|
||||
**Meaning.** git could not read a reviewer's diff, so the pass stopped (exit 1)
|
||||
before writing a manifest or a brief. `reason=timeout` means `git diff` ran past
|
||||
`timeout_ms` (120,000 ms; fixed, no flag); `reason=git-exit` gives git's exit code.
|
||||
The line carries the diff base, git's first stderr line with remote URLs
|
||||
replaced by `<url>`, and `log=`, a file under the run directory holding git's whole
|
||||
stderr, redacted the same way. It is also printed when git cannot list the
|
||||
files `review_exclude_paths` matches.
|
||||
|
||||
**Fix.** By reason: a timeout, rerun once unchanged, then split the PR or
|
||||
exclude its largest data folders on the base; git exit 128 (a missing object or
|
||||
an unfetched base), `git fetch origin <base>` and rerun once; anything else, stop
|
||||
and ask a person with the log file.
|
||||
|
||||
<a id="review-exclude-reference"></a>
|
||||
### `REVIEW_EXCLUDE_REFERENCE`
|
||||
|
||||
**Meaning.** Informational, never a stop. A kept code file names a folder that
|
||||
`review_exclude_paths` excludes (for example the harness that writes
|
||||
`receipts/`). A folder reference does not keep files; only a full file path
|
||||
does, so readers see that code without the files it reads.
|
||||
|
||||
**Fix.** Nothing for the review itself. If the code's correctness depends on the
|
||||
excluded files, narrow the pattern on the base so they stay in the diff.
|
||||
|
||||
<a id="execution-unknown"></a>
|
||||
### `EXECUTION_UNKNOWN`
|
||||
|
||||
@@ -3848,14 +3919,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
|
||||
@@ -5488,6 +5559,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`
|
||||
|
||||
@@ -5895,8 +5989,10 @@ retargeted stacked PR the parent branch and the base.
|
||||
conflict-only base merge, a test-only change, release metadata, a registered
|
||||
output the pregate's regen check proved, a rebase onto a covered tree with
|
||||
the recorded product diff, a stacked PR's retarget once its parent fully
|
||||
landed on the base, or base context next to unchanged hunks after a clean
|
||||
scoped review ([`RECEIPT_CARRY_SCOPED_REVIEW`](#receipt-carry-scoped-review)), which the
|
||||
landed on the base, a stamp on the branch's own rows in a stamp file it also
|
||||
changed as product (every changed line at a branch-added position, the mode
|
||||
unchanged, the base not touching the file), or base context next to unchanged
|
||||
hunks after a clean scoped review ([`RECEIPT_CARRY_SCOPED_REVIEW`](#receipt-carry-scoped-review)), which the
|
||||
`carry` object in the carried receipt records with the product patch-id and
|
||||
the range-diff evidence ([unattended.md](unattended.md#the-terminal-line)).
|
||||
|
||||
|
||||
+54
-4
@@ -216,8 +216,9 @@ review-log row is the same row every Step 11 pass writes
|
||||
(the outside provider that reviewed, with the model recorded, for the
|
||||
cross-family subagent; `codex` for completed Codex CLI output, as
|
||||
`ship/sections/adversarial.md` prescribes),
|
||||
`outside_provider` (the provider the host would have used, as the generated
|
||||
prose names it), `outside_status` (`completed` for a finished outside pass;
|
||||
`outside_provider` (on Capy, `host-subagent`, with `outside_model`, both copied
|
||||
from the pass program's `OUTSIDE_RECORD:` line; elsewhere the provider the
|
||||
generated prose names), `outside_status` (`completed` for a finished outside pass;
|
||||
`unavailable` when neither the CLI nor a subagent completed, the cause on the
|
||||
`gstack-outside-voice run:` stderr line and in `<out>.meta.json` rather than
|
||||
in the row; `disabled` under
|
||||
@@ -237,7 +238,16 @@ when `input.models['outside-voice']` is empty and the lane model is
|
||||
rounds ran. Any other lane must name a model. An outside voice from the same
|
||||
provider as the lane model or the native voice stops the pass (`STOP:`) before
|
||||
either voice is created, including on a rerun, and each outside voice prints an
|
||||
`OUTSIDE_VOICE:` line saying whether it is the certified configuration.
|
||||
`OUTSIDE_VOICE:` line saying whether it is the certified configuration. An
|
||||
outside voice still running with no output after `input.stuckMs` (20 minutes by
|
||||
default) is stopped, and one that stops or fails without leaving findings, or
|
||||
finishes empty, is collected as unavailable coverage (`COLLECT_UNAVAILABLE:`)
|
||||
instead of stopping the pass, so Step 11 goes on to judge the native voice. A
|
||||
stopped or failed outside voice whose reply holds findings is collected like any
|
||||
finished one, so its findings are kept; one whose reply holds none is unavailable. That outside voice then gets its own
|
||||
row (`source: "host-subagent"`, `status: "unavailable"`, `completed` and
|
||||
`converged` false, written without `--finish` before the native row), so a
|
||||
missing voice never reads as converged or as an unavailable Codex CLI.
|
||||
|
||||
### The carry across a base merge or a test-only fix
|
||||
|
||||
@@ -337,7 +347,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 +363,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