v1.96.0.0 release: fold the changelog fragments (#4025)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-05 05:05:32 +00:00
1 parent a102f5849f
commit c8db3c5b60
64 files changed
+355 -299

No files matched your search

+352
View File
@@ -1,5 +1,357 @@
# Changelog
## [1.96.0.0] - 2026-11-05
- **A reviewer's "I found nothing CRITICAL" no longer counts as a CRITICAL finding.** /ship's review collect step used to turn a reader's own closing sentence into a blocking finding when the sentence only denied a severity ("I found nothing CRITICAL.", "I found no defect that is CRITICAL.", "Neither finding is CRITICAL."), so the review loop counted blockers that named no defect and a Captain had to grant an exception. A sentence that is wholly a denial, a count of findings or "nothing blocks", with no file, line, exception clause or field on its line, now collects nothing at the label it denies; when the same line also holds other prose, that line stays on record as an INFORMATIONAL note instead of a blocker. Every real blocker still collects: a file:line, an exception clause ("except the token leak in log.ts:3"), "nothing else" or "no other", a defect named in the same sentence, another labelled sentence, a list item or a Severity or Blocks field keeps the line at its label. (GST-435)
### Fixed
- **The secret scanner no longer reads the sha in an owner's `review_loop_grant` as a phone number, or a git sha that starts with `bc1` as a Bitcoin address. Both made free tests flaky.** About one 12-character sha in 300 is all digits, and `pii.phone.e164` flagged it, so `gstack-approvals add` refused the owner's `review_loop_grant <sha>` grant with APPROVAL_RECORD_SECRET (exit 3). A digits-only span right after `review_loop_grant` or `review_loop_exception` is now a sha. A phone number with a `+`, spaces or dashes, or the same digits anywhere else (an all-digit sha in other wording included), still reports (Free Tests 41667825418, 41766865679, 41680498938, 41843093520). A sha that starts with `bc1` was redacted as `<REDACTED-pii.wallet>` in CSO's history range. A `bc1` address now has to use the bech32 alphabet and carry a valid bech32 or bech32m checksum, which every real address has. An address with a mistyped character no longer reports (Free Tests 41715139988). Both rules have true-positive controls in `test/redact-context-exemptions.test.ts`.
- **`test/plan-reality-receipts.test.ts`**: the commit-sha case writes its receipt up to the sha's first hex letter. A bare all-digit prefix is not a receipt by design, and about one 10-character prefix in 110 is all digits (Free Tests 41669379416, 41790613651).
- **`test/helpers/pregate-fixture.ts`**: the fixture's bare origin turns off auto-maintenance, as `test/helpers/restamp-fixture.ts` already does. Git's local transport drops the free runner's maintenance-off setting, so each push into the origin started a detached `git maintenance run --auto` that could change `origin.git/objects` while the template was being copied (ENOENT, Free Tests 41760556132). A new test traces a push and checks that no maintenance starts.
Claude Code and Cursor no longer start /land-and-deploy or /modernize on their own. Type `/land-and-deploy` or `/modernize` to run them; a request in plain words like "land it" now gets a pointer to the command instead of a mistaken merge or rewrite of an old branch. Both skills already stopped for your approval before merging or pushing, so this removes surprise rather than a missing safeguard. /ship, /careful, /freeze, /guard and /unfreeze still start from plain words like "ship it". gstack's router and the CLAUDE.md routing block now tell the model to point you to these two commands instead of invoking them. Under the hood, a new `manual_only: true` template flag maps to `disable-model-invocation: true` on the Claude and Cursor hosts, and host `conditionalFields` now work in denylist mode too.
- **The release gate fails closed.** A gate run that concluded success but left no report-verdict artifact was recorded GREEN; it is now INCOMPLETE and re-dispatched once like any other incomplete gate. A release-gate run that failed before it had a commit to gate (a refused policy, unreadable notes, an unresolved branch) left the workflow job green with no check run anywhere; that job now fails. (GST-1263)
- **/ship's carry route now says to push before it carries, and the docs give the real fix-loop cap.** `gstack-ship-preflight`'s carry `NEXT:` line went straight from the pregate to `carry`, which refuses a head that isn't pushed. It now says to commit, push with `gstack-ship-release push --early` (the secrets scan runs first), wait for CI with `gstack-ci-carry wait`, run the pregate and then carry. docs/ship-policy.md and docs/unattended.md give the same order. docs/review-policy.md, docs/ship-policy.md and docs/unattended.md now say `review_scope: diff-plus-blocking` stops at Step 9.4's third fix cycle, because the two-round review-plan cap runs only under `frozen`. (GST-1346)
- **/ship's workflow now matches what its tools do, so main's backstop judge can pass.** The `ship/SKILL.md workflow` judge still failed on main at 622e8b1f (run 41884510834) because the prose and the code disagreed in five places. Step 11's Codex passes and every `CODEX_MODE` branch now follow the review plan's `DISPATCH:` lines, as /ship's driver does; they no longer say the native pass always runs. The review policy now gives one fix-loop cap. It is two rounds under `review_scope: frozen` (the review plan's stop) and Step 9.4's third fix cycle under the other scopes. Step 16 says `finish` runs stages 1–4, diffs the reviewed tree and checks each lane's receipt itself, what to pass when no reviewed tree was saved, and that a stage 5 commit of regenerated outputs is content-preserving. A Step 1 carry now says to push with the secrets scan and wait for CI before the pregate, and that it ends the run with its own result line. The ship, review and unattended docs now say `diff-plus-blocking` stops at Step 9.4's third fix cycle, as the workflow runs it, and give the same carry order. The Step 5 CI-carry push asks no consent, and Step 6.5's Greptile question says it cannot undo that push. No behavior changes. Reviewer briefs are byte-identical; the adversarial section's pin is updated under GST-1346's brief-identity check. (GST-1346)
- **/ship's workflow now gives one answer where it used to give two, so main's backstop gate can pass again.** The `ship/SKILL.md workflow` judge failed twice on main at b244e6d (run 41817566885) because its readers kept finding the same conflicts. Steps 12 and 16 now say their item and stage commands serve only a `STOP:`, apart from stage 1's builds, which go to the one `gstack-ship-release finish` call as `--build`. Stage 5 says `finish` already made the release commit and only regenerated outputs are left to commit. The CI-carry early push names its full draft-PR command, says Step 6.5's question covers only Greptile, and runs `accept` after Step 17's push. A PR that `gh` failed to create (Step 19 exit 3) now ends Step 22 with `status=refused` instead of a result line that needs a PR number. The ship skill is 70 bytes smaller. (GST-1346)
### Fixed
- **Under `merge: direct`, a red main backstop no longer freezes the queue for good.** The backstop wrote a freeze that `land --pin` and `release-cut` refused under, but the only code that cleared a freeze was a repair train's verify, and `train build --repair` is off when trains are off. A later green backstop didn't clear it either, so the freeze from run 41817566885 (#3992) blocked every land and the release cut. Now `gstack-ship-queue land --pr <n> --pin <sha> --repair` lands exactly one repair PR while frozen: it runs every normal pin check, records the repair on the freeze under the land lock, and leaves the freeze active. The main backstop clears the freeze only when its gate is green on a main at or after the freeze's main that contains that repair's merge, and it writes `cleared_by: "repair #<n>; backstop <run> green on <sha>"`. A red gate keeps the freeze and rewrites its reason; when that main contains the repair, the slot opens for the next repair. A land without `--repair` stays refused while frozen, and nothing clears a freeze without a green gate.
- **The evidence ledger records /review as helps, with its three caveats beside the status.** R2 at the after pin (gstack-evals #207, superseded by #211) measured /review with the contract Stop hook on Opus 5.5 at gstack 699a41cb: blocking recall 26.2% against 3.6% for plain Opus 5.5 with no gstack, +22.6 points (95% CI +11.0 to +33.7). The `review` row in `docs/inventory/evidence.json` now says helps, with its sources, scope and R48 KEEP. A new `caveats` field, allowed only on helps, no-effect and hurts rows, holds the result's limits, and `docs/inventory.md` and `gstack-skill-bill` print them beside the status: /review and the hook count as one unit (42 of 66 sessions ran the readers only after the hook blocked), 41 of the suite's 42 bugs came from gstack-family reviews, and stacks-r1 is a development suite, so the result makes no release claim. The earlier review results stay under `prior`.
- **The AI scorer that graded skill instructions for clarity is gone, so main's backstop no longer goes red on a model's opinion of /ship's prose.** The `ship/SKILL.md workflow` judge failed main's full run twice (runs 41817566885 and 41884510834), freezing merges. Garry's call: the evals that check real outcomes are the real eval. The 21 cases that had an LLM score a skill's instruction text for clarity, completeness and actionability are removed, along with their input builders, response cache, calibration configurations, the cookie-workflow manual-review approval and the control-bundle script. The three remaining judges in `test/skill-llm-eval.test.ts` (QA anti-refusal, cross-skill greptile consistency, voice directive) keep their thresholds and samples, and the judge file's supervision wall shrinks from 3,170 to 480 seconds to match. The paid report now rejects any manual-review claim. (GST-1371)
- **The last two AI checks that guessed at agent behavior are gone, and the Greptile consistency check is now free.** `qa/SKILL.md anti-refusal` (a model predicted from /qa's text whether an agent would refuse to open the browser) and `voice directive tone` (a model judged whether the voice directive covered its topics) are removed, with their prompt builders, schemas, calibration configurations and corpora, budgets and durations. `cross-skill greptile consistency` is replaced by `test/review-bot-history-facts.test.ts`, which compares the actual history facts in /review, /ship, /retro, `review/greptile-triage.md`, `gstack-retro-metrics` and `gstack-review-bots` (paths, row columns, record types, categories, legacy rows) against `lib/review-bots.ts` with no model call, and fails on any of ten planted disagreements. `test/skill-llm-eval.test.ts` had no cases left and is deleted; no other paid case changes. (GST-1403)
- **Agents comment on GitHub only under a bot identity.** CONTRIBUTING.md (issue triage, the wave process and porting step 5), docs/triage.md, docs/captain.md, docs/recommended-harness.md and /modernize let an agent post the close note, the thank-you, the ask for missing information, tags and review requests, under its bot identity (`capy-ai[bot]` on Capy) and never the maintainer's own account; an agent with no bot identity drafts the note for the maintainer instead. Email, chat, DMs and social posts to people, and reading the maintainer's inbox, stay off limits for agents. (GST-1417)
- **The land stamp no longer rewrites other projects' versions.** A PR that wrote `QM v0.1.15` into a README listed in `stamp_files` landed as `QM v0.0.115`, because the stamper read any `vX` newer than the branch's fork point on a line the PR added as the PR's own tentative stamp, and the tree receipt called the rewrite stamp-only. Built-in `vX`, `since: X` and `(was X)` mentions above the version being stamped now move only when the branch itself recorded that version, so a third-party version above it survives byte for byte and a rewrite of one reads as a product change. A declared `stamp_patterns` slot keeps the old bound but moves only inside the slot. (GST-1245, reported by HARN-7)
- **`land` restamps after a stamp is reset.** When a PR branch was reset to its reviewed pin after land stamped it, the rerun resumed the old journal and refused with `QUEUE_TRAIN_MOVED` until the journal was deleted by hand. Land now discards that journal, says so on a `JOURNAL:` line, and restamps the pin. (GST-1245)
- **A Codex outside review that puts its recommendation under a `## Recommendation` heading now counts as finished.** The outside-review parser accepted only an inline `Recommendation: <action> because <reason>` line. When the model wrote the recommendation as a Markdown heading with the sentence on the next line, /ship and /codex reported the finished review as unavailable (`missing_markers`). A heading followed by a sentence with a `because` reason now reads the same as the inline line. A heading with nothing under it, a sentence with no `because`, a bullet or quote line under the heading, or a heading inside a code fence still reports the recommendation as missing. Severity labels and `Verdict:` lines under the heading are read exactly as before, so a blocking review still blocks. (#3980, thanks @nfitzsimmons)
- `/sync-gbrain`'s GBrain Search Guidance block now tells agents to search past plans, retros and learnings with `--source gstack-artifacts-<user>`, the source named after the repo `gstack-artifacts-init` creates. It still said `gstack-brain-<user>`, the name from before the v1.27.0.0 rename, so every agent following the block got `unknown_source`, and a hand fix to CLAUDE.md reverted on the next `/sync-gbrain`. This repo's own CLAUDE.md copy and the write-surfaces checklist are fixed too. `test/regression-3799-sync-gbrain-artifacts-source.test.ts` pins the block to the name `gstack-artifacts-init` uses. Reported in #3799 by @extremenelson.
- **Pregate no longer crashes on large diffs.** When many touched files each reach many tests, the lanes check stopped with `lanes: threw: Maximum call stack size exceeded`, so PREGATE was incomplete and /ship was blocked before any test ran (seen on a 335-file gbrain diff). The lanes check now records every provenance row without hitting the runtime's argument limit, and the secrets check does the same for scanner findings on a very large set of added lines. (GST-1334)
- **`land --pin` no longer leaves the land lock stuck in `merging` when GitHub applies a lock push and still answers HTTP 502.** On 2026-11-04, during GitHub 502s, the first land of #3990 reported "the land lock push to origin failed and the lock there is unchanged" while origin's `refs/notes/gstack-land` already held #3990 in `merging`. Its release then failed against the old lease, and two reruns refused the same way until `--break-lock`. Now a lock push that reports failure asks origin (`git ls-remote`) what the ref holds, and treats a push origin applied as written. When a lock fetch fails, the read takes origin's sha from `ls-remote` if this clone already has that commit, so a rerun resumes its own lock instead of leasing a stale sha. When a phase change or release is rejected because origin holds a newer lock from this same run, land adopts that sha and retries once, so a refusal still releases the lock it wrote. A push that origin can't confirm either way now says so instead of claiming the lock is unchanged.
- **Step 11's review voices no longer leave an unsent brief behind (GST-1368).** When /ship ran Step 11 on its own output folder, which happens after a fix round moves the head, `gstack-review-dispatch pass` built the manifest from the whole pass-1 plan and wrote the specialist brief into the ledger, but the voices program never sends it. `gstack-review-dispatch verify` then failed the run with `BRIEF specialist-batched NOT_SENT`. Step 11 now asks for `pass --voices`, which keeps only the plan's voices, so nothing is rendered that no reader gets. `--voices` refuses `--extras`, and a reviewer pass refuses to resume a voices-only manifest. A ledger written before this fix still verifies dirty; rerun Step 11 with a fresh `--out` and `--pass` to get a clean one.
- **Trunk Dynamic CI now watches pull request CI in learning mode, and it can't skip anything.** A new `trunk-dynamic-ci` job in `free-tests.yml` asks Trunk, on every pull request from a branch in this repository, whether it would run or skip `free-suite` and `macos-named-regressions`. Trunk records the answer. No job reads the answer, so every test still runs, and every required check keeps its name and its result. The job runs one pinned action. It checks out no code, holds no GitHub token scopes, skips fork pull requests, skips its step when the token is missing, and never fails the run or a required check. The one exception is when the job itself can't start or finish, for example when GitHub can't fetch the pinned action. Then the job's own check shows red, and if a rerun doesn't clear it, the fix is to delete the job. Merge evidence (the `free-tests` aggregate, CI carry, the pregate, the train gate, the backstop and the release gate) doesn't read Trunk. Review the results one week after this merges. The details are in `docs/TESTING_INTERNALS.md`.
- **Adding `review_exclude_paths` to the base policy no longer stops every receipt written before it.** `gstack-ship-queue ready`, train build and land refused each older receipt with `field=review_exclude expected=<digest> recorded=none`, even when the PR touched none of the new patterns. On harness-evals one policy PR stopped 9 queued PRs, and the only fix was to rerun review on each unchanged head. The queue now accepts a receipt with no recorded digest when no file the PR changes matches any current pattern, since its reviewers read the same full diff. It still refuses a recorded digest that differs, a PR that touches a pattern (excluded or kept), and a base policy it can't read. Carry still compares digests, so an affected PR that merges main still needs /ship once. (GST-1408)
### Fixed
- **A design-token file is no longer a secret path by its name alone, so a copy edit to one gets the review depth its content earns.** The review planner treated any path with a `token` or `tokens` segment as a secret path and a gate path, so founderstar's `scripts/ui/token.gd` (Godot UI design tokens) pushed a copy-only PR to the full panel with 11 readers. A token-named style or engine file (`.gd`, `.css`, `.scss`, `.sass`, `.less`, `.styl`) now counts as a secret path only when its added or removed lines hold a value the credential scanner behind pregate's secrets check reads as a secret, or have no text to read. Every other token file still counts by name. That covers code such as `lib/token.ts`, where auth tokens are minted and checked, and also `token.json`, `tokens.yaml` and binary token stores. Secrets, credentials and keychain names still count by name alone, and so do dotenv files (`.env`, `.env.production`, but not `.env.example`), SSH private keys (`id_rsa`, `id_ed25519`) and key-store files (`.pem`, `.key`, `.p12`, `.pfx`, `.jks`, `.keystore`, `.ppk`), which the planner didn't flag before.
**The two red cases in the 2026-11-01 Codex census (#3784) are fixed: one was a test fixture and one was unclear wording in /pair-agent.** Run 41279336076 (main @ 482ffd1) went red on the codex-astra arm.
### Fixed
- **/pair-agent says the instruction block includes its first and last `=====` lines.** The skill said the command "prints everything between its `=====` divider lines", so Codex copied every line but those two outer dividers, and `safety-pair-agent-block` failed. The rule's pinned hash in `test/helpers/safety-rules.ts` moves with the new wording. With the new wording, the rule arm passed on gpt-6-astra and on Claude, and the removed-rule arm still failed on Claude.
- **`cso-diff-mode`'s fixture now has a page that reads the paid plan.** The forged payment webhook wrote to a map that nothing read. /cso daily reports leave out a candidate whose impact needs code outside the snapshot, so Codex correctly left out the forgery finding. The fixture now serves a premium report only to accounts marked paid, which makes the forgery an in-snapshot entitlement bypass. The assertions are unchanged.
**The rule-removed control for safety-rule evals now really removes the rule on the Codex host (GST-840).** The Codex host installs every gstack skill under `~/.codex/skills` as a link into the checkout. A removed-arm run therefore still had the rule in the installed skill, and in GSTA-211's `safety-pair-agent-block` run the model read it there and passed the control.
### Fixed
- **`test/helpers/codex-host-runner.ts`**: under `GSTACK_SAFETY_ARM=removed`, each installed skill whose markdown carries a registered safety-rule paragraph gets a private copy of every linked directory on the way to that file (the skill directory itself or a linked directory inside it), and the file is rewritten with that paragraph deleted. Files are replaced, never written in place, so the checkout is never touched. The `rule` arm is unchanged.
- **`test/codex-host-safety-arm.test.ts`** checks both arms on a linked install like the one `./setup --host codex` makes, including a rule file under a linked directory inside a real skill directory, with no model and no setup run.
**A remote pregate killed by a machine restart is rerun, not destroyed (GST-1028 b).** When a lane's machine restarted, its background pregate died but its Ubicloud VM kept running, and the line the pregate printed at create time told lanes to destroy it. Lanes did, and paid for a fresh VM, even though rerunning the same command reattaches to the live VM and finishes the run.
### Changed
- **The create-time line says to rerun first.** `pregate-remote.sh` now prints "rerun the same command to reattach; if the rerun does not reattach to it, or you give up, destroy it with: …".
- **`docs/pregate.md` explains reattach.** It lists what the run key covers (the tree, the planned files, the commit `origin/HEAD` points at, `UBI_LOCATION`, size, environment, and the runner script and setup it uploads), so a lane knows when a rerun will find the old VM. It also says to destroy the old VM when the rerun creates a new one instead.
- **The /ship driver says it too.** `ship/DRIVER.md` tells a lane to rerun the same command after a restart, and stays under its 12,288-byte cap.
### Tests
- `test/ubicloud-pregate-remote.test.ts` pins the new create-time line, and `test/ship-driver.test.ts` pins the driver's restart line.
**A full `gstack-ship-preflight` no longer merges the base under an open /ship run's reviews (GST-1204).** Running the full preflight again in the middle of /ship merged the base into the working tree (Step 3) while review readers were diffing that tree, which voided a review pass (gbrain-evals #317, a Capy lane).
### Fixed
- **`gstack-ship-preflight`:** `--run <id>` records the /ship run once Steps 1-3 pass: a marker in the checkout's git dir, and an open run on the checkout's own lease, so the Capy driver needs no workflow lease. A later full preflight on that checkout stops with exit 3, before any fetch or merge, until `gstack-ship-release finish` clears the marker or the run is 120 minutes old (GST-1205's rule) or its holder is gone. The STOP names the run and the marker and says what to do: rerun Step 5 with `--tests-only`, let the /ship finish, or remove the marker of a run that stopped. `--tests-only` and `--audit` are never stopped. /ship's own first preflight, and a rerun after a Step 3 STOP, still work. A new `--run` id from the open run's own holder (the Claude Code session, or Capy's `capy-server`) replaces a run that stopped before finish; the same id, no `--run`, or another live session's run still stops. A `--run` the lease can't record (its lock held by a live process) stops before the fetch instead of going on unprotected. A `--run` value that isn't a plain id is a usage error (2).
- **`gstack-ship-release finish`:** clears the run's marker once Steps 15-16 pass.
- **`gstack-lease open --run <id> [--pid <p> | --from <pid>]`:** exits 0 while that run is open on a live holder and 1 otherwise, using the same released, 120-minute and dead or reused-pid rules as `live`.
- **/ship Step 1** passes `--run <SESSION_ID>`, and **`ship/DRIVER.md` Phase 2** passes `--run <run dir name>`.
- **Docs:** `docs/troubleshooting.md#ship-preflight-run-open` explains the new STOP; `docs/capy-install.md` lists `gstack-lease open`.
### Tests
- `test/ship-preflight.test.ts`: a full rerun with the run open stops before the fetch and leaves HEAD, `origin/main` and the tree untouched, with or without `--run`; the driver's Phase 2 sequence refuses a mid-run full preflight while `--audit` and `--tests-only` go on; a finished or aged-out run doesn't block; after a stopped run and `gstack-skill-end`'s real release, a new id from the same holder goes on while another live holder's run is still refused; an unrecordable `--run` stops before the fetch; /ship's first preflight and its rerun after a conflict STOP work; a bad `--run` is usage.
- `test/gstack-lease.test.ts`: `open` for an open, released, aged-out, reused-pid, dead and unrecorded run, with `--pid`, `--from` or neither, plus usage errors.
**Three /ship tooling fixes from the gbrain-evals F1 rollout (GST-1204).** Version strings quoted in lane reports no longer trip the secret scan, `gstack-pr-shots` renders on a fresh Capy machine, and `gstack-models` explains why it has no outside-voice role.
### Fixed
- **Secret scan (`pii.ip_public`):** a four-part version that gstack names right before it, shaped like a gstack release (major 0 to 2), is not read as a public IPv4 address. That covers `gstack 1.94.4.0`, `upgraded gstack from 1.93.14.0`, and the install check's trailers `gstack: ok 1.94.4.0 for=ship` and `gstack: fail claude,codex (1.93.37.0)`. It also covers the far end of a range whose first end is a declared version with the same major (`gstack 1.94.4.0 to 1.94.8.0`). Before this, every push and PR-body post that quoted those lines stopped on a MEDIUM finding. Real addresses still report: `gstack 8.8.8.8`, `gstack talks to 8.8.8.8`, `gstack-proxy 1.128.1.0`, `my-gstack 1.94.4.0`, `landed in 1.94.4.0`, `gstack 1.94.4.0 - 8.8.8.8`, and a range with no declared first end.
- **`gstack-pr-shots` and every renderer that uses gstack's own browser:** when Playwright's Chromium isn't installed (the Capy install defers it to first use), the renderer now runs the checkout's `bin/gstack-browser-ensure` once and retries. Before, it stopped with `browse newtab failed: [browse] Starting server...`, which hid the cause. When the install fails, the error names the launch failure and the installer's last line instead of the daemon's banner.
- **`gstack-models resolve --role outside-voice`:** it still exits as a usage error, but now says the outside voice follows the harness, not the model policy, and points to docs/model-policy.md.
### Tests
- `test/redact-context-exemptions.test.ts`: six gstack-named version shapes stay clean; twelve address controls still report MEDIUM.
- `test/aside-render.test.ts`: a missing Chromium runs gstack-browser-ensure once and the render proceeds; the install time doesn't count against the render deadline; a failed install names the launch error; the existing newtab cases are unchanged.
- `test/model-policy-cli.test.ts`: the outside-voice hint.
- **Three /ship papercuts from gstack-evals#191 are fixed (GST-1219).** `gstack-review-dispatch judge` no longer crashes when every reader came back clean: an empty `merged.jsonl` with no `judged.jsonl` now writes an empty judged set and prints `JUDGMENT: ok nothing to judge`, while a merge with findings and no judged file is refused with the finding count. `gstack-review-dispatch pass --pass` reads its id as lowercase, so `--pass P1` runs pass `p1`; an id with any character other than letters, digits and dashes gets its own error naming that rule instead of the missing-flag message. `gstack-docs-candidate snapshot --docs` takes a comma-separated list as well as repeated flags; a value that names an existing path is kept whole, and a space-separated list or an empty value is refused with a pointer to commas or repeated flags.
**A Ubicloud run survives a dead controller (GST-1220).** When the machine that ran `ubi-runner.sh up` died, its VM kept running but its ssh key and state lived only on that machine, so no other machine could pull the results or destroy the VM before its TTL. Paid runs were lost with the controller.
### Added
- **Encrypted recovery bundles and `recover`.** With `UBI_RECOVERY_DIR` set, `up` writes each VM's key, state and tags to `<dir>/<owner>/<vm>.bundle` (mode 600). The bundle is AES-256 through `openssl enc` under keys stretched with PBKDF2 from `UBI_RECOVERY_KEY`, or `UBICLOUD_API_KEY` when that is unset. An HMAC-SHA256 is checked before anything is decrypted, and a bundle renamed or moved to another VM or owner is refused. `recover [--owner OLD]` on another machine rebuilds the state, so `pull`, `ssh` and `down` work there, and prints `RECOVERED`, `RECOVER_GONE` or `RECOVER_FAILED` for each VM. A confirmed `down` deletes the bundle. Run `recover` only after the creating machine is dead, because it can't tell a paused controller from a dead one.
- **`--counted` runs require a bundle.** `up`, `run` and `verify` take `--counted` (or `UBI_COUNTED=1`) and refuse before any create with `UBI_RECOVERY_REQUIRED` or `UBI_RECOVERY_UNAVAILABLE` when no bundle can be written.
- **`--tag key=value`** is kept in the VM's state and bundle, and `recover` prints it, so a shard map can be rebuilt from the bundles.
- **A results sink on the VM, off by default.** `up --sink https-put:<presigned URL> --sink-path PATH` starts a pusher on the VM that uploads `heartbeat.json` and the PATH entries as `results.tgz` every `UBI_SINK_INTERVAL` seconds (300 by default), so results still arrive when every controller is gone. No account credential goes to the VM. The URL is written to a 600 curl config file over ssh, never into a command line or a log, and the run record never keeps it (pass it as `UBI_SINK`). Snapshots skip `~/.ubirun-*`, and one over `UBI_SINK_MAX_MB` is never uploaded, so the last good one stays. `down` stops the pusher and makes one last push, and a failed one prints `SINK_FLUSH_FAILED` and turns a passing `run` or `verify` of that VM into exit 1. A counted rerun refuses to reattach to a VM whose bundle is missing or doesn't open. No bucket exists yet, so nothing uses the sink until a run names one.
### Changed
- **`--pass NAME` forwards only what you exported.** The value is the one your environment held when the runner started, and the runner refuses `UBICLOUD_*`, `UBI_RECOVERY_KEY`, `UBI_SINK` and every variable it sets itself, so a variable inside the runner can't stand in for yours or carry its recovery secret or sink URL to a VM.
### Tests
- `test/ubicloud-runner-recovery.test.ts` (new, offline on the fake VM) covers a controller dying after `up` while the sink keeps pushing, recovery on a second controller followed by `pull` and `down`, wrong-secret, tampered, renamed and moved bundles, an API outage and a gone VM, the counted refusals, the sink-spec rules, a failed final push, an oversized snapshot, and a reaped VM's failed push not failing an unrelated run. It checks every output stream and the ssh log for key bytes and the presigned URL.
**`gstack-post pr-body` keeps a PR's opening paragraph and Capy badge, and a GitHub Actions run list no longer reads as a phone number (GST-1227).** Reported from truther#1174 on /ship 1.94.15.
### Fixed
- **A /ship post keeps the text above the PR's first heading.** Since GST-412, a body file with a receipt above its first `## ` section dropped everything above the PR's first heading, so a /ship run on a Capy-opened PR deleted the opening paragraph and the `<!-- capy-badge:start -->…<!-- capy-badge:end -->` badge. That text is now kept byte for byte and the new receipt goes right under it, so the next /ship post doesn't read it as a stale note under the receipt. Opening lines the file brings of its own still replace it, and a stale note between the receipt and the first heading is still dropped and named on stderr.
- **A Capy badge at the end of the body stays there.** A `<!-- capy-badge:start -->` … `<!-- capy-badge:end -->` block that ends the body after its first receipt or heading is now a piece of its own, so rewriting the section above it no longer deletes it, new sections go above it, and a badge only the file brings goes last, once, so reposting the same file gives the same bytes. Every other marker block, a badge with text after it and an unclosed marker stay inside their section, as before.
- **Every run id in a `runs=<id>/<attempt>,<id>/<attempt>` list is exempt from `pii.phone.e164`.** Only the id right after `runs=` was exempt (and only at 11 digits), so the CI carry and run clear receipts `lib/run-evidence.ts` prints needed `--confirm` from their second run on. The scanner now exempts each 11- or 12-digit id of that list (`run=` too) and an id after an `actions/runs/` path with no leading `/` when it starts the line or follows whitespace, `(`, `[`, `<` or a quote. Each id needs its `/<attempt>`, so `runs=4155550123`, `runs=4155550123/1`, `sales-actions/runs/4155550123` and `+1 415 555 0123` on the same line still report. See docs/troubleshooting.md#redact-run-id-as-phone.
### Tests
- `test/gstack-post.test.ts`: "a full post keeps the opening text and a badge block above the first heading or at the end of the body byte for byte" and "run ids in a runs= list or an actions/runs/ path are not phone numbers; a phone number beside them still is" both fail on main. Four GST-412 tests that expected the opening text to be dropped now expect it kept.
- `test/gstack-post.test.ts`: "a marker block that is not a trailing Capy badge stays inside its section", "a trailing badge the file brings lands at the end once and reposts byte for byte", "a fenced heading inside a marker block and an unclosed badge marker change nothing about the sections" and "two marker blocks with the same name merge by their sections" pin the main-branch handling of every other marker block.
- `test/redact-context-exemptions.test.ts`: run ids in a `runs=` list or an `actions/runs/` path stay clean (5 cases, all failing on main), and 10 controls, `call +1 415 555 0123` and `sales-actions/runs/4155550123` among them, still report.
**Design exits ease out, never ease in (GST-1240).** `/design-consultation` wrote `exit(ease-in)` into every DESIGN.md it made, and `/design-review` checked against the same rule, so an exit that lags was both generated and never flagged. Ease-in starts slow at the moment the user is watching, so a dismissal, menu close or toast exit reads as lag. Rewrite of #3900 by @nfitzsimmons.
### Fixed
- **`/design-review` motion checklist:** ease-out to enter and exit, ease-in-out to move, and flag plain ease-in. The new line is one byte longer than the old one, so /design-review stays inside its size cap.
- **`/design-consultation` DESIGN.md template:** `exit(ease-in)` becomes `exit(ease-out)`.
- **gstack's own `DESIGN.md`:** the motion easing matches, `exit(ease-out)`.
**`gstack-autoplan export --public` no longer carries the owner's own words (GST-1243).** GEVA's public bundle (GEVA-225, gstack 1.93.34.0) kept all 38 `owner_reply` fields in decisions.jsonl, and a sealed reviewer file (`ceo-native.a5.bound.md`) quoted an owner reply word for word. GEVA redacted both by hand. Garry's rule: nothing a person wrote leaves without their click.
### Fixed
- **Whose words.** Every owner reply the private run ever recorded counts: decision rows, `run.json`'s gate and the journal (`attempts.jsonl` keeps a reply a later answer at the same gate replaced), the words a carried gate reused, and a reason a human gave for `close --deferred --by human`. Replies that parsed as gate grammar are the decision code. An agent's own words (`answer_words`) and an agent's deferral reason stay.
- **Dropped fields.** The public copy drops `owner_reply` everywhere and a carry record's `words`, and replaces a human deferral's `reason` with `[owner words redacted]`. The decision itself stays: `chosen`, the gate-grammar `reply`, `owner_mode` and `owner_reply_parsed`.
- **gstack's renderings.** A human's deferral reason is replaced where gstack writes it into fixed text (voice `detail`, consensus head, gate title, a blocker's `why`, the run log), and carried words in the `CARRIED:` line. Each rendering is matched with its delimiters on both sides, so a one-word reason never rewrites a longer one that starts with it.
- **Redacted quotes.** Every exported file, sealed `*.bound.md` reviewer results included, is scanned. Before matching, the text is normalized: JSON string escapes and HTML entities decoded (an accented letter such as `&eacute;` reads as its letter), format characters dropped by Unicode category (`\p{Cf}`, variation selectors, the combining grapheme joiner), HTML tags read as spaces (in the rendered view the whole tag, attributes included) and hyphen-newline wraps joined. It is read both as rendered (comments, footnote markers, reference definitions and link targets and titles dropped, link text kept) and as written, so markup can neither split a quote nor hide one. Replies are indexed in both views too, so a link target an owner gave is found. Quotes are redacted on the original bytes and again after recorded roots are rewritten, so a path in a reply neither hides a quote nor makes the export refuse. Contractions split ("can't" and "cannot" read "can not", "it's" "it is", "I'd" "I would", and -n't, -'re, -'ve, -'ll, -'d). A word is a run of letters and digits, and any other character separates words. A reply of 3 or more words is a quote wherever it appears whole; from 8 words, any 8 consecutive words of it are a quote too. Replies of 1 or 2 words ("yes", "do it") are not scanned for. A quote extends over neighboring words of the same reply, and the marker replaces the original characters from its first word to its last.
- **Echoes stay.** Words the owner repeated from text recorded before his reply that he didn't write (the plan at an earlier gate, that gate's items, reviewer results bound before it) stay; only the new words around them are redacted. Every run of 3 or more words a reply adds between echoes is matched on its own (and by any 8 of its words from 8), so those words are redacted even where they are quoted without the echo.
- **Refusal.** A quote or rendering inside plan.md code (fenced, or inline with any number of backticks), which export never rewrites, stops the export with `ARTIFACT_OWNER_WORDS`, file:line and exit 3, and nothing is written. `gstack-artifact validate --public <dir> --private <run.json>` runs the same check. The error never prints the words. Each changed file gets an `OWNER_WORDS: <file> dropped=<n> redacted=<m>` line.
### Tests
- `test/autoplan-public-owner-words.test.ts`: a GEVA-shaped bundle (3 `owner_reply` rows, a carried gate, one reviewer quoting a reply on one line and one reflowing a reply across blockquote lines, a control file with no owner words) exports with every owner field dropped and both quotes redacted, while the control and the round-1 reviewer files stay byte-identical. Also: a quote in plan.md code refuses; validate catches a quote or rendering added later; a human's deferral reason leaves gate.json's `why` and every other rendering, an agent's stays; a one-word human reason leaves an agent's longer one alone; a reply replaced at the same gate is still redacted; a rendering never rewrites plan.md code and an exact one there refuses; echoed plan and finding text stays; a 4-word reply is redacted; a short quote split by a tag or a reference link is redacted and validates; the new words a reply added to an echoed plan line are redacted when quoted alone, and plan.md stays byte-identical.
- `test/autoplan-owner-words.test.ts`: the matching rule (1-2 word and grammar replies, whole phrases, the 8-word window, eleven kinds of glue, fifteen kinds of markup and invisible characters (tags, comments, reference links, footnotes, link titles, literal JSON escapes, zero-width and soft-hyphen entities, direction marks and isolates, the combining grapheme joiner, a variation selector), redaction offsets over markup and escapes, contractions, new words added to an echo, extension, echoes, kept code spans, bounded renderings) and a ratchet on every template that renders a deferral reason.
### Fixed
- **A required edit after review binds no longer forces a third review round (GST-1276, GST-1311).** Four instances in two days each cost a Captain grant (#3970, #3985, gstack-evals#203, gbrain#8523 and #8525). Now `gstack-ship-release finish` proves a post-review edit's kind from the diff and sends it to the one scoped reader that test-code edits already take, which isn't a review round (Garry, 2026-11-04). The kinds are: a docs-audit fix to `docs/*.md`, `AGENTS.md` or `CLAUDE.md` whose bytes are the ones the post-audit snapshot recorded; a `.gstack/pregate.json` change that only adds lanes (pregate's own `headPolicyDiff`, with every other top-level key unchanged); and a file matching the new `bookkeeping` ship-policy key. Each needs `--reviewed-base`, at most 20 changed lines, readable text and no gate-scan tag. `ROUTE:` prints `bounded=<n>`. The packet carries the targeted brief for every class, and the reader's clean record must carry the packet's `packet_sha256`. A candidate that fails a proof stays behavior and prints `ROUTE_BOUNDED_REFUSED: <path> <reason> (fix: …)`. Gate-path prose (`review/specialists/targeted.md`, skill templates) never qualifies.
- **A test literal changed after review takes the scoped reader, not behavior (GST-1276, #3984).** This covers a removed line swapped one for one with a line that differs only inside quoted strings and has no assertion or test call, such as a fixture value the secrets scan flagged. A changed expected value or a deleted assertion still stops.
- **`gstack-diff-scope` reads a `.gstack/`-only diff as config** instead of `SCOPE_ERROR=unmatched`, and `.gstack/always-run.json` is now a gate path, so its review depth stays full.
### Added
- **`bookkeeping` ship-policy key**: root-anchored globs of `.tsv`, `.csv` or `.txt` tables, never under `.gstack/` or `.github/`, read from the base only. It needs `min_gstack_version`. See docs/ship-policy.md#bookkeeping.
### Changed
- Finish's scoped-review STOP now says "one scoped reader (not a review round) reads", and its record template includes `packet_sha256`. See docs/review-policy.md#bounded-post-review-edits and docs/troubleshooting.md#route-bounded-refused.
- **/ship now enforces the reviewer fetch rule: a review stage that downloads another repository is thrown out and rerun once.** Before each review stage starts, /ship lists the git repositories and files of 100 MB or more under the workspace, home and the temp dir, and lists them again when the stage's last reader is done. A new repository with a network remote, or a new big file, quarantines every reader of that stage. Their replies move under `<out>/quarantine/`, their findings never reach the Red Team, the judge or PR text, and `gstack-review-dispatch verify` refuses with `FETCH_VIOLATION`. Each reader reruns once as `<reader>#2` (not a review round), and a second fetch stops the pass with `FETCH_RERUN_SPENT`; running it again starts no later stage. The rerun starts only after every path the first attempt found is gone. While one is still there, no reader of any stage starts, Step 11's voices included, and the pass stops with `FETCH_RERUN_BLOCKED`, so no reader can read what the first attempt downloaded. Nothing is deleted for you, and no delete command is printed. Each path it finds is listed for you to inspect and leave alone, and a remote URL is shown without its `user:token@` part or query (or as `<redacted>` when it doesn't parse as a URL). Capy doesn't tell a pass which reader did what, so blame lands on the whole stage, not one reader. The sweep only sees repositories that keep a network remote and files of 100 MB or more; an extracted archive, a smaller download, or a clone nested in another repository or deleted before the stage ends is not caught yet. (GST-1322)
- **/ship's reviewers no longer clone other repositories on their own, and a repository can declare what they must never reach.** On gbrain-evals #362 a reader cloned a full public repository (4.2 GB) that holds that project's sealed held-out eval conversations. Every reader brief now ends with a fetch rule that says to review only this checkout and its local git history, and never to clone, fetch or download another repository, dataset or archive. A repository can add a `readers` key to `.gstack/ship-policy.json` on its base branch: `forbid_fetch` hosts or repositories (web page reads included), `forbid_paths` globs that are cut out of every reader's diff, and a one-line `brief_note` every brief carries. A pull request can add `forbid_fetch` entries but never remove one, and `forbid_paths` and the note always come from the base branch. `gstack-review-dispatch audit-fetch` scans a reader's own tool calls against the rule and records the result in the pass manifest as a note; enforcement follows in a second PR. (GST-1322)
### Fixed
- **Lead judgment can lower a native adversarial finding to the Severity its reader stated beside `Blocks: no` (GST-1406).** When the native reader wrote its `Severity` and `Blocks: no` after prose (`… so the docs are wrong. Severity: Medium. Blocks: no.`), collect kept the heading's FIXABLE as CRITICAL. Judge refused any lowering, so the native record never resolved and /ship's Step 11.5 couldn't bind. Collect still can't tell those fields from another subject's written under the same heading, so it keeps the label. Now `gstack-review-dispatch judge` lets lead judgment lower such a finding to its stated Severity, never below, with a reason. It's recorded as `dismissed`. The ledger's blocker count already left such a finding out (GST-1031) and still does, and `verify` now also checks each merged row's reader against the output it came from, so a row relabelled as native fails replay. Other readers, a finding that states `Blocks: yes`, no `Blocks` or a Severity that isn't a severity (such as `Severity: FIXABLE`), and a lowering below the stated Severity or with no reason are still refused as downgrades. See docs/review-policy.md.
### Fixed
- **docs/review-policy.md now says `verify` checks each merged row's reader.** Since GST-1406, `gstack-review-dispatch verify` checks every merged row's reader, label and raw text against the output collect parsed. The policy text named only the label and raw text. This is the documentation debt #4017 shipped with.
- **Test impact: tracer and reuse keys (GST-167, part 2).** New `scripts/test-impact.ts` traces a free test file under strace and computes a key over everything it read, its import graph, `bun.lock`, tsconfig, pregate dependencies, the runner and key code, and the runtime. `explain` shows each input and why a file can or can't be reused. Network, real HOME, shared /tmp and `.git` reads, missing or week-old traces, always-run tests, import chains past 12 hops, and any change to the key code make a file ineligible. Nothing in CI changes yet. See docs/test-impact.md.
- **Pregate keeps main's always-run entries (GST-233).** The `lanes` selection now unions `.gstack/always-run.json` from the merge base with the branch's copy, so a branch can't drop a whole-tree test by deleting its glob.
**`gstack-ubi sync` and `pack` work on a subdirectory of a git checkout (GST-935).** Packing any folder that sat inside a checkout but was not its root died with "does not appear to be a git repository", after the VM was already up, because pack fetched the checkout's refs from the subdirectory and git only fetches from a repository's root. The gstack-evals launcher syncs `quick/laneb/runner`, so every launch from a git checkout failed at its first sync.
### Fixed
- **A subdirectory packs its own files.** Pack ships the files git lists under that folder (tracked, edited and untracked, not ignored) with no `.git`. A whole-repository `.git` would show the VM every file outside the folder as deleted. Packing a checkout's root and packing a non-git folder work as before.
- **A subdirectory git lists no files under is refused.** When the checkout ignores the folder, pack exits with "pack: git lists no files under <dir>" instead of uploading an empty folder, which a run could otherwise pass on.
### Tests
- `test/ubicloud-runner.test.ts`: packing a subdirectory of a checkout and of a linked worktree ships its tracked, edited and untracked files without a `.git` and leaves out ignored ones, and an ignored subdirectory is refused. This test fails on 1.92.58.0.
**/ship now gives full review to edits of the code that hides files from reviewers or picks the diff base, and real review to harness pin edits (GST-972 follow-up).** Both were slipping through with less review than they need.
### Fixed
- **Gate paths:** `lib/review-exclude.ts` decides which files reviewers never see, and `lib/default-base.ts` picks the base every diff is cut from. An edit to either was planned as plain code at light depth, with no gate class. Both are now gate paths, so an edit gets full depth, with the Red Team as its own reader plus Step 11's adversarial and outside voices. `review_exclude_paths` also can't hide them from reviewers anymore.
- **Harness pins:** files under `scripts/harness-pins/` repeat their harness file's path, so a pin like `scripts/harness-pins/test/helpers/e2e-gate.ts.json` matched the `test/` rule and planned as tests. Since the one-file-per-pin split (#3903), a harness helper edit with its pin got no reviewers at all, where the old shared `scripts/harness-version.json` made it code class. Pins outside `test/` were worse. No code names a pin file, so the unused-data rule planned them as docs. Pins now always classify as code, so the batched reader with the testing and security checklists and Step 11's adversarial pass run on them again. `review_exclude_paths` can't hide pins from reviewers anymore either; they're kept like other config files.
### Tests
- `test/review-depth.test.ts`: both modules plan gate class at full depth; harness pins classify as code and never as data, while review pin fixtures stay tests; a helper edit with its pin plans the code-class reviewers. A new no-loss check (replay GST-972) runs the GST-967 history grid on the changed paths and their stand-ins, which reproduce main's plans byte for byte: the branch only raises class and depth and drops no reviewer. `lib/review-depth.ts` is repinned under that replay.
**/ship loses less review credit to merges and docs commits, and an owner can accept an open finding after the second full pass (GST-1195, GST-1130).**
### Fixed
- **A granted fix-check keeps its credit across a base merge that edits the same file.** `gstack-ship-release finish` now credits a fix-checked path when its change against the base is the one the fix-check read, even if the base moved under it. Before, it compared file contents, so a merge that also touched the file printed `FIX_CHECK: reviewed none` and sent the path back through review (#3916). The docs audit carries such a path too. An edit made after the merge still stops.
- **A recorded grant gets its fix-check when round 2 was itself a fix-check** (GEVA-223). `gstack-review-plan --grant <id> --fix-checks 1` now plans the one granted fix-check after a round-2 fix-check that found or fixed a blocker, instead of `NEXT: stop`. A second granted fix-check is still refused.
- **A targeted reader's record carries across a plain docs commit made after the read** (TRUT-26, truther#1168). `gstack-ship-receipt write --review-targeted` refused the record on any product change, so the Step 14 and 14.5 docs commits cost another reader run. A docs-only change now carries the record, unless it adds a numeric claim, touches a results, prereg or eval path, or mentions money, security, secrets or auth. Code changes still need the reader again.
### Added
- **`review_loop_exception.second_pass`** (TRUT-7, truther#1169). An owner who accepts an open finding after the second full pass records `{ accepted_by, date, via, reason, second_pass: { head, deferred: [<finding id>] } }`. It needs `--review-loop` with at least two passes and one deferred finding, plus the printed `Review-loop exception: …` line in the body. A stop with no fix-check and fewer than two full passes is still refused.
**`/setup-gbrain` no longer stops on a working brain because gbrain's skill checks failed on another agent's skills folder (#3790).** gbrain doctor also grades whatever skills folder it finds on its own, such as `~/.openclaw/workspace`. When one of those skill checks failed, Step 5 read the whole report as `unhealthy` and stopped, and the agent then offered to edit the other agent's workspace to get past it. `gstack-gbrain-detect` reported `gbrain_doctor_ok: false` for the same reason.
### Fixed
- **Step 5 and `gbrain_doctor_ok` judge the brain from brain checks only** (`bin/gstack-gbrain-detect`, `setup-gbrain`). A new `gstack-gbrain-detect --doctor` runs `gbrain doctor --json --scope=brain`, ignores failed checks tagged `category: "skill"` from gbrain versions that still run them, and prints `brain_ok`, `brain_failures` and `skill_failures`. Step 5 proceeds on `brain_ok`, notes any skill failures, and tells the agent never to edit another agent's workspace to make setup pass. A failed connection, schema_version or pgvector check still stops setup.
- **A fully healthy brain passes Step 5.** gbrain reports `healthy`, `warnings` or `unhealthy`; Step 5 used to accept only `ok` or `warnings`, so a brain with no warnings at all stopped too.
- **The doctor gate fails closed with a cause.** No JSON, a crash, a timeout or a gbrain that can't start reports `brain_ok: false` and why, instead of crashing detect. A JSON-null `~/.gbrain/config.json` no longer breaks Step 1's detection. Doctor now runs through gbrain's spawn helper, so the Windows `gbrain.cmd` shim works, and a large report reaches Step 5 whole.
### Tests
- `test/setup-gbrain-doctor-gate.test.ts` runs the real Step 5 block and detect against a stub gbrain: skill-only failures with and without `--scope=brain`, brain failures, a fully healthy brain, no JSON, empty output, a large report through a pipe and a JSON-null config. 10 of its first 14 cases failed on main 6bffa20.
- `test/gbrain-spawn-windows-shell.test.ts` checks that detect spawns doctor through `spawnGbrain`.
**Crews now write a postmortem when a merged PR has to be reverted, and every READY and lane report says what the lane didn't check itself.** A revert records what was wrong with the code. It doesn't record why the lane and every reviewer passed it, so the same mistake comes back in another lane.
### Added
- **Postmortem template.** `captain/templates/postmortem.md` covers what broke, how it was found, impact, a timeline, and a root cause section that asks "why did the lane and every reviewer believe it was right?". That section is answered from the review outputs, receipts, decision trail and approvals, with one row per believer and a source for each. It ends with what caught the bug, what should have, and a table of rule changes with an owner, a status and an effectiveness check.
- **Postmortem step.** `/captain` and `docs/captain.md` tell the controller to start one after any revert or S1 fix of a merged crew PR, keep it at `reports/postmortem-<n>.md` and link it from the fixing PR. The rule change it names ships as a gstack PR.
- **Unverified line.** `captain/templates/READY.md` and `captain/templates/report.md` carry a required `Unverified:` line: every claim the lane didn't check itself (a local test that timed out, a skipped paid run, a number a model reported, a review finding set aside on an assumption nobody tested), or `none`. The READY contract in `/captain`, `docs/captain.md` and the Capy lane guide names it. A missing line is asked for in a follow-up message. It never holds a PR out of a train, and it doesn't make earlier READYs stale. No tool parses or enforces it.
### For contributors
- **A dispatched screen now builds its CI image from the commit it tests.** evals.yml's `build-image` job used to check out the dispatch ref (main) and key the image on main's `bun.lock`, while the slices checked out the PR's merge commit. A PR that changed the lockfile was then tested against main's installed browsers. Screen run 41662815506 for #3947 failed eight cases with `Executable doesn't exist … chromium_headless_shell-1248` because the image had only revision 1234. `build-image` now checks out `screen_merge_sha` (or `github.sha` for every other run) before it hashes and builds, without persisting the token. It also waits for the `route` job: a refused dispatch (a fork or closed PR, for one) and a superseded screen build no image.
bump: micro
---
### Fixed
- **`ship/DRIVER.md` fits its 12 KB limit again.** The restart line from #3886 pushed it to 12,329 bytes, past the 12,288-byte cap that `test/ship-driver.test.ts` enforces, so the free tests failed on main. The fix trims wording in five places and keeps every instruction. The file is now 12,282 bytes.
bump: micro
---
### Fixed
- **`/runaway-stopper` no longer reports a clean sweep when it was run from outside the crew.** The skill tells big crews to run its scan from a fresh automation thread. That thread belongs to no crew, so its crew list came back empty and the scan checked only itself, then printed a clean `SWEEP:`. The program now takes the Captain's code (`captain`) and writes the crew list to `crew.json`. `gstack-runaway scan --dir` treats an empty list as `RUNAWAY_INPUT` and exits 1.
bump: minor
---
**New skill: /runaway-stopper finds runaway agent threads and subagents, stops them at a clean point, and sets the defaults that keep them from coming back (GST-1279).** One Founderstar thread (FOUN-4) used 6.9B tokens in 13 days, about 4B of it its own context re-read on every step, many of them wakes that ended with nothing to say. One of its /ship subagents reached 485M because it was reused for five /ship runs, so each run re-read every earlier one.
### Added
- **`/runaway-stopper`.** Detect, stop, prevent. A Captain (or the org sweep) fetches its crew's thread data to files in one program and scans it. Own tokens past 500M or a thread active past 3 days means rotate, a subagent past 100M means stop, and empty wakes past 25% or review rounds past 2 per PR are warnings (org rule R70). Captains are reported, never rotated. Threads mid-/ship or mid-merge are deferred. Rotation uses a handoff file and a successor brief (`runaway-stopper/templates/`), and only the thread's own Captain archives it.
- **`gstack-runaway scan`.** The read-only detector behind the skill. It reads `tools.thread.get`, `tools.subagent.list` and transcript data and prints `RUNAWAY:`, `FLAG:`, `DEFER:` and `SWEEP:` lines. It never acts. A thread file it can't read prints `RUNAWAY_INPUT:`, is never counted as OK, and makes the scan exit 1.
### Changed
- **A /ship subagent can meter its own cost exactly, and then gets a 100M cap by default.** A subagent whose brief names its title (`GSTACK_SHIP_TASK_TITLE`) reads its own row of the lane's task list and starts `gstack-ship-budget start --reading-from task`. In observe mode with no `--token-cap`, that run caps at 100M and prints `BUDGET_DEFAULT_CAP: 100M` (single-run /ship subagents measured 64M median, 92M p90 on Nov 3). `--token-cap none` turns it off. Tree-metered subagents, root runs and admit mode are unchanged. The Capy lane guide now says to run each /ship in a fresh subagent titled `ship #<n> at <head7>`.
### Tests
- `test/runaway.test.ts`: FOUN-4's real reading (rotate on own tokens and age, the 485M /ship subagent flagged), live-subagent stop deferred mid-/ship, active-by-recent-turn, a Captain reported but not rotated, the empty-wake rate, the subagent tree, the directory sweep and usage errors, unreadable files, the LOOK verdict, and the skill's call bound and rotation lines. `test/ship-budget-meter-scope.test.ts`: the default cap, its stop, and the cases that must not get it.
- **A declared test runner only gets the files it can run.** With `runner: "bun test {files}"` in `.gstack/pregate.json`, pregate used to hand every selected test to bun, `*_test.py` files included. If only Python files were selected, bun exited 1 and the lane failed (PREGATE_LANE_FAILED). If a TS test was selected too, bun skipped the Python files without saying so, and they were counted as run. Now the local lane gives a declared runner only the files its tool runs, judged by extension: JS/TS for `bun test`, `node --test`, vitest, jest and similar, and `.py` for pytest. A custom dispatcher such as `node scripts/run-tests.js {files}` still gets every file. The lanes detail names each file it left out as not run and not counted as covered. When no runner is set, tests are still split by language as before. (GST-1331)
- **A /ship receipt now names the gstack release that wrote it.** `gstack_version` in the receipt used to record the version gstack reads policies as, which jumps to the newest policy key the checkout knows. On a 1.95.5.0 checkout that carries the unreleased 1.96.0.0 keys, every receipt said 1.96.0.0, a release that never ran. The receipt now records the checkout's VERSION file. Policy reading is unchanged, so a checkout that carries a new key still reads policies written for it. (GST-1366)
**`gstack-doctor` finds the launch probe again on macOS's built-in bash (#3601).** macOS ships bash 3.2 as `/bin/bash`. Bash 3.2 ends a `$( )` at the `)` of a `case` pattern that has no leading `(`. So `bin/gstack-launch-probe.sh` failed to parse there, and the doctor reported the probe as missing. Thanks to @bokeeeey, who reported it in #3601 and proposed the fix in #3602.
### Fixed
- **`bin/gstack-launch-probe.sh`** writes the pattern with its POSIX leading `(`, which every bash version accepts.
- **A new test scans every tracked shell script** for a `case` pattern without a leading `(` inside `$( )` or `<( )`, so Linux CI catches this before it reaches a Mac. The probe test also parses the file with `/bin/bash`, which catches it when the test runs on a Mac.
### Fixed
- **A docs or data PR no longer gets a money reader because a file name says "cost", and summing or printing a `cost_usd` field no longer counts as money code (GST-961, GST-962).** On gstack-evals#138, a results write-up planned as docs at light depth still dispatched a second, targeted money reader, because `wiki/.../cost-gstack-full.tsv` has "cost" in its name. On gstack-evals#139, an analysis module that only sums and prints per-trial `cost_usd` raised the money class. Now a data file that no code reads, under `docs/`, `wiki/`, `results/` or `reports/`, never counts as money for its name or for a reported cost. A `spend_cap` line or a paid-provider call in that file still counts. A `_usd` or `Usd` name is not money only when it is a reported cost (made of words like cost, total, per, trial or sum) on a line with no cap, limit, budget, price, charge, reservation or estimate word, no `<`, `>` or `<>` comparison and no number set on it. Reservations, estimates, amounts, refunds, spend guards and pricing tables all still count. Over the last 100 main commits, no commit's depth, class, risk classes or targeted classes changed.
### Tests
- `test/review-targeted.test.ts`: #138's file list plans no targeted reader, and the same file read by code still plans money. A `cost_usd` that is only summed or printed names no money. Positive controls: caps, budgets, limits, optional-chain and wrapped comparisons, `Math.min`, charges and refunds, reservations and estimates from `lib/eval-plan.ts` and `lib/autoplan-run.ts`, the `trial_reservation_usd` validator, a pricing table outside a results tree, a spend cap in an unread results file, and a secret in an unread data file.
- `test/review-risk-redos.test.ts`: `usdMoney` and its comparison patterns stay under 250 ms on 100,000-character lines.
**CI carry no longer refuses a skipped file that skips here too (GST-1314).** When CI skipped every test in a file, `gstack-evidence record-ci` reruns it here through the lane's `per_file` runner, and a file that skipped everything again ended the carry with `skipped_zero_run`, so the whole free suite reran. Free Tests run 41754278952 had six such files, all env-gated or tool-gated by design (0 pass, 20 skip on a Capy machine), so every gstack /ship carry would have fallen back to the full lane.
### Fixed
- **`gstack-evidence record-ci`:** runs the per_file remainder one file at a time. A file whose Bun summary registered tests and skipped every one here too is recorded as not owed (`skipped_here.not_owed` in the CI row, `not_owed_here=<n>` on the `LANE:` line) instead of refusing. A file that registers no test here still refuses with `skipped_zero_run`, even beside a file that ran, and so does a file whose log hit the 2 MB cap, since a cut log's last summary may not be the run's. A failing file's `skipped_failed` names it.
- **`per_file` paths** reach the runner anchored with `./`, because Bun reads a bare relative path as a substring filter (`test/x.test.ts` also matched `browse/test/x.test.ts`).
- **gstack's `bun run test` lane** declares `"per_file": "bun --no-env-file test --timeout=30000 {files}"`, so the runner is on main before the Free Tests writer starts listing skipped files (#3979). `--no-env-file` keeps a local `.env` from switching an env-gated file on.
- **Docs:** `docs/pregate.md#skipped-files` describes the per-file runs, the not-owed case and the anchoring.
### Tests
- `test/ship-ci-carry.test.ts`: run 41754278952's six remainder files, each printing its own skip-only Bun summary, carry with `not_owed_here=6` and reach the runner as `./<path>`; a remainder that mixes a file that ran with one that registers no test refuses with `skipped_zero_run` naming that file; a remainder file that registers no test refuses; a remainder file whose log passed 2 MB after a nested `Ran 5 tests` summary refuses instead of counting as not owed.
**`gstack-ship-queue land --pin` can test the merged tree when a PR's base moved after its CI (GST-1316).** Under `merge: direct`, land checked the PR's own green checks, but those ran on the PR before newer commits reached main. #3886 was green on its own, landed onto a newer main, and pushed `ship/DRIVER.md` to 12,329 bytes against its 12,288-byte cap, so Free Tests on main went red.
### Added
- **`land` policy key:** `land.merged_checks` lists the commands land runs when the base moved past the pin's merge base. Land builds a throwaway commit of the pin merged onto main's tip, runs each command in a worktree of it, and merges only when every one passes. It prints `MERGED_CHECKS: pass tree=… base=… pin=… checks: …`. A failure, a timeout or a check that rewrites a tracked file refuses with the new `QUEUE_LAND_MERGED_CHECKS`, a conflict refuses with `QUEUE_LAND_NOT_CLEAN`, and nothing merges. No lane has to merge main, rerun CI and re-ship each time another PR lands first. Without the key, a stale base lands as before with a `STALE_BASE: warn … no land.merged_checks configured` line. The commands are repo commands (the merged tree's `package.json` decides what they run), so land runs them only with `--allow-repo-commands` (else `REPO_COMMANDS_NOT_ALLOWED`), with `PATH` and the locale as the whole environment and a throwaway `HOME` (no sandbox: they can still write the checkout's `.git` and `node_modules`), runs git around them with hooks off, kills a command's whole process group when it ends or times out (so a passing check that leaves a child behind passes at once), and skips them under `--dry-run` with a `DRY-RUN: land.merged_checks not run` line. A check that moves HEAD fails, land re-reads the base policy after its locked fetch, and a merged tree whose lockfile or dependencies differ from the checkout's is refused with `QUEUE_NOT_READY` before any check runs, since the linked `node_modules` can't vouch for it. gstack's own policy adopts the key after the release that carries it.
### Fixed
- **`gstack-ship-queue land --pin`:** land now checks that GitHub reports the PR clean before it runs any merged check, then takes the land lock and re-fetches main before it checks and merges, refreshes the lock right before the merge, and releases it afterward. A rerun takes over or releases a lock an earlier run of the same PR left, and `land --pin --break-lock` breaks another lander's stale lock. A rerun after GitHub merged a stale base with no `land.merged_checks` checks the tree only, with a `STALE_BASE: warn` line, instead of reporting `QUEUE_LAND_UNVERIFIED`.
- **`gstack-ship-queue land --pin`:** after the merge, land checks that GitHub merged onto the base it checked or one the pin contains. If main moved in between, it reruns `land.merged_checks` on the landed tree and reports `QUEUE_LAND_UNVERIFIED` with no land note when they fail or the key is absent.
- **Docs:** `docs/ship-policy.md#land-direct` describes the merged-tree check and the `land` key. The release section now says a release may not delete a release output, and that a rerun after GitHub merged a release fetches the pin, then reads its parent, with an unreadable parent refused as `QUEUE_NOT_READY`. `docs/troubleshooting.md` and the `docs/captain.md` refusal table cover the new code.
### Tests
- `test/ship-queue-land-direct.test.ts`: on a stale base, a pin whose merged tree breaks a stand-in budget check is refused with `QUEUE_LAND_MERGED_CHECKS` and main stays put. A pin that passes lands with main's tree equal to the checked merged tree, and a rerun checks again. A conflicting pin is refused with `QUEUE_LAND_NOT_CLEAN`, and with no key a stale base lands with the warn line. The checks see the merged commit as HEAD, and a timed-out check (even one exiting 0 on SIGTERM) or a tracked-file rewrite fails. Without `--allow-repo-commands` the checks are refused and never run, under `--dry-run` and on a PR GitHub doesn't report clean they never run, and when they run they see no token and a throwaway `HOME` while land holds the land lock. A timed-out check's background child is killed with it, a passing check that leaves a child behind passes at once, and the checkout's post-checkout hook does not run when land builds the merged worktree. A check that ignores SIGTERM still stops at its timeout, a check that moves HEAD fails, a dependency change is refused before the checks, and a `land.merged_checks` that main gains while land runs is applied. A no-key rerun lands with the tree check. A lock left by an interrupted run is taken over, another PR's lock refuses (and `--break-lock` breaks only a stale one), a lock left after the merge is released by the rerun, and a rerun without `--allow-repo-commands` that must run the checks on the landed tree refuses. The key's validation is covered, and the mid-land race test still expects `QUEUE_LAND_UNVERIFIED` with no land note on the first run.
**A release gate can now gate main every 8 hours instead of on every push, and it feeds the stable release.** It is off until main's `.gstack/ship-policy.json` declares a `release_gate` block, so this release changes nothing on its own. With the block, `.github/workflows/release-gate.yml` runs `gstack-release-gate run` every 8 hours. When main moved since the last gate, it runs the full paid gate on main's head once, records the verdict as a `release` note in `refs/notes/gstack-trains`, and publishes the `main-backstop` check run on that commit. A red gate bisects its red cases within `bisect_spend_cap_usd` and names the first bad commit. The per-push main backstop stands down while the block is set, so main is never gated twice. `scripts/release-cut.ts` is still the only thing that moves `stable`, and its 72-hour floor and exceptions are unchanged. It now reads a green release record as proof the commit was gated, and it refuses every cut while the newest release gate is red with no green gate on a later commit. That hold is the freeze. See docs/ship-policy.md#release-gate.
### Added
- **`release_gate` policy key** (`lib/release-gate-policy.ts`, which needs `min_gstack_version` at this release): `cadence_hours` (8 to 168), `gate_workflow`, `branch`, `stable_ref`, `check_name`, `on_red: hold-release` and `bisect_spend_cap_usd`, every field required.
- **`bin/gstack-release-gate`** (`lib/release-gate.ts`): `run [--dry-run] [--sha] [--ignore-cadence]`, `status` and `bisect`. `--policy <file>` reads the block from a file, for a dry run or a proof before adoption. Exit 0 means gated, skipped, inert or a dry run; 1 means held, failed or a refused policy; 2 is a usage error.
- **`.github/workflows/release-gate.yml`**: an 8-hour schedule and a manual dispatch. The check run comes from a separate job that holds `checks: write` and runs no repository code.
- **Capy skill installer follows the update channel** (`capy/skill/gstack/scripts/install.sh`, wrapper revision 4). With `GSTACK_UPDATE_CHANNEL=stable` or a stable `update_channel`, a fresh clone or a floor upgrade goes to `stable` when it meets the support floor, and to main otherwise. `--revision` still wins.
### Changed
- **`evals-main-backstop.yml`** stands down (no gate, no freeze, no check run) when main's policy declares `release_gate`.
- **`evals.yml`** routes a release gate bisect (`release-gate/<id>` refs) only under a `release_gate` block at the base revision, and that bisect publishes its prefix records as `gstack-release-record`.
### Tests
- `test/release-gate.test.ts` drives run, bisect and status against a fake `gh` and a scratch remote. `test/release-gate-workflow.test.ts` runs the workflows' policy steps on scratch repositories and pins the cron to the cadence floor. `test/release-cut.test.ts` proves a red release record refuses the cut and a green one on a later commit releases it. `test/train-gate.test.ts` covers the bisect route, and `test/capy-skill-wrapper.test.ts` covers the stable channel and its fallback to main.
- **Versions at release under `merge: direct`.** `gstack-release-stamp open` stamps `origin/main` with the new `gstack-restamp --release`, which folds every `changelog.d` fragment under one new CHANGELOG heading, bumps VERSION and `package.json` by the highest fragment bump, reruns the release tool and moves tentative `*_KEY_VERSION` constants. It then pushes `release/<version>` and opens the release PR. `gstack-ship-queue land --pin` lands a `release/<version>` head without a /ship receipt, but only when it is exactly that stamp (`RELEASE_STAMP_INVALID` otherwise), and the Version Gate applies the same check. This lets the stable cut tag a new version again now that PRs land unstamped.
- **The review summary leaves an abandoned review pass out of the receipt's counts** (`gstack-review-scope summary`, GST-1168). A pass whose dispatch ledger was never judged, that no review-log row names, and that a later judged or logged pass replaced is left out, and the summary names it on a `REVIEW_SCOPE_VOIDED:` line instead of counting its findings as found and unrecorded. A fix-check ledger and the last pass always count.
**Five routine /ship and /autoplan stops no longer wait for a Captain: each is checked, recorded in the receipt's `no_stop` field, and the run keeps going (GSTA-258 L5, plan K1).** GBrain-Evals pre-granted these after GBEV-30 stopped about seven times in one day on them. A case that doesn't qualify, and every other stop, still goes to the owner.
### Added
- **`gstack-no-stop record --class <class> --reason <text> --evidence <ref>`** (`lib/no-stop.ts`) checks a stop and appends `{class, reason, evidence}` to the branch's ledger, or prints `NO_STOP: refused` and exits 3. The five classes:
- `scoped-fix-check`: one scoped fix-check past R51's two-round cap, when the round-2 fix is docs class only or only raises timeouts. The range must start at the commit the last review pass read. It records the standing grant in the approval record and prints `GRANT:` for `gstack-review-plan --grant`. One per branch.
- `test-timeout-fix`: a fix whose every changed line is in a test file and only raises a timeout value. A line with an assertion or a comparison never qualifies. Its gate finding takes a printed `kept` reason instead of `open`.
- `pregate-flake-rerun`: one rerun of a failed pregate case at the same tree (rule 16). One per branch.
- `stamp-only-carry`: a refused carry where HEAD differs from the receipt head merged into the base only in a version stamp. /ship then takes the carry's `RECHECK:` route instead of running Steps 2–16 again.
- `autoplan-regate`: see below.
- **`gstack-no-stop import` and `list`** add an autoplan run's records to the branch and print the ledger. /ship's receipt write passes it as `--no-stop @<file>`.
### Changed
- **An autoplan plan edit made after the owner's answer carries their words when it only adds ELI5 lines.** Every added line must sit in the plan's `## ELI5` section and name no number, spend, budget, cap, limit, arm, model, judge or verdict. Such an edit carries as `reason=no-stop autoplan-regate: …` and is written to `<run>/no-stop.json`, and the run still exports the reviewed plan. A removed or reworded line, a line added anywhere else, or an edit to the reviewed plan itself still goes to the owner. The run keeps a copy of each plan file it hashes, so the edit is diffed against what the owner answered.
- docs/review-policy.md#no-stop has the table. docs/ship-policy.md lists the receipt's `no_stop` field, and docs/unattended.md describes the carry-forward change.
### Tests
- `test/no-stop.test.ts`: each class's qualifying and refused cases, the ledger, the grant row, the receipt's `no_stop` validation, stamp-only after a base merge, import, and the template wording.
- `test/autoplan-carry.test.ts`: an ELI5-only addition after the revise now carries and is recorded. A cap move still refuses.
### Fixed
- **An outside review that says it could not verify the change is no longer read as a pass (#3925).** `lib/outside-review-result.ts` printed `VERDICT: clean` / `FINDINGS: none` for a Codex reply ending `Verdict: Can't verify (one changed file excluded and four required suites blocked)`. Now the reply's last `Verdict:` line has to approve the change (`ready`, `LGTM`, `approve`, `ship it`, `pass`, `clean`, `good to go`, or exactly `NO_FINDINGS`) or say `not ready`, which still blocks. Anything else, an approval with a question mark, or an approval that says something was excluded, blocked, skipped, not run, not executed or could not be verified, is `VERDICT: unverified` with the new `REASON: unverified_verdict` and exit 4. Which lines count as a verdict is unchanged. It is still a `Verdict:` line read with inline code blanked, so a verdict written only in code reads as unverified. See docs/troubleshooting.md#outside-review-unverified-verdict.
- **A numbered finding without a P0-P3 tag is no longer dropped (#3925).** A reply with `1. **Nonblocking — x**`, `Verdict: ready` and `END OF REPORT: 1 findings (0 blocking)` printed `FINDINGS: none`. In a reply with a `Verdict:` line that doesn't conclude it found nothing, every numbered item is now counted as a finding, and one with no readable severity makes `FINDINGS:` print `unspecified`, in the helper and in `gstack-outside-voice run`. The verdict is unchanged (`clean` here, since the reviewer approved). A consult answer with no `Verdict:` line, and a real clean reply, still read `clean` / `none`; a consult verdict that picks an option (`Verdict: go with B`) is `unverified`, since a consult isn't a pass/fail review.
### Fixed
- **The AskUserQuestion log hook no longer kills a slow first run on Windows, and its calls now share Claude Code's 5-second hook limit.** `question-log-hook` gave each `gstack-question-log` call a fixed 3-second timeout. On a cold Windows runner the first call takes just over 3 seconds (3,149 and 3,188 ms in windows-pregate runs 41605458462 and 41406459043), so the hook killed it and logged "exited null" even though Claude Code allows the hook 5 seconds. A fixed timeout per call also let several questions run well past those 5 seconds (six slow calls took 16 s). Now the calls share one budget: 5 seconds from when Bun starts the hook, less a 500 ms exit reserve. On Windows the hook finds Git Bash before it reads that budget, so the lookup comes out of the budget instead of adding to a call's time. When less than 250 ms is left, the hook skips the remaining questions and logs which ones; a killed call's log line names the signal and how long it ran. The budget doesn't cover the bash shim that starts Bun, and the first Git Bash lookup on a fresh install has no time limit of its own, so Claude Code can still stop the hook in those cases.
### Fixed
- **A tap or touch right after a page loads now lands where you aimed (GSTA-262).** `$B goto <url> && $B tap 240 440` on a phone-emulated tab could hit a different element. `$B goto` returns once the HTML is parsed, but before the page has drawn a frame with its mobile viewport, and Chromium places touch points using the last drawn frame. So the touch was scaled by 0.401 (393/980, the desktop default width) and landed at (96, 176), on the zoom area instead of the fire button. Every `tap`, `swipe`, `pinch` and `touch down` now waits for the page to draw a frame before the first touch (two animation frames; skipped for a hidden tab, and capped at one second, so a page that replaces `requestAnimationFrame` or navigates mid-wait can't stall the gesture). `$B touch cancel` also waits for a drawn frame before it returns, because Chromium delivers the touchcancel event with the next frame, so a check right after it could still see the old touch. This also fixes the flaky `touch input > held fingers are cancelled on navigation and by touch cancel` test (22 failures on 2026-11-03 and 12 more the next day, all in free shard 4). Under CPU load, a loop of navigate-then-touch missed 45 of 60 times before the fix and 0 of 60 after; the whole test file failed 1 of 20 runs before and 0 of 20 after. A touch-then-cancel loop under the same load saw the stale touch 2 of 60 times before the cancel wait and 0 of 300 after.
### Fixed
- **Two paid flaky cases now accept what their fixtures already allow, and a third prints the evidence it was missing.** These come from main backstop run 41817566885 (main b244e6d1). Each case failed once and passed its rerun.
- `plan-design-with-ui-scope`: in CI the image generator has no API key, so no mockup ever gets approved. The fixture's declared Voices menu still said "with the approved mockup as anchor". One run told the truth and wrote "(text-only this time, since mockups weren't available)" instead, and the picker refused it. The fixture now declares that one replacement. A free test replays the captured question; it fails on main and passes with the fix, and six variations that are not the declared replacement are still refused.
- `ship-docsync-current`: the docs interface says a private artifact can live "in a new directory you create there" under the fixture home, and `gstack-docs-candidate snapshot` does not create the folder for its `--out` path. The parent ran `mkdir -p <home>/ship2` as its own call, and the detector refused it. The detector now accepts a plain `mkdir` or `mkdir -p` of one new folder directly under the home, as its own call. A free test replays the whole captured run, which fails on main and passes now. The test also shows that 25 other `mkdir` forms are still refused: the repository, protected roots, nested paths, `..`, symlinks, globs, variables, options and joined lines.
- `autoplan-guard-pty`: the first guarded call got an unverified allow (`journal_lag`), because Claude Code didn't write its journal within the guard's 2-second window. The run had all four allows, so it never printed the journal, and cleanup deleted it. Now any run that falls short of four verified allows prints the journal and the time of each decision. The guard and its window are unchanged.
## [1.95.5.0] - 2026-11-04
- **gstack's own PRs now merge at their own green head.** Its ship policy sets `merge: direct`, so the Captain lands each READY PR with `gstack-ship-queue land --pr <n> --pin <sha>` instead of a train, and VERSION waits for the release stamp. The policy's `min_gstack_version` rises to 1.95.0.0, the release that introduced the key, so an older gstack refuses it by name (`POLICY_GSTACK_TOO_OLD`) and asks for an upgrade.
+1 -1
View File
@@ -1 +1 @@
1.95.5.0
1.96.0.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.95.5.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.96.0.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
-1
View File
@@ -1 +0,0 @@
- **A reviewer's "I found nothing CRITICAL" no longer counts as a CRITICAL finding.** /ship's review collect step used to turn a reader's own closing sentence into a blocking finding when the sentence only denied a severity ("I found nothing CRITICAL.", "I found no defect that is CRITICAL.", "Neither finding is CRITICAL."), so the review loop counted blockers that named no defect and a Captain had to grant an exception. A sentence that is wholly a denial, a count of findings or "nothing blocks", with no file, line, exception clause or field on its line, now collects nothing at the label it denies; when the same line also holds other prose, that line stays on record as an INFORMATIONAL note instead of a blocker. Every real blocker still collects: a file:line, an exception clause ("except the token leak in log.ts:3"), "nothing else" or "no other", a defect named in the same sentence, another labelled sentence, a list item or a Severity or Blocks field keeps the line at its label. (GST-435)
-5
View File
@@ -1,5 +0,0 @@
### Fixed
- **The secret scanner no longer reads the sha in an owner's `review_loop_grant` as a phone number, or a git sha that starts with `bc1` as a Bitcoin address. Both made free tests flaky.** About one 12-character sha in 300 is all digits, and `pii.phone.e164` flagged it, so `gstack-approvals add` refused the owner's `review_loop_grant <sha>` grant with APPROVAL_RECORD_SECRET (exit 3). A digits-only span right after `review_loop_grant` or `review_loop_exception` is now a sha. A phone number with a `+`, spaces or dashes, or the same digits anywhere else (an all-digit sha in other wording included), still reports (Free Tests 41667825418, 41766865679, 41680498938, 41843093520). A sha that starts with `bc1` was redacted as `<REDACTED-pii.wallet>` in CSO's history range. A `bc1` address now has to use the bech32 alphabet and carry a valid bech32 or bech32m checksum, which every real address has. An address with a mistyped character no longer reports (Free Tests 41715139988). Both rules have true-positive controls in `test/redact-context-exemptions.test.ts`.
- **`test/plan-reality-receipts.test.ts`**: the commit-sha case writes its receipt up to the sha's first hex letter. A bare all-digit prefix is not a receipt by design, and about one 10-character prefix in 110 is all digits (Free Tests 41669379416, 41790613651).
- **`test/helpers/pregate-fixture.ts`**: the fixture's bare origin turns off auto-maintenance, as `test/helpers/restamp-fixture.ts` already does. Git's local transport drops the free runner's maintenance-off setting, so each push into the origin started a detached `git maintenance run --auto` that could change `origin.git/objects` while the template was being copied (ENOENT, Free Tests 41760556132). A new test traces a push and checks that no maintenance starts.
@@ -1 +0,0 @@
Claude Code and Cursor no longer start /land-and-deploy or /modernize on their own. Type `/land-and-deploy` or `/modernize` to run them; a request in plain words like "land it" now gets a pointer to the command instead of a mistaken merge or rewrite of an old branch. Both skills already stopped for your approval before merging or pushing, so this removes surprise rather than a missing safeguard. /ship, /careful, /freeze, /guard and /unfreeze still start from plain words like "ship it". gstack's router and the CLAUDE.md routing block now tell the model to point you to these two commands instead of invoking them. Under the hood, a new `manual_only: true` template flag maps to `disable-model-invocation: true` on the Claude and Cursor hosts, and host `conditionalFields` now work in denylist mode too.
@@ -1 +0,0 @@
- **The release gate fails closed.** A gate run that concluded success but left no report-verdict artifact was recorded GREEN; it is now INCOMPLETE and re-dispatched once like any other incomplete gate. A release-gate run that failed before it had a commit to gate (a refused policy, unreadable notes, an unresolved branch) left the workflow job green with no check run anywhere; that job now fails. (GST-1263)
@@ -1 +0,0 @@
- **/ship's carry route now says to push before it carries, and the docs give the real fix-loop cap.** `gstack-ship-preflight`'s carry `NEXT:` line went straight from the pregate to `carry`, which refuses a head that isn't pushed. It now says to commit, push with `gstack-ship-release push --early` (the secrets scan runs first), wait for CI with `gstack-ci-carry wait`, run the pregate and then carry. docs/ship-policy.md and docs/unattended.md give the same order. docs/review-policy.md, docs/ship-policy.md and docs/unattended.md now say `review_scope: diff-plus-blocking` stops at Step 9.4's third fix cycle, because the two-round review-plan cap runs only under `frozen`. (GST-1346)
@@ -1 +0,0 @@
- **/ship's workflow now matches what its tools do, so main's backstop judge can pass.** The `ship/SKILL.md workflow` judge still failed on main at 622e8b1f (run 41884510834) because the prose and the code disagreed in five places. Step 11's Codex passes and every `CODEX_MODE` branch now follow the review plan's `DISPATCH:` lines, as /ship's driver does; they no longer say the native pass always runs. The review policy now gives one fix-loop cap. It is two rounds under `review_scope: frozen` (the review plan's stop) and Step 9.4's third fix cycle under the other scopes. Step 16 says `finish` runs stages 1–4, diffs the reviewed tree and checks each lane's receipt itself, what to pass when no reviewed tree was saved, and that a stage 5 commit of regenerated outputs is content-preserving. A Step 1 carry now says to push with the secrets scan and wait for CI before the pregate, and that it ends the run with its own result line. The ship, review and unattended docs now say `diff-plus-blocking` stops at Step 9.4's third fix cycle, as the workflow runs it, and give the same carry order. The Step 5 CI-carry push asks no consent, and Step 6.5's Greptile question says it cannot undo that push. No behavior changes. Reviewer briefs are byte-identical; the adversarial section's pin is updated under GST-1346's brief-identity check. (GST-1346)
@@ -1 +0,0 @@
- **/ship's workflow now gives one answer where it used to give two, so main's backstop gate can pass again.** The `ship/SKILL.md workflow` judge failed twice on main at b244e6d (run 41817566885) because its readers kept finding the same conflicts. Steps 12 and 16 now say their item and stage commands serve only a `STOP:`, apart from stage 1's builds, which go to the one `gstack-ship-release finish` call as `--build`. Stage 5 says `finish` already made the release commit and only regenerated outputs are left to commit. The CI-carry early push names its full draft-PR command, says Step 6.5's question covers only Greptile, and runs `accept` after Step 17's push. A PR that `gh` failed to create (Step 19 exit 3) now ends Step 22 with `status=refused` instead of a result line that needs a PR number. The ship skill is 70 bytes smaller. (GST-1346)
@@ -1,3 +0,0 @@
### Fixed
- **Under `merge: direct`, a red main backstop no longer freezes the queue for good.** The backstop wrote a freeze that `land --pin` and `release-cut` refused under, but the only code that cleared a freeze was a repair train's verify, and `train build --repair` is off when trains are off. A later green backstop didn't clear it either, so the freeze from run 41817566885 (#3992) blocked every land and the release cut. Now `gstack-ship-queue land --pr <n> --pin <sha> --repair` lands exactly one repair PR while frozen: it runs every normal pin check, records the repair on the freeze under the land lock, and leaves the freeze active. The main backstop clears the freeze only when its gate is green on a main at or after the freeze's main that contains that repair's merge, and it writes `cleared_by: "repair #<n>; backstop <run> green on <sha>"`. A red gate keeps the freeze and rewrites its reason; when that main contains the repair, the slot opens for the next repair. A land without `--repair` stays refused while frozen, and nothing clears a freeze without a green gate.
@@ -1 +0,0 @@
- **The evidence ledger records /review as helps, with its three caveats beside the status.** R2 at the after pin (gstack-evals #207, superseded by #211) measured /review with the contract Stop hook on Opus 5.5 at gstack 699a41cb: blocking recall 26.2% against 3.6% for plain Opus 5.5 with no gstack, +22.6 points (95% CI +11.0 to +33.7). The `review` row in `docs/inventory/evidence.json` now says helps, with its sources, scope and R48 KEEP. A new `caveats` field, allowed only on helps, no-effect and hurts rows, holds the result's limits, and `docs/inventory.md` and `gstack-skill-bill` print them beside the status: /review and the hook count as one unit (42 of 66 sessions ran the readers only after the hook blocked), 41 of the suite's 42 bugs came from gstack-family reviews, and stacks-r1 is a development suite, so the result makes no release claim. The earlier review results stay under `prior`.
@@ -1 +0,0 @@
- **The AI scorer that graded skill instructions for clarity is gone, so main's backstop no longer goes red on a model's opinion of /ship's prose.** The `ship/SKILL.md workflow` judge failed main's full run twice (runs 41817566885 and 41884510834), freezing merges. Garry's call: the evals that check real outcomes are the real eval. The 21 cases that had an LLM score a skill's instruction text for clarity, completeness and actionability are removed, along with their input builders, response cache, calibration configurations, the cookie-workflow manual-review approval and the control-bundle script. The three remaining judges in `test/skill-llm-eval.test.ts` (QA anti-refusal, cross-skill greptile consistency, voice directive) keep their thresholds and samples, and the judge file's supervision wall shrinks from 3,170 to 480 seconds to match. The paid report now rejects any manual-review claim. (GST-1371)
@@ -1 +0,0 @@
- **The last two AI checks that guessed at agent behavior are gone, and the Greptile consistency check is now free.** `qa/SKILL.md anti-refusal` (a model predicted from /qa's text whether an agent would refuse to open the browser) and `voice directive tone` (a model judged whether the voice directive covered its topics) are removed, with their prompt builders, schemas, calibration configurations and corpora, budgets and durations. `cross-skill greptile consistency` is replaced by `test/review-bot-history-facts.test.ts`, which compares the actual history facts in /review, /ship, /retro, `review/greptile-triage.md`, `gstack-retro-metrics` and `gstack-review-bots` (paths, row columns, record types, categories, legacy rows) against `lib/review-bots.ts` with no model call, and fails on any of ten planted disagreements. `test/skill-llm-eval.test.ts` had no cases left and is deleted; no other paid case changes. (GST-1403)
@@ -1 +0,0 @@
- **Agents comment on GitHub only under a bot identity.** CONTRIBUTING.md (issue triage, the wave process and porting step 5), docs/triage.md, docs/captain.md, docs/recommended-harness.md and /modernize let an agent post the close note, the thank-you, the ask for missing information, tags and review requests, under its bot identity (`capy-ai[bot]` on Capy) and never the maintainer's own account; an agent with no bot identity drafts the note for the maintainer instead. Email, chat, DMs and social posts to people, and reading the maintainer's inbox, stay off limits for agents. (GST-1417)
@@ -1,2 +0,0 @@
- **The land stamp no longer rewrites other projects' versions.** A PR that wrote `QM v0.1.15` into a README listed in `stamp_files` landed as `QM v0.0.115`, because the stamper read any `vX` newer than the branch's fork point on a line the PR added as the PR's own tentative stamp, and the tree receipt called the rewrite stamp-only. Built-in `vX`, `since: X` and `(was X)` mentions above the version being stamped now move only when the branch itself recorded that version, so a third-party version above it survives byte for byte and a rewrite of one reads as a product change. A declared `stamp_patterns` slot keeps the old bound but moves only inside the slot. (GST-1245, reported by HARN-7)
- **`land` restamps after a stamp is reset.** When a PR branch was reset to its reviewed pin after land stamped it, the rerun resumed the old journal and refused with `QUEUE_TRAIN_MOVED` until the journal was deleted by hand. Land now discards that journal, says so on a `JOURNAL:` line, and restamps the pin. (GST-1245)
@@ -1 +0,0 @@
- **A Codex outside review that puts its recommendation under a `## Recommendation` heading now counts as finished.** The outside-review parser accepted only an inline `Recommendation: <action> because <reason>` line. When the model wrote the recommendation as a Markdown heading with the sentence on the next line, /ship and /codex reported the finished review as unavailable (`missing_markers`). A heading followed by a sentence with a `because` reason now reads the same as the inline line. A heading with nothing under it, a sentence with no `because`, a bullet or quote line under the heading, or a heading inside a code fence still reports the recommendation as missing. Severity labels and `Verdict:` lines under the heading are read exactly as before, so a blocking review still blocks. (#3980, thanks @nfitzsimmons)
@@ -1 +0,0 @@
- `/sync-gbrain`'s GBrain Search Guidance block now tells agents to search past plans, retros and learnings with `--source gstack-artifacts-<user>`, the source named after the repo `gstack-artifacts-init` creates. It still said `gstack-brain-<user>`, the name from before the v1.27.0.0 rename, so every agent following the block got `unknown_source`, and a hand fix to CLAUDE.md reverted on the next `/sync-gbrain`. This repo's own CLAUDE.md copy and the write-surfaces checklist are fixed too. `test/regression-3799-sync-gbrain-artifacts-source.test.ts` pins the block to the name `gstack-artifacts-init` uses. Reported in #3799 by @extremenelson.
@@ -1 +0,0 @@
- **Pregate no longer crashes on large diffs.** When many touched files each reach many tests, the lanes check stopped with `lanes: threw: Maximum call stack size exceeded`, so PREGATE was incomplete and /ship was blocked before any test ran (seen on a 335-file gbrain diff). The lanes check now records every provenance row without hitting the runtime's argument limit, and the secrets check does the same for scanner findings on a very large set of added lines. (GST-1334)
@@ -1 +0,0 @@
- **`land --pin` no longer leaves the land lock stuck in `merging` when GitHub applies a lock push and still answers HTTP 502.** On 2026-11-04, during GitHub 502s, the first land of #3990 reported "the land lock push to origin failed and the lock there is unchanged" while origin's `refs/notes/gstack-land` already held #3990 in `merging`. Its release then failed against the old lease, and two reruns refused the same way until `--break-lock`. Now a lock push that reports failure asks origin (`git ls-remote`) what the ref holds, and treats a push origin applied as written. When a lock fetch fails, the read takes origin's sha from `ls-remote` if this clone already has that commit, so a rerun resumes its own lock instead of leasing a stale sha. When a phase change or release is rejected because origin holds a newer lock from this same run, land adopts that sha and retries once, so a refusal still releases the lock it wrote. A push that origin can't confirm either way now says so instead of claiming the lock is unchanged.
@@ -1 +0,0 @@
- **Step 11's review voices no longer leave an unsent brief behind (GST-1368).** When /ship ran Step 11 on its own output folder, which happens after a fix round moves the head, `gstack-review-dispatch pass` built the manifest from the whole pass-1 plan and wrote the specialist brief into the ledger, but the voices program never sends it. `gstack-review-dispatch verify` then failed the run with `BRIEF specialist-batched NOT_SENT`. Step 11 now asks for `pass --voices`, which keeps only the plan's voices, so nothing is rendered that no reader gets. `--voices` refuses `--extras`, and a reviewer pass refuses to resume a voices-only manifest. A ledger written before this fix still verifies dirty; rerun Step 11 with a fresh `--out` and `--pass` to get a clean one.
@@ -1 +0,0 @@
- **Trunk Dynamic CI now watches pull request CI in learning mode, and it can't skip anything.** A new `trunk-dynamic-ci` job in `free-tests.yml` asks Trunk, on every pull request from a branch in this repository, whether it would run or skip `free-suite` and `macos-named-regressions`. Trunk records the answer. No job reads the answer, so every test still runs, and every required check keeps its name and its result. The job runs one pinned action. It checks out no code, holds no GitHub token scopes, skips fork pull requests, skips its step when the token is missing, and never fails the run or a required check. The one exception is when the job itself can't start or finish, for example when GitHub can't fetch the pinned action. Then the job's own check shows red, and if a rerun doesn't clear it, the fix is to delete the job. Merge evidence (the `free-tests` aggregate, CI carry, the pregate, the train gate, the backstop and the release gate) doesn't read Trunk. Review the results one week after this merges. The details are in `docs/TESTING_INTERNALS.md`.
@@ -1 +0,0 @@
- **Adding `review_exclude_paths` to the base policy no longer stops every receipt written before it.** `gstack-ship-queue ready`, train build and land refused each older receipt with `field=review_exclude expected=<digest> recorded=none`, even when the PR touched none of the new patterns. On harness-evals one policy PR stopped 9 queued PRs, and the only fix was to rerun review on each unchanged head. The queue now accepts a receipt with no recorded digest when no file the PR changes matches any current pattern, since its reviewers read the same full diff. It still refuses a recorded digest that differs, a PR that touches a pattern (excluded or kept), and a base policy it can't read. Carry still compares digests, so an affected PR that merges main still needs /ship once. (GST-1408)
@@ -1,3 +0,0 @@
### Fixed
- **A design-token file is no longer a secret path by its name alone, so a copy edit to one gets the review depth its content earns.** The review planner treated any path with a `token` or `tokens` segment as a secret path and a gate path, so founderstar's `scripts/ui/token.gd` (Godot UI design tokens) pushed a copy-only PR to the full panel with 11 readers. A token-named style or engine file (`.gd`, `.css`, `.scss`, `.sass`, `.less`, `.styl`) now counts as a secret path only when its added or removed lines hold a value the credential scanner behind pregate's secrets check reads as a secret, or have no text to read. Every other token file still counts by name. That covers code such as `lib/token.ts`, where auth tokens are minted and checked, and also `token.json`, `tokens.yaml` and binary token stores. Secrets, credentials and keychain names still count by name alone, and so do dotenv files (`.env`, `.env.production`, but not `.env.example`), SSH private keys (`id_rsa`, `id_ed25519`) and key-store files (`.pem`, `.key`, `.p12`, `.pfx`, `.jks`, `.keystore`, `.ppk`), which the planner didn't flag before.
@@ -1,5 +0,0 @@
**The two red cases in the 2026-11-01 Codex census (#3784) are fixed: one was a test fixture and one was unclear wording in /pair-agent.** Run 41279336076 (main @ 482ffd1) went red on the codex-astra arm.
### Fixed
- **/pair-agent says the instruction block includes its first and last `=====` lines.** The skill said the command "prints everything between its `=====` divider lines", so Codex copied every line but those two outer dividers, and `safety-pair-agent-block` failed. The rule's pinned hash in `test/helpers/safety-rules.ts` moves with the new wording. With the new wording, the rule arm passed on gpt-6-astra and on Claude, and the removed-rule arm still failed on Claude.
- **`cso-diff-mode`'s fixture now has a page that reads the paid plan.** The forged payment webhook wrote to a map that nothing read. /cso daily reports leave out a candidate whose impact needs code outside the snapshot, so Codex correctly left out the forgery finding. The fixture now serves a premium report only to accounts marked paid, which makes the forgery an in-snapshot entitlement bypass. The assertions are unchanged.
@@ -1,5 +0,0 @@
**The rule-removed control for safety-rule evals now really removes the rule on the Codex host (GST-840).** The Codex host installs every gstack skill under `~/.codex/skills` as a link into the checkout. A removed-arm run therefore still had the rule in the installed skill, and in GSTA-211's `safety-pair-agent-block` run the model read it there and passed the control.
### Fixed
- **`test/helpers/codex-host-runner.ts`**: under `GSTACK_SAFETY_ARM=removed`, each installed skill whose markdown carries a registered safety-rule paragraph gets a private copy of every linked directory on the way to that file (the skill directory itself or a linked directory inside it), and the file is rewritten with that paragraph deleted. Files are replaced, never written in place, so the checkout is never touched. The `rule` arm is unchanged.
- **`test/codex-host-safety-arm.test.ts`** checks both arms on a linked install like the one `./setup --host codex` makes, including a rule file under a linked directory inside a real skill directory, with no model and no setup run.
@@ -1,9 +0,0 @@
**A remote pregate killed by a machine restart is rerun, not destroyed (GST-1028 b).** When a lane's machine restarted, its background pregate died but its Ubicloud VM kept running, and the line the pregate printed at create time told lanes to destroy it. Lanes did, and paid for a fresh VM, even though rerunning the same command reattaches to the live VM and finishes the run.
### Changed
- **The create-time line says to rerun first.** `pregate-remote.sh` now prints "rerun the same command to reattach; if the rerun does not reattach to it, or you give up, destroy it with: …".
- **`docs/pregate.md` explains reattach.** It lists what the run key covers (the tree, the planned files, the commit `origin/HEAD` points at, `UBI_LOCATION`, size, environment, and the runner script and setup it uploads), so a lane knows when a rerun will find the old VM. It also says to destroy the old VM when the rerun creates a new one instead.
- **The /ship driver says it too.** `ship/DRIVER.md` tells a lane to rerun the same command after a restart, and stays under its 12,288-byte cap.
### Tests
- `test/ubicloud-pregate-remote.test.ts` pins the new create-time line, and `test/ship-driver.test.ts` pins the driver's restart line.
-12
View File
@@ -1,12 +0,0 @@
**A full `gstack-ship-preflight` no longer merges the base under an open /ship run's reviews (GST-1204).** Running the full preflight again in the middle of /ship merged the base into the working tree (Step 3) while review readers were diffing that tree, which voided a review pass (gbrain-evals #317, a Capy lane).
### Fixed
- **`gstack-ship-preflight`:** `--run <id>` records the /ship run once Steps 1-3 pass: a marker in the checkout's git dir, and an open run on the checkout's own lease, so the Capy driver needs no workflow lease. A later full preflight on that checkout stops with exit 3, before any fetch or merge, until `gstack-ship-release finish` clears the marker or the run is 120 minutes old (GST-1205's rule) or its holder is gone. The STOP names the run and the marker and says what to do: rerun Step 5 with `--tests-only`, let the /ship finish, or remove the marker of a run that stopped. `--tests-only` and `--audit` are never stopped. /ship's own first preflight, and a rerun after a Step 3 STOP, still work. A new `--run` id from the open run's own holder (the Claude Code session, or Capy's `capy-server`) replaces a run that stopped before finish; the same id, no `--run`, or another live session's run still stops. A `--run` the lease can't record (its lock held by a live process) stops before the fetch instead of going on unprotected. A `--run` value that isn't a plain id is a usage error (2).
- **`gstack-ship-release finish`:** clears the run's marker once Steps 15-16 pass.
- **`gstack-lease open --run <id> [--pid <p> | --from <pid>]`:** exits 0 while that run is open on a live holder and 1 otherwise, using the same released, 120-minute and dead or reused-pid rules as `live`.
- **/ship Step 1** passes `--run <SESSION_ID>`, and **`ship/DRIVER.md` Phase 2** passes `--run <run dir name>`.
- **Docs:** `docs/troubleshooting.md#ship-preflight-run-open` explains the new STOP; `docs/capy-install.md` lists `gstack-lease open`.
### Tests
- `test/ship-preflight.test.ts`: a full rerun with the run open stops before the fetch and leaves HEAD, `origin/main` and the tree untouched, with or without `--run`; the driver's Phase 2 sequence refuses a mid-run full preflight while `--audit` and `--tests-only` go on; a finished or aged-out run doesn't block; after a stopped run and `gstack-skill-end`'s real release, a new id from the same holder goes on while another live holder's run is still refused; an unrecordable `--run` stops before the fetch; /ship's first preflight and its rerun after a conflict STOP work; a bad `--run` is usage.
- `test/gstack-lease.test.ts`: `open` for an open, released, aged-out, reused-pid, dead and unrecorded run, with `--pid`, `--from` or neither, plus usage errors.
-11
View File
@@ -1,11 +0,0 @@
**Three /ship tooling fixes from the gbrain-evals F1 rollout (GST-1204).** Version strings quoted in lane reports no longer trip the secret scan, `gstack-pr-shots` renders on a fresh Capy machine, and `gstack-models` explains why it has no outside-voice role.
### Fixed
- **Secret scan (`pii.ip_public`):** a four-part version that gstack names right before it, shaped like a gstack release (major 0 to 2), is not read as a public IPv4 address. That covers `gstack 1.94.4.0`, `upgraded gstack from 1.93.14.0`, and the install check's trailers `gstack: ok 1.94.4.0 for=ship` and `gstack: fail claude,codex (1.93.37.0)`. It also covers the far end of a range whose first end is a declared version with the same major (`gstack 1.94.4.0 to 1.94.8.0`). Before this, every push and PR-body post that quoted those lines stopped on a MEDIUM finding. Real addresses still report: `gstack 8.8.8.8`, `gstack talks to 8.8.8.8`, `gstack-proxy 1.128.1.0`, `my-gstack 1.94.4.0`, `landed in 1.94.4.0`, `gstack 1.94.4.0 - 8.8.8.8`, and a range with no declared first end.
- **`gstack-pr-shots` and every renderer that uses gstack's own browser:** when Playwright's Chromium isn't installed (the Capy install defers it to first use), the renderer now runs the checkout's `bin/gstack-browser-ensure` once and retries. Before, it stopped with `browse newtab failed: [browse] Starting server...`, which hid the cause. When the install fails, the error names the launch failure and the installer's last line instead of the daemon's banner.
- **`gstack-models resolve --role outside-voice`:** it still exits as a usage error, but now says the outside voice follows the harness, not the model policy, and points to docs/model-policy.md.
### Tests
- `test/redact-context-exemptions.test.ts`: six gstack-named version shapes stay clean; twelve address controls still report MEDIUM.
- `test/aside-render.test.ts`: a missing Chromium runs gstack-browser-ensure once and the render proceeds; the install time doesn't count against the render deadline; a failed install names the launch error; the existing newtab cases are unchanged.
- `test/model-policy-cli.test.ts`: the outside-voice hint.
-1
View File
@@ -1 +0,0 @@
- **Three /ship papercuts from gstack-evals#191 are fixed (GST-1219).** `gstack-review-dispatch judge` no longer crashes when every reader came back clean: an empty `merged.jsonl` with no `judged.jsonl` now writes an empty judged set and prints `JUDGMENT: ok nothing to judge`, while a merge with findings and no judged file is refused with the finding count. `gstack-review-dispatch pass --pass` reads its id as lowercase, so `--pass P1` runs pass `p1`; an id with any character other than letters, digits and dashes gets its own error naming that rule instead of the missing-flag message. `gstack-docs-candidate snapshot --docs` takes a comma-separated list as well as repeated flags; a value that names an existing path is kept whole, and a space-separated list or an empty value is refused with a pointer to commas or repeated flags.
-13
View File
@@ -1,13 +0,0 @@
**A Ubicloud run survives a dead controller (GST-1220).** When the machine that ran `ubi-runner.sh up` died, its VM kept running but its ssh key and state lived only on that machine, so no other machine could pull the results or destroy the VM before its TTL. Paid runs were lost with the controller.
### Added
- **Encrypted recovery bundles and `recover`.** With `UBI_RECOVERY_DIR` set, `up` writes each VM's key, state and tags to `<dir>/<owner>/<vm>.bundle` (mode 600). The bundle is AES-256 through `openssl enc` under keys stretched with PBKDF2 from `UBI_RECOVERY_KEY`, or `UBICLOUD_API_KEY` when that is unset. An HMAC-SHA256 is checked before anything is decrypted, and a bundle renamed or moved to another VM or owner is refused. `recover [--owner OLD]` on another machine rebuilds the state, so `pull`, `ssh` and `down` work there, and prints `RECOVERED`, `RECOVER_GONE` or `RECOVER_FAILED` for each VM. A confirmed `down` deletes the bundle. Run `recover` only after the creating machine is dead, because it can't tell a paused controller from a dead one.
- **`--counted` runs require a bundle.** `up`, `run` and `verify` take `--counted` (or `UBI_COUNTED=1`) and refuse before any create with `UBI_RECOVERY_REQUIRED` or `UBI_RECOVERY_UNAVAILABLE` when no bundle can be written.
- **`--tag key=value`** is kept in the VM's state and bundle, and `recover` prints it, so a shard map can be rebuilt from the bundles.
- **A results sink on the VM, off by default.** `up --sink https-put:<presigned URL> --sink-path PATH` starts a pusher on the VM that uploads `heartbeat.json` and the PATH entries as `results.tgz` every `UBI_SINK_INTERVAL` seconds (300 by default), so results still arrive when every controller is gone. No account credential goes to the VM. The URL is written to a 600 curl config file over ssh, never into a command line or a log, and the run record never keeps it (pass it as `UBI_SINK`). Snapshots skip `~/.ubirun-*`, and one over `UBI_SINK_MAX_MB` is never uploaded, so the last good one stays. `down` stops the pusher and makes one last push, and a failed one prints `SINK_FLUSH_FAILED` and turns a passing `run` or `verify` of that VM into exit 1. A counted rerun refuses to reattach to a VM whose bundle is missing or doesn't open. No bucket exists yet, so nothing uses the sink until a run names one.
### Changed
- **`--pass NAME` forwards only what you exported.** The value is the one your environment held when the runner started, and the runner refuses `UBICLOUD_*`, `UBI_RECOVERY_KEY`, `UBI_SINK` and every variable it sets itself, so a variable inside the runner can't stand in for yours or carry its recovery secret or sink URL to a VM.
### Tests
- `test/ubicloud-runner-recovery.test.ts` (new, offline on the fake VM) covers a controller dying after `up` while the sink keeps pushing, recovery on a second controller followed by `pull` and `down`, wrong-secret, tampered, renamed and moved bundles, an API outage and a gone VM, the counted refusals, the sink-spec rules, a failed final push, an oversized snapshot, and a reaped VM's failed push not failing an unrelated run. It checks every output stream and the ssh log for key bytes and the presigned URL.
-11
View File
@@ -1,11 +0,0 @@
**`gstack-post pr-body` keeps a PR's opening paragraph and Capy badge, and a GitHub Actions run list no longer reads as a phone number (GST-1227).** Reported from truther#1174 on /ship 1.94.15.
### Fixed
- **A /ship post keeps the text above the PR's first heading.** Since GST-412, a body file with a receipt above its first `## ` section dropped everything above the PR's first heading, so a /ship run on a Capy-opened PR deleted the opening paragraph and the `<!-- capy-badge:start -->…<!-- capy-badge:end -->` badge. That text is now kept byte for byte and the new receipt goes right under it, so the next /ship post doesn't read it as a stale note under the receipt. Opening lines the file brings of its own still replace it, and a stale note between the receipt and the first heading is still dropped and named on stderr.
- **A Capy badge at the end of the body stays there.** A `<!-- capy-badge:start -->` … `<!-- capy-badge:end -->` block that ends the body after its first receipt or heading is now a piece of its own, so rewriting the section above it no longer deletes it, new sections go above it, and a badge only the file brings goes last, once, so reposting the same file gives the same bytes. Every other marker block, a badge with text after it and an unclosed marker stay inside their section, as before.
- **Every run id in a `runs=<id>/<attempt>,<id>/<attempt>` list is exempt from `pii.phone.e164`.** Only the id right after `runs=` was exempt (and only at 11 digits), so the CI carry and run clear receipts `lib/run-evidence.ts` prints needed `--confirm` from their second run on. The scanner now exempts each 11- or 12-digit id of that list (`run=` too) and an id after an `actions/runs/` path with no leading `/` when it starts the line or follows whitespace, `(`, `[`, `<` or a quote. Each id needs its `/<attempt>`, so `runs=4155550123`, `runs=4155550123/1`, `sales-actions/runs/4155550123` and `+1 415 555 0123` on the same line still report. See docs/troubleshooting.md#redact-run-id-as-phone.
### Tests
- `test/gstack-post.test.ts`: "a full post keeps the opening text and a badge block above the first heading or at the end of the body byte for byte" and "run ids in a runs= list or an actions/runs/ path are not phone numbers; a phone number beside them still is" both fail on main. Four GST-412 tests that expected the opening text to be dropped now expect it kept.
- `test/gstack-post.test.ts`: "a marker block that is not a trailing Capy badge stays inside its section", "a trailing badge the file brings lands at the end once and reposts byte for byte", "a fenced heading inside a marker block and an unclosed badge marker change nothing about the sections" and "two marker blocks with the same name merge by their sections" pin the main-branch handling of every other marker block.
- `test/redact-context-exemptions.test.ts`: run ids in a `runs=` list or an `actions/runs/` path stay clean (5 cases, all failing on main), and 10 controls, `call +1 415 555 0123` and `sales-actions/runs/4155550123` among them, still report.
-6
View File
@@ -1,6 +0,0 @@
**Design exits ease out, never ease in (GST-1240).** `/design-consultation` wrote `exit(ease-in)` into every DESIGN.md it made, and `/design-review` checked against the same rule, so an exit that lags was both generated and never flagged. Ease-in starts slow at the moment the user is watching, so a dismissal, menu close or toast exit reads as lag. Rewrite of #3900 by @nfitzsimmons.
### Fixed
- **`/design-review` motion checklist:** ease-out to enter and exit, ease-in-out to move, and flag plain ease-in. The new line is one byte longer than the old one, so /design-review stays inside its size cap.
- **`/design-consultation` DESIGN.md template:** `exit(ease-in)` becomes `exit(ease-out)`.
- **gstack's own `DESIGN.md`:** the motion easing matches, `exit(ease-out)`.
@@ -1,13 +0,0 @@
**`gstack-autoplan export --public` no longer carries the owner's own words (GST-1243).** GEVA's public bundle (GEVA-225, gstack 1.93.34.0) kept all 38 `owner_reply` fields in decisions.jsonl, and a sealed reviewer file (`ceo-native.a5.bound.md`) quoted an owner reply word for word. GEVA redacted both by hand. Garry's rule: nothing a person wrote leaves without their click.
### Fixed
- **Whose words.** Every owner reply the private run ever recorded counts: decision rows, `run.json`'s gate and the journal (`attempts.jsonl` keeps a reply a later answer at the same gate replaced), the words a carried gate reused, and a reason a human gave for `close --deferred --by human`. Replies that parsed as gate grammar are the decision code. An agent's own words (`answer_words`) and an agent's deferral reason stay.
- **Dropped fields.** The public copy drops `owner_reply` everywhere and a carry record's `words`, and replaces a human deferral's `reason` with `[owner words redacted]`. The decision itself stays: `chosen`, the gate-grammar `reply`, `owner_mode` and `owner_reply_parsed`.
- **gstack's renderings.** A human's deferral reason is replaced where gstack writes it into fixed text (voice `detail`, consensus head, gate title, a blocker's `why`, the run log), and carried words in the `CARRIED:` line. Each rendering is matched with its delimiters on both sides, so a one-word reason never rewrites a longer one that starts with it.
- **Redacted quotes.** Every exported file, sealed `*.bound.md` reviewer results included, is scanned. Before matching, the text is normalized: JSON string escapes and HTML entities decoded (an accented letter such as `&eacute;` reads as its letter), format characters dropped by Unicode category (`\p{Cf}`, variation selectors, the combining grapheme joiner), HTML tags read as spaces (in the rendered view the whole tag, attributes included) and hyphen-newline wraps joined. It is read both as rendered (comments, footnote markers, reference definitions and link targets and titles dropped, link text kept) and as written, so markup can neither split a quote nor hide one. Replies are indexed in both views too, so a link target an owner gave is found. Quotes are redacted on the original bytes and again after recorded roots are rewritten, so a path in a reply neither hides a quote nor makes the export refuse. Contractions split ("can't" and "cannot" read "can not", "it's" "it is", "I'd" "I would", and -n't, -'re, -'ve, -'ll, -'d). A word is a run of letters and digits, and any other character separates words. A reply of 3 or more words is a quote wherever it appears whole; from 8 words, any 8 consecutive words of it are a quote too. Replies of 1 or 2 words ("yes", "do it") are not scanned for. A quote extends over neighboring words of the same reply, and the marker replaces the original characters from its first word to its last.
- **Echoes stay.** Words the owner repeated from text recorded before his reply that he didn't write (the plan at an earlier gate, that gate's items, reviewer results bound before it) stay; only the new words around them are redacted. Every run of 3 or more words a reply adds between echoes is matched on its own (and by any 8 of its words from 8), so those words are redacted even where they are quoted without the echo.
- **Refusal.** A quote or rendering inside plan.md code (fenced, or inline with any number of backticks), which export never rewrites, stops the export with `ARTIFACT_OWNER_WORDS`, file:line and exit 3, and nothing is written. `gstack-artifact validate --public <dir> --private <run.json>` runs the same check. The error never prints the words. Each changed file gets an `OWNER_WORDS: <file> dropped=<n> redacted=<m>` line.
### Tests
- `test/autoplan-public-owner-words.test.ts`: a GEVA-shaped bundle (3 `owner_reply` rows, a carried gate, one reviewer quoting a reply on one line and one reflowing a reply across blockquote lines, a control file with no owner words) exports with every owner field dropped and both quotes redacted, while the control and the round-1 reviewer files stay byte-identical. Also: a quote in plan.md code refuses; validate catches a quote or rendering added later; a human's deferral reason leaves gate.json's `why` and every other rendering, an agent's stays; a one-word human reason leaves an agent's longer one alone; a reply replaced at the same gate is still redacted; a rendering never rewrites plan.md code and an exact one there refuses; echoed plan and finding text stays; a 4-word reply is redacted; a short quote split by a tag or a reference link is redacted and validates; the new words a reply added to an echoed plan line are redacted when quoted alone, and plan.md stays byte-identical.
- `test/autoplan-owner-words.test.ts`: the matching rule (1-2 word and grammar replies, whole phrases, the 8-word window, eleven kinds of glue, fifteen kinds of markup and invisible characters (tags, comments, reference links, footnotes, link titles, literal JSON escapes, zero-width and soft-hyphen entities, direction marks and isolates, the combining grapheme joiner, a variation selector), redaction offsets over markup and escapes, contractions, new words added to an echo, extension, echoes, kept code spans, bounded renderings) and a ratchet on every template that renders a deferral reason.
@@ -1,13 +0,0 @@
### Fixed
- **A required edit after review binds no longer forces a third review round (GST-1276, GST-1311).** Four instances in two days each cost a Captain grant (#3970, #3985, gstack-evals#203, gbrain#8523 and #8525). Now `gstack-ship-release finish` proves a post-review edit's kind from the diff and sends it to the one scoped reader that test-code edits already take, which isn't a review round (Garry, 2026-11-04). The kinds are: a docs-audit fix to `docs/*.md`, `AGENTS.md` or `CLAUDE.md` whose bytes are the ones the post-audit snapshot recorded; a `.gstack/pregate.json` change that only adds lanes (pregate's own `headPolicyDiff`, with every other top-level key unchanged); and a file matching the new `bookkeeping` ship-policy key. Each needs `--reviewed-base`, at most 20 changed lines, readable text and no gate-scan tag. `ROUTE:` prints `bounded=<n>`. The packet carries the targeted brief for every class, and the reader's clean record must carry the packet's `packet_sha256`. A candidate that fails a proof stays behavior and prints `ROUTE_BOUNDED_REFUSED: <path> <reason> (fix: …)`. Gate-path prose (`review/specialists/targeted.md`, skill templates) never qualifies.
- **A test literal changed after review takes the scoped reader, not behavior (GST-1276, #3984).** This covers a removed line swapped one for one with a line that differs only inside quoted strings and has no assertion or test call, such as a fixture value the secrets scan flagged. A changed expected value or a deleted assertion still stops.
- **`gstack-diff-scope` reads a `.gstack/`-only diff as config** instead of `SCOPE_ERROR=unmatched`, and `.gstack/always-run.json` is now a gate path, so its review depth stays full.
### Added
- **`bookkeeping` ship-policy key**: root-anchored globs of `.tsv`, `.csv` or `.txt` tables, never under `.gstack/` or `.github/`, read from the base only. It needs `min_gstack_version`. See docs/ship-policy.md#bookkeeping.
### Changed
- Finish's scoped-review STOP now says "one scoped reader (not a review round) reads", and its record template includes `packet_sha256`. See docs/review-policy.md#bounded-post-review-edits and docs/troubleshooting.md#route-bounded-refused.
-1
View File
@@ -1 +0,0 @@
- **/ship now enforces the reviewer fetch rule: a review stage that downloads another repository is thrown out and rerun once.** Before each review stage starts, /ship lists the git repositories and files of 100 MB or more under the workspace, home and the temp dir, and lists them again when the stage's last reader is done. A new repository with a network remote, or a new big file, quarantines every reader of that stage. Their replies move under `<out>/quarantine/`, their findings never reach the Red Team, the judge or PR text, and `gstack-review-dispatch verify` refuses with `FETCH_VIOLATION`. Each reader reruns once as `<reader>#2` (not a review round), and a second fetch stops the pass with `FETCH_RERUN_SPENT`; running it again starts no later stage. The rerun starts only after every path the first attempt found is gone. While one is still there, no reader of any stage starts, Step 11's voices included, and the pass stops with `FETCH_RERUN_BLOCKED`, so no reader can read what the first attempt downloaded. Nothing is deleted for you, and no delete command is printed. Each path it finds is listed for you to inspect and leave alone, and a remote URL is shown without its `user:token@` part or query (or as `<redacted>` when it doesn't parse as a URL). Capy doesn't tell a pass which reader did what, so blame lands on the whole stage, not one reader. The sweep only sees repositories that keep a network remote and files of 100 MB or more; an extracted archive, a smaller download, or a clone nested in another repository or deleted before the stage ends is not caught yet. (GST-1322)
@@ -1 +0,0 @@
- **/ship's reviewers no longer clone other repositories on their own, and a repository can declare what they must never reach.** On gbrain-evals #362 a reader cloned a full public repository (4.2 GB) that holds that project's sealed held-out eval conversations. Every reader brief now ends with a fetch rule that says to review only this checkout and its local git history, and never to clone, fetch or download another repository, dataset or archive. A repository can add a `readers` key to `.gstack/ship-policy.json` on its base branch: `forbid_fetch` hosts or repositories (web page reads included), `forbid_paths` globs that are cut out of every reader's diff, and a one-line `brief_note` every brief carries. A pull request can add `forbid_fetch` entries but never remove one, and `forbid_paths` and the note always come from the base branch. `gstack-review-dispatch audit-fetch` scans a reader's own tool calls against the rule and records the result in the pass manifest as a note; enforcement follows in a second PR. (GST-1322)
-3
View File
@@ -1,3 +0,0 @@
### Fixed
- **Lead judgment can lower a native adversarial finding to the Severity its reader stated beside `Blocks: no` (GST-1406).** When the native reader wrote its `Severity` and `Blocks: no` after prose (`… so the docs are wrong. Severity: Medium. Blocks: no.`), collect kept the heading's FIXABLE as CRITICAL. Judge refused any lowering, so the native record never resolved and /ship's Step 11.5 couldn't bind. Collect still can't tell those fields from another subject's written under the same heading, so it keeps the label. Now `gstack-review-dispatch judge` lets lead judgment lower such a finding to its stated Severity, never below, with a reason. It's recorded as `dismissed`. The ledger's blocker count already left such a finding out (GST-1031) and still does, and `verify` now also checks each merged row's reader against the output it came from, so a row relabelled as native fails replay. Other readers, a finding that states `Blocks: yes`, no `Blocks` or a Severity that isn't a severity (such as `Severity: FIXABLE`), and a lowering below the stated Severity or with no reason are still refused as downgrades. See docs/review-policy.md.
@@ -1,3 +0,0 @@
### Fixed
- **docs/review-policy.md now says `verify` checks each merged row's reader.** Since GST-1406, `gstack-review-dispatch verify` checks every merged row's reader, label and raw text against the output collect parsed. The policy text named only the label and raw text. This is the documentation debt #4017 shipped with.
-2
View File
@@ -1,2 +0,0 @@
- **Test impact: tracer and reuse keys (GST-167, part 2).** New `scripts/test-impact.ts` traces a free test file under strace and computes a key over everything it read, its import graph, `bun.lock`, tsconfig, pregate dependencies, the runner and key code, and the runtime. `explain` shows each input and why a file can or can't be reused. Network, real HOME, shared /tmp and `.git` reads, missing or week-old traces, always-run tests, import chains past 12 hops, and any change to the key code make a file ineligible. Nothing in CI changes yet. See docs/test-impact.md.
- **Pregate keeps main's always-run entries (GST-233).** The `lanes` selection now unions `.gstack/always-run.json` from the merge base with the branch's copy, so a branch can't drop a whole-tree test by deleting its glob.
-8
View File
@@ -1,8 +0,0 @@
**`gstack-ubi sync` and `pack` work on a subdirectory of a git checkout (GST-935).** Packing any folder that sat inside a checkout but was not its root died with "does not appear to be a git repository", after the VM was already up, because pack fetched the checkout's refs from the subdirectory and git only fetches from a repository's root. The gstack-evals launcher syncs `quick/laneb/runner`, so every launch from a git checkout failed at its first sync.
### Fixed
- **A subdirectory packs its own files.** Pack ships the files git lists under that folder (tracked, edited and untracked, not ignored) with no `.git`. A whole-repository `.git` would show the VM every file outside the folder as deleted. Packing a checkout's root and packing a non-git folder work as before.
- **A subdirectory git lists no files under is refused.** When the checkout ignores the folder, pack exits with "pack: git lists no files under <dir>" instead of uploading an empty folder, which a run could otherwise pass on.
### Tests
- `test/ubicloud-runner.test.ts`: packing a subdirectory of a checkout and of a linked worktree ships its tracked, edited and untracked files without a `.git` and leaves out ignored ones, and an ignored subdirectory is refused. This test fails on 1.92.58.0.
@@ -1,8 +0,0 @@
**/ship now gives full review to edits of the code that hides files from reviewers or picks the diff base, and real review to harness pin edits (GST-972 follow-up).** Both were slipping through with less review than they need.
### Fixed
- **Gate paths:** `lib/review-exclude.ts` decides which files reviewers never see, and `lib/default-base.ts` picks the base every diff is cut from. An edit to either was planned as plain code at light depth, with no gate class. Both are now gate paths, so an edit gets full depth, with the Red Team as its own reader plus Step 11's adversarial and outside voices. `review_exclude_paths` also can't hide them from reviewers anymore.
- **Harness pins:** files under `scripts/harness-pins/` repeat their harness file's path, so a pin like `scripts/harness-pins/test/helpers/e2e-gate.ts.json` matched the `test/` rule and planned as tests. Since the one-file-per-pin split (#3903), a harness helper edit with its pin got no reviewers at all, where the old shared `scripts/harness-version.json` made it code class. Pins outside `test/` were worse. No code names a pin file, so the unused-data rule planned them as docs. Pins now always classify as code, so the batched reader with the testing and security checklists and Step 11's adversarial pass run on them again. `review_exclude_paths` can't hide pins from reviewers anymore either; they're kept like other config files.
### Tests
- `test/review-depth.test.ts`: both modules plan gate class at full depth; harness pins classify as code and never as data, while review pin fixtures stay tests; a helper edit with its pin plans the code-class reviewers. A new no-loss check (replay GST-972) runs the GST-967 history grid on the changed paths and their stand-ins, which reproduce main's plans byte for byte: the branch only raises class and depth and drops no reviewer. `lib/review-depth.ts` is repinned under that replay.
-9
View File
@@ -1,9 +0,0 @@
**/ship loses less review credit to merges and docs commits, and an owner can accept an open finding after the second full pass (GST-1195, GST-1130).**
### Fixed
- **A granted fix-check keeps its credit across a base merge that edits the same file.** `gstack-ship-release finish` now credits a fix-checked path when its change against the base is the one the fix-check read, even if the base moved under it. Before, it compared file contents, so a merge that also touched the file printed `FIX_CHECK: reviewed none` and sent the path back through review (#3916). The docs audit carries such a path too. An edit made after the merge still stops.
- **A recorded grant gets its fix-check when round 2 was itself a fix-check** (GEVA-223). `gstack-review-plan --grant <id> --fix-checks 1` now plans the one granted fix-check after a round-2 fix-check that found or fixed a blocker, instead of `NEXT: stop`. A second granted fix-check is still refused.
- **A targeted reader's record carries across a plain docs commit made after the read** (TRUT-26, truther#1168). `gstack-ship-receipt write --review-targeted` refused the record on any product change, so the Step 14 and 14.5 docs commits cost another reader run. A docs-only change now carries the record, unless it adds a numeric claim, touches a results, prereg or eval path, or mentions money, security, secrets or auth. Code changes still need the reader again.
### Added
- **`review_loop_exception.second_pass`** (TRUT-7, truther#1169). An owner who accepts an open finding after the second full pass records `{ accepted_by, date, via, reason, second_pass: { head, deferred: [<finding id>] } }`. It needs `--review-loop` with at least two passes and one deferred finding, plus the printed `Review-loop exception: …` line in the body. A stop with no fix-check and fewer than two full passes is still refused.
@@ -1,10 +0,0 @@
**`/setup-gbrain` no longer stops on a working brain because gbrain's skill checks failed on another agent's skills folder (#3790).** gbrain doctor also grades whatever skills folder it finds on its own, such as `~/.openclaw/workspace`. When one of those skill checks failed, Step 5 read the whole report as `unhealthy` and stopped, and the agent then offered to edit the other agent's workspace to get past it. `gstack-gbrain-detect` reported `gbrain_doctor_ok: false` for the same reason.
### Fixed
- **Step 5 and `gbrain_doctor_ok` judge the brain from brain checks only** (`bin/gstack-gbrain-detect`, `setup-gbrain`). A new `gstack-gbrain-detect --doctor` runs `gbrain doctor --json --scope=brain`, ignores failed checks tagged `category: "skill"` from gbrain versions that still run them, and prints `brain_ok`, `brain_failures` and `skill_failures`. Step 5 proceeds on `brain_ok`, notes any skill failures, and tells the agent never to edit another agent's workspace to make setup pass. A failed connection, schema_version or pgvector check still stops setup.
- **A fully healthy brain passes Step 5.** gbrain reports `healthy`, `warnings` or `unhealthy`; Step 5 used to accept only `ok` or `warnings`, so a brain with no warnings at all stopped too.
- **The doctor gate fails closed with a cause.** No JSON, a crash, a timeout or a gbrain that can't start reports `brain_ok: false` and why, instead of crashing detect. A JSON-null `~/.gbrain/config.json` no longer breaks Step 1's detection. Doctor now runs through gbrain's spawn helper, so the Windows `gbrain.cmd` shim works, and a large report reaches Step 5 whole.
### Tests
- `test/setup-gbrain-doctor-gate.test.ts` runs the real Step 5 block and detect against a stub gbrain: skill-only failures with and without `--scope=brain`, brain failures, a fully healthy brain, no JSON, empty output, a large report through a pipe and a JSON-null config. 10 of its first 14 cases failed on main 6bffa20.
- `test/gbrain-spawn-windows-shell.test.ts` checks that detect spawns doctor through `spawnGbrain`.
@@ -1,6 +0,0 @@
**Crews now write a postmortem when a merged PR has to be reverted, and every READY and lane report says what the lane didn't check itself.** A revert records what was wrong with the code. It doesn't record why the lane and every reviewer passed it, so the same mistake comes back in another lane.
### Added
- **Postmortem template.** `captain/templates/postmortem.md` covers what broke, how it was found, impact, a timeline, and a root cause section that asks "why did the lane and every reviewer believe it was right?". That section is answered from the review outputs, receipts, decision trail and approvals, with one row per believer and a source for each. It ends with what caught the bug, what should have, and a table of rule changes with an owner, a status and an effectiveness check.
- **Postmortem step.** `/captain` and `docs/captain.md` tell the controller to start one after any revert or S1 fix of a merged crew PR, keep it at `reports/postmortem-<n>.md` and link it from the fixing PR. The rule change it names ships as a gstack PR.
- **Unverified line.** `captain/templates/READY.md` and `captain/templates/report.md` carry a required `Unverified:` line: every claim the lane didn't check itself (a local test that timed out, a skipped paid run, a number a model reported, a review finding set aside on an assumption nobody tested), or `none`. The READY contract in `/captain`, `docs/captain.md` and the Capy lane guide names it. A missing line is asked for in a follow-up message. It never holds a PR out of a train, and it doesn't make earlier READYs stale. No tool parses or enforces it.
@@ -1,2 +0,0 @@
### For contributors
- **A dispatched screen now builds its CI image from the commit it tests.** evals.yml's `build-image` job used to check out the dispatch ref (main) and key the image on main's `bun.lock`, while the slices checked out the PR's merge commit. A PR that changed the lockfile was then tested against main's installed browsers. Screen run 41662815506 for #3947 failed eight cases with `Executable doesn't exist … chromium_headless_shell-1248` because the image had only revision 1234. `build-image` now checks out `screen_merge_sha` (or `github.sha` for every other run) before it hashes and builds, without persisting the token. It also waits for the `route` job: a refused dispatch (a fork or closed PR, for one) and a superseded screen build no image.
-4
View File
@@ -1,4 +0,0 @@
bump: micro
---
### Fixed
- **`ship/DRIVER.md` fits its 12 KB limit again.** The restart line from #3886 pushed it to 12,329 bytes, past the 12,288-byte cap that `test/ship-driver.test.ts` enforces, so the free tests failed on main. The fix trims wording in five places and keeps every instruction. The file is now 12,282 bytes.
@@ -1,4 +0,0 @@
bump: micro
---
### Fixed
- **`/runaway-stopper` no longer reports a clean sweep when it was run from outside the crew.** The skill tells big crews to run its scan from a fresh automation thread. That thread belongs to no crew, so its crew list came back empty and the scan checked only itself, then printed a clean `SWEEP:`. The program now takes the Captain's code (`captain`) and writes the crew list to `crew.json`. `gstack-runaway scan --dir` treats an empty list as `RUNAWAY_INPUT` and exits 1.
-13
View File
@@ -1,13 +0,0 @@
bump: minor
---
**New skill: /runaway-stopper finds runaway agent threads and subagents, stops them at a clean point, and sets the defaults that keep them from coming back (GST-1279).** One Founderstar thread (FOUN-4) used 6.9B tokens in 13 days, about 4B of it its own context re-read on every step, many of them wakes that ended with nothing to say. One of its /ship subagents reached 485M because it was reused for five /ship runs, so each run re-read every earlier one.
### Added
- **`/runaway-stopper`.** Detect, stop, prevent. A Captain (or the org sweep) fetches its crew's thread data to files in one program and scans it. Own tokens past 500M or a thread active past 3 days means rotate, a subagent past 100M means stop, and empty wakes past 25% or review rounds past 2 per PR are warnings (org rule R70). Captains are reported, never rotated. Threads mid-/ship or mid-merge are deferred. Rotation uses a handoff file and a successor brief (`runaway-stopper/templates/`), and only the thread's own Captain archives it.
- **`gstack-runaway scan`.** The read-only detector behind the skill. It reads `tools.thread.get`, `tools.subagent.list` and transcript data and prints `RUNAWAY:`, `FLAG:`, `DEFER:` and `SWEEP:` lines. It never acts. A thread file it can't read prints `RUNAWAY_INPUT:`, is never counted as OK, and makes the scan exit 1.
### Changed
- **A /ship subagent can meter its own cost exactly, and then gets a 100M cap by default.** A subagent whose brief names its title (`GSTACK_SHIP_TASK_TITLE`) reads its own row of the lane's task list and starts `gstack-ship-budget start --reading-from task`. In observe mode with no `--token-cap`, that run caps at 100M and prints `BUDGET_DEFAULT_CAP: 100M` (single-run /ship subagents measured 64M median, 92M p90 on Nov 3). `--token-cap none` turns it off. Tree-metered subagents, root runs and admit mode are unchanged. The Capy lane guide now says to run each /ship in a fresh subagent titled `ship #<n> at <head7>`.
### Tests
- `test/runaway.test.ts`: FOUN-4's real reading (rotate on own tokens and age, the 485M /ship subagent flagged), live-subagent stop deferred mid-/ship, active-by-recent-turn, a Captain reported but not rotated, the empty-wake rate, the subagent tree, the directory sweep and usage errors, unreadable files, the LOOK verdict, and the skill's call bound and rotation lines. `test/ship-budget-meter-scope.test.ts`: the default cap, its stop, and the cases that must not get it.
@@ -1 +0,0 @@
- **A declared test runner only gets the files it can run.** With `runner: "bun test {files}"` in `.gstack/pregate.json`, pregate used to hand every selected test to bun, `*_test.py` files included. If only Python files were selected, bun exited 1 and the lane failed (PREGATE_LANE_FAILED). If a TS test was selected too, bun skipped the Python files without saying so, and they were counted as run. Now the local lane gives a declared runner only the files its tool runs, judged by extension: JS/TS for `bun test`, `node --test`, vitest, jest and similar, and `.py` for pytest. A custom dispatcher such as `node scripts/run-tests.js {files}` still gets every file. The lanes detail names each file it left out as not run and not counted as covered. When no runner is set, tests are still split by language as before. (GST-1331)
@@ -1 +0,0 @@
- **A /ship receipt now names the gstack release that wrote it.** `gstack_version` in the receipt used to record the version gstack reads policies as, which jumps to the newest policy key the checkout knows. On a 1.95.5.0 checkout that carries the unreleased 1.96.0.0 keys, every receipt said 1.96.0.0, a release that never ran. The receipt now records the checkout's VERSION file. Policy reading is unchanged, so a checkout that carries a new key still reads policies written for it. (GST-1366)
-5
View File
@@ -1,5 +0,0 @@
**`gstack-doctor` finds the launch probe again on macOS's built-in bash (#3601).** macOS ships bash 3.2 as `/bin/bash`. Bash 3.2 ends a `$( )` at the `)` of a `case` pattern that has no leading `(`. So `bin/gstack-launch-probe.sh` failed to parse there, and the doctor reported the probe as missing. Thanks to @bokeeeey, who reported it in #3601 and proposed the fix in #3602.
### Fixed
- **`bin/gstack-launch-probe.sh`** writes the pattern with its POSIX leading `(`, which every bash version accepts.
- **A new test scans every tracked shell script** for a `case` pattern without a leading `(` inside `$( )` or `<( )`, so Linux CI catches this before it reaches a Mac. The probe test also parses the file with `/bin/bash`, which catches it when the test runs on a Mac.
@@ -1,8 +0,0 @@
### Fixed
- **A docs or data PR no longer gets a money reader because a file name says "cost", and summing or printing a `cost_usd` field no longer counts as money code (GST-961, GST-962).** On gstack-evals#138, a results write-up planned as docs at light depth still dispatched a second, targeted money reader, because `wiki/.../cost-gstack-full.tsv` has "cost" in its name. On gstack-evals#139, an analysis module that only sums and prints per-trial `cost_usd` raised the money class. Now a data file that no code reads, under `docs/`, `wiki/`, `results/` or `reports/`, never counts as money for its name or for a reported cost. A `spend_cap` line or a paid-provider call in that file still counts. A `_usd` or `Usd` name is not money only when it is a reported cost (made of words like cost, total, per, trial or sum) on a line with no cap, limit, budget, price, charge, reservation or estimate word, no `<`, `>` or `<>` comparison and no number set on it. Reservations, estimates, amounts, refunds, spend guards and pricing tables all still count. Over the last 100 main commits, no commit's depth, class, risk classes or targeted classes changed.
### Tests
- `test/review-targeted.test.ts`: #138's file list plans no targeted reader, and the same file read by code still plans money. A `cost_usd` that is only summed or printed names no money. Positive controls: caps, budgets, limits, optional-chain and wrapped comparisons, `Math.min`, charges and refunds, reservations and estimates from `lib/eval-plan.ts` and `lib/autoplan-run.ts`, the `trial_reservation_usd` validator, a pricing table outside a results tree, a spend cap in an unread results file, and a secret in an unread data file.
- `test/review-risk-redos.test.ts`: `usdMoney` and its comparison patterns stay under 250 ms on 100,000-character lines.
-10
View File
@@ -1,10 +0,0 @@
**CI carry no longer refuses a skipped file that skips here too (GST-1314).** When CI skipped every test in a file, `gstack-evidence record-ci` reruns it here through the lane's `per_file` runner, and a file that skipped everything again ended the carry with `skipped_zero_run`, so the whole free suite reran. Free Tests run 41754278952 had six such files, all env-gated or tool-gated by design (0 pass, 20 skip on a Capy machine), so every gstack /ship carry would have fallen back to the full lane.
### Fixed
- **`gstack-evidence record-ci`:** runs the per_file remainder one file at a time. A file whose Bun summary registered tests and skipped every one here too is recorded as not owed (`skipped_here.not_owed` in the CI row, `not_owed_here=<n>` on the `LANE:` line) instead of refusing. A file that registers no test here still refuses with `skipped_zero_run`, even beside a file that ran, and so does a file whose log hit the 2 MB cap, since a cut log's last summary may not be the run's. A failing file's `skipped_failed` names it.
- **`per_file` paths** reach the runner anchored with `./`, because Bun reads a bare relative path as a substring filter (`test/x.test.ts` also matched `browse/test/x.test.ts`).
- **gstack's `bun run test` lane** declares `"per_file": "bun --no-env-file test --timeout=30000 {files}"`, so the runner is on main before the Free Tests writer starts listing skipped files (#3979). `--no-env-file` keeps a local `.env` from switching an env-gated file on.
- **Docs:** `docs/pregate.md#skipped-files` describes the per-file runs, the not-owed case and the anchoring.
### Tests
- `test/ship-ci-carry.test.ts`: run 41754278952's six remainder files, each printing its own skip-only Bun summary, carry with `not_owed_here=6` and reach the runner as `./<path>`; a remainder that mixes a file that ran with one that registers no test refuses with `skipped_zero_run` naming that file; a remainder file that registers no test refuses; a remainder file whose log passed 2 MB after a nested `Ran 5 tests` summary refuses instead of counting as not owed.
-12
View File
@@ -1,12 +0,0 @@
**`gstack-ship-queue land --pin` can test the merged tree when a PR's base moved after its CI (GST-1316).** Under `merge: direct`, land checked the PR's own green checks, but those ran on the PR before newer commits reached main. #3886 was green on its own, landed onto a newer main, and pushed `ship/DRIVER.md` to 12,329 bytes against its 12,288-byte cap, so Free Tests on main went red.
### Added
- **`land` policy key:** `land.merged_checks` lists the commands land runs when the base moved past the pin's merge base. Land builds a throwaway commit of the pin merged onto main's tip, runs each command in a worktree of it, and merges only when every one passes. It prints `MERGED_CHECKS: pass tree=… base=… pin=… checks: …`. A failure, a timeout or a check that rewrites a tracked file refuses with the new `QUEUE_LAND_MERGED_CHECKS`, a conflict refuses with `QUEUE_LAND_NOT_CLEAN`, and nothing merges. No lane has to merge main, rerun CI and re-ship each time another PR lands first. Without the key, a stale base lands as before with a `STALE_BASE: warn … no land.merged_checks configured` line. The commands are repo commands (the merged tree's `package.json` decides what they run), so land runs them only with `--allow-repo-commands` (else `REPO_COMMANDS_NOT_ALLOWED`), with `PATH` and the locale as the whole environment and a throwaway `HOME` (no sandbox: they can still write the checkout's `.git` and `node_modules`), runs git around them with hooks off, kills a command's whole process group when it ends or times out (so a passing check that leaves a child behind passes at once), and skips them under `--dry-run` with a `DRY-RUN: land.merged_checks not run` line. A check that moves HEAD fails, land re-reads the base policy after its locked fetch, and a merged tree whose lockfile or dependencies differ from the checkout's is refused with `QUEUE_NOT_READY` before any check runs, since the linked `node_modules` can't vouch for it. gstack's own policy adopts the key after the release that carries it.
### Fixed
- **`gstack-ship-queue land --pin`:** land now checks that GitHub reports the PR clean before it runs any merged check, then takes the land lock and re-fetches main before it checks and merges, refreshes the lock right before the merge, and releases it afterward. A rerun takes over or releases a lock an earlier run of the same PR left, and `land --pin --break-lock` breaks another lander's stale lock. A rerun after GitHub merged a stale base with no `land.merged_checks` checks the tree only, with a `STALE_BASE: warn` line, instead of reporting `QUEUE_LAND_UNVERIFIED`.
- **`gstack-ship-queue land --pin`:** after the merge, land checks that GitHub merged onto the base it checked or one the pin contains. If main moved in between, it reruns `land.merged_checks` on the landed tree and reports `QUEUE_LAND_UNVERIFIED` with no land note when they fail or the key is absent.
- **Docs:** `docs/ship-policy.md#land-direct` describes the merged-tree check and the `land` key. The release section now says a release may not delete a release output, and that a rerun after GitHub merged a release fetches the pin, then reads its parent, with an unreadable parent refused as `QUEUE_NOT_READY`. `docs/troubleshooting.md` and the `docs/captain.md` refusal table cover the new code.
### Tests
- `test/ship-queue-land-direct.test.ts`: on a stale base, a pin whose merged tree breaks a stand-in budget check is refused with `QUEUE_LAND_MERGED_CHECKS` and main stays put. A pin that passes lands with main's tree equal to the checked merged tree, and a rerun checks again. A conflicting pin is refused with `QUEUE_LAND_NOT_CLEAN`, and with no key a stale base lands with the warn line. The checks see the merged commit as HEAD, and a timed-out check (even one exiting 0 on SIGTERM) or a tracked-file rewrite fails. Without `--allow-repo-commands` the checks are refused and never run, under `--dry-run` and on a PR GitHub doesn't report clean they never run, and when they run they see no token and a throwaway `HOME` while land holds the land lock. A timed-out check's background child is killed with it, a passing check that leaves a child behind passes at once, and the checkout's post-checkout hook does not run when land builds the merged worktree. A check that ignores SIGTERM still stops at its timeout, a check that moves HEAD fails, a dependency change is refused before the checks, and a `land.merged_checks` that main gains while land runs is applied. A no-key rerun lands with the tree check. A lock left by an interrupted run is taken over, another PR's lock refuses (and `--break-lock` breaks only a stale one), a lock left after the merge is released by the rerun, and a rerun without `--allow-repo-commands` that must run the checks on the landed tree refuses. The key's validation is covered, and the mid-land race test still expects `QUEUE_LAND_UNVERIFIED` with no land note on the first run.
-17
View File
@@ -1,17 +0,0 @@
---
bump: minor
---
**A release gate can now gate main every 8 hours instead of on every push, and it feeds the stable release.** It is off until main's `.gstack/ship-policy.json` declares a `release_gate` block, so this release changes nothing on its own. With the block, `.github/workflows/release-gate.yml` runs `gstack-release-gate run` every 8 hours. When main moved since the last gate, it runs the full paid gate on main's head once, records the verdict as a `release` note in `refs/notes/gstack-trains`, and publishes the `main-backstop` check run on that commit. A red gate bisects its red cases within `bisect_spend_cap_usd` and names the first bad commit. The per-push main backstop stands down while the block is set, so main is never gated twice. `scripts/release-cut.ts` is still the only thing that moves `stable`, and its 72-hour floor and exceptions are unchanged. It now reads a green release record as proof the commit was gated, and it refuses every cut while the newest release gate is red with no green gate on a later commit. That hold is the freeze. See docs/ship-policy.md#release-gate.
### Added
- **`release_gate` policy key** (`lib/release-gate-policy.ts`, which needs `min_gstack_version` at this release): `cadence_hours` (8 to 168), `gate_workflow`, `branch`, `stable_ref`, `check_name`, `on_red: hold-release` and `bisect_spend_cap_usd`, every field required.
- **`bin/gstack-release-gate`** (`lib/release-gate.ts`): `run [--dry-run] [--sha] [--ignore-cadence]`, `status` and `bisect`. `--policy <file>` reads the block from a file, for a dry run or a proof before adoption. Exit 0 means gated, skipped, inert or a dry run; 1 means held, failed or a refused policy; 2 is a usage error.
- **`.github/workflows/release-gate.yml`**: an 8-hour schedule and a manual dispatch. The check run comes from a separate job that holds `checks: write` and runs no repository code.
- **Capy skill installer follows the update channel** (`capy/skill/gstack/scripts/install.sh`, wrapper revision 4). With `GSTACK_UPDATE_CHANNEL=stable` or a stable `update_channel`, a fresh clone or a floor upgrade goes to `stable` when it meets the support floor, and to main otherwise. `--revision` still wins.
### Changed
- **`evals-main-backstop.yml`** stands down (no gate, no freeze, no check run) when main's policy declares `release_gate`.
- **`evals.yml`** routes a release gate bisect (`release-gate/<id>` refs) only under a `release_gate` block at the base revision, and that bisect publishes its prefix records as `gstack-release-record`.
### Tests
- `test/release-gate.test.ts` drives run, bisect and status against a fake `gh` and a scratch remote. `test/release-gate-workflow.test.ts` runs the workflows' policy steps on scratch repositories and pins the cron to the cadence floor. `test/release-cut.test.ts` proves a red release record refuses the cut and a green one on a later commit releases it. `test/train-gate.test.ts` covers the bisect route, and `test/capy-skill-wrapper.test.ts` covers the stable channel and its fallback to main.
-4
View File
@@ -1,4 +0,0 @@
---
bump: minor
---
- **Versions at release under `merge: direct`.** `gstack-release-stamp open` stamps `origin/main` with the new `gstack-restamp --release`, which folds every `changelog.d` fragment under one new CHANGELOG heading, bumps VERSION and `package.json` by the highest fragment bump, reruns the release tool and moves tentative `*_KEY_VERSION` constants. It then pushes `release/<version>` and opens the release PR. `gstack-ship-queue land --pin` lands a `release/<version>` head without a /ship receipt, but only when it is exactly that stamp (`RELEASE_STAMP_INVALID` otherwise), and the Version Gate applies the same check. This lets the stable cut tag a new version again now that PRs land unstamped.
-1
View File
@@ -1 +0,0 @@
- **The review summary leaves an abandoned review pass out of the receipt's counts** (`gstack-review-scope summary`, GST-1168). A pass whose dispatch ledger was never judged, that no review-log row names, and that a later judged or logged pass replaced is left out, and the summary names it on a `REVIEW_SCOPE_VOIDED:` line instead of counting its findings as found and unrecorded. A fix-check ledger and the last pass always count.
-18
View File
@@ -1,18 +0,0 @@
**Five routine /ship and /autoplan stops no longer wait for a Captain: each is checked, recorded in the receipt's `no_stop` field, and the run keeps going (GSTA-258 L5, plan K1).** GBrain-Evals pre-granted these after GBEV-30 stopped about seven times in one day on them. A case that doesn't qualify, and every other stop, still goes to the owner.
### Added
- **`gstack-no-stop record --class <class> --reason <text> --evidence <ref>`** (`lib/no-stop.ts`) checks a stop and appends `{class, reason, evidence}` to the branch's ledger, or prints `NO_STOP: refused` and exits 3. The five classes:
- `scoped-fix-check`: one scoped fix-check past R51's two-round cap, when the round-2 fix is docs class only or only raises timeouts. The range must start at the commit the last review pass read. It records the standing grant in the approval record and prints `GRANT:` for `gstack-review-plan --grant`. One per branch.
- `test-timeout-fix`: a fix whose every changed line is in a test file and only raises a timeout value. A line with an assertion or a comparison never qualifies. Its gate finding takes a printed `kept` reason instead of `open`.
- `pregate-flake-rerun`: one rerun of a failed pregate case at the same tree (rule 16). One per branch.
- `stamp-only-carry`: a refused carry where HEAD differs from the receipt head merged into the base only in a version stamp. /ship then takes the carry's `RECHECK:` route instead of running Steps 2–16 again.
- `autoplan-regate`: see below.
- **`gstack-no-stop import` and `list`** add an autoplan run's records to the branch and print the ledger. /ship's receipt write passes it as `--no-stop @<file>`.
### Changed
- **An autoplan plan edit made after the owner's answer carries their words when it only adds ELI5 lines.** Every added line must sit in the plan's `## ELI5` section and name no number, spend, budget, cap, limit, arm, model, judge or verdict. Such an edit carries as `reason=no-stop autoplan-regate: …` and is written to `<run>/no-stop.json`, and the run still exports the reviewed plan. A removed or reworded line, a line added anywhere else, or an edit to the reviewed plan itself still goes to the owner. The run keeps a copy of each plan file it hashes, so the edit is diffed against what the owner answered.
- docs/review-policy.md#no-stop has the table. docs/ship-policy.md lists the receipt's `no_stop` field, and docs/unattended.md describes the carry-forward change.
### Tests
- `test/no-stop.test.ts`: each class's qualifying and refused cases, the ledger, the grant row, the receipt's `no_stop` validation, stamp-only after a base merge, import, and the template wording.
- `test/autoplan-carry.test.ts`: an ELI5-only addition after the revise now carries and is recorded. A cap move still refuses.
@@ -1,4 +0,0 @@
### Fixed
- **An outside review that says it could not verify the change is no longer read as a pass (#3925).** `lib/outside-review-result.ts` printed `VERDICT: clean` / `FINDINGS: none` for a Codex reply ending `Verdict: Can't verify (one changed file excluded and four required suites blocked)`. Now the reply's last `Verdict:` line has to approve the change (`ready`, `LGTM`, `approve`, `ship it`, `pass`, `clean`, `good to go`, or exactly `NO_FINDINGS`) or say `not ready`, which still blocks. Anything else, an approval with a question mark, or an approval that says something was excluded, blocked, skipped, not run, not executed or could not be verified, is `VERDICT: unverified` with the new `REASON: unverified_verdict` and exit 4. Which lines count as a verdict is unchanged. It is still a `Verdict:` line read with inline code blanked, so a verdict written only in code reads as unverified. See docs/troubleshooting.md#outside-review-unverified-verdict.
- **A numbered finding without a P0-P3 tag is no longer dropped (#3925).** A reply with `1. **Nonblocking — x**`, `Verdict: ready` and `END OF REPORT: 1 findings (0 blocking)` printed `FINDINGS: none`. In a reply with a `Verdict:` line that doesn't conclude it found nothing, every numbered item is now counted as a finding, and one with no readable severity makes `FINDINGS:` print `unspecified`, in the helper and in `gstack-outside-voice run`. The verdict is unchanged (`clean` here, since the reviewer approved). A consult answer with no `Verdict:` line, and a real clean reply, still read `clean` / `none`; a consult verdict that picks an option (`Verdict: go with B`) is `unverified`, since a consult isn't a pass/fail review.
-3
View File
@@ -1,3 +0,0 @@
### Fixed
- **The AskUserQuestion log hook no longer kills a slow first run on Windows, and its calls now share Claude Code's 5-second hook limit.** `question-log-hook` gave each `gstack-question-log` call a fixed 3-second timeout. On a cold Windows runner the first call takes just over 3 seconds (3,149 and 3,188 ms in windows-pregate runs 41605458462 and 41406459043), so the hook killed it and logged "exited null" even though Claude Code allows the hook 5 seconds. A fixed timeout per call also let several questions run well past those 5 seconds (six slow calls took 16 s). Now the calls share one budget: 5 seconds from when Bun starts the hook, less a 500 ms exit reserve. On Windows the hook finds Git Bash before it reads that budget, so the lookup comes out of the budget instead of adding to a call's time. When less than 250 ms is left, the hook skips the remaining questions and logs which ones; a killed call's log line names the signal and how long it ran. The budget doesn't cover the bash shim that starts Bun, and the first Git Bash lookup on a fresh install has no time limit of its own, so Claude Code can still stop the hook in those cases.
-3
View File
@@ -1,3 +0,0 @@
### Fixed
- **A tap or touch right after a page loads now lands where you aimed (GSTA-262).** `$B goto <url> && $B tap 240 440` on a phone-emulated tab could hit a different element. `$B goto` returns once the HTML is parsed, but before the page has drawn a frame with its mobile viewport, and Chromium places touch points using the last drawn frame. So the touch was scaled by 0.401 (393/980, the desktop default width) and landed at (96, 176), on the zoom area instead of the fire button. Every `tap`, `swipe`, `pinch` and `touch down` now waits for the page to draw a frame before the first touch (two animation frames; skipped for a hidden tab, and capped at one second, so a page that replaces `requestAnimationFrame` or navigates mid-wait can't stall the gesture). `$B touch cancel` also waits for a drawn frame before it returns, because Chromium delivers the touchcancel event with the next frame, so a check right after it could still see the old touch. This also fixes the flaky `touch input > held fingers are cancelled on navigation and by touch cancel` test (22 failures on 2026-11-03 and 12 more the next day, all in free shard 4). Under CPU load, a loop of navigate-then-touch missed 45 of 60 times before the fix and 0 of 60 after; the whole test file failed 1 of 20 runs before and 0 of 20 after. A touch-then-cancel loop under the same load saw the stale touch 2 of 60 times before the cancel wait and 0 of 300 after.
-6
View File
@@ -1,6 +0,0 @@
### Fixed
- **Two paid flaky cases now accept what their fixtures already allow, and a third prints the evidence it was missing.** These come from main backstop run 41817566885 (main b244e6d1). Each case failed once and passed its rerun.
- `plan-design-with-ui-scope`: in CI the image generator has no API key, so no mockup ever gets approved. The fixture's declared Voices menu still said "with the approved mockup as anchor". One run told the truth and wrote "(text-only this time, since mockups weren't available)" instead, and the picker refused it. The fixture now declares that one replacement. A free test replays the captured question; it fails on main and passes with the fix, and six variations that are not the declared replacement are still refused.
- `ship-docsync-current`: the docs interface says a private artifact can live "in a new directory you create there" under the fixture home, and `gstack-docs-candidate snapshot` does not create the folder for its `--out` path. The parent ran `mkdir -p <home>/ship2` as its own call, and the detector refused it. The detector now accepts a plain `mkdir` or `mkdir -p` of one new folder directly under the home, as its own call. A free test replays the whole captured run, which fails on main and passes now. The test also shows that 25 other `mkdir` forms are still refused: the repository, protected roots, nested paths, `..`, symlinks, globs, variables, options and joined lines.
- `autoplan-guard-pty`: the first guarded call got an unverified allow (`journal_lag`), because Claude Code didn't write its journal within the guard's 2-second window. The run had all four allows, so it never printed the journal, and cleanup deleted it. Now any run that falls short of four verified allows prints the journal and the time of each decision. The guard and its window are unchanged.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.95.5",
"version": "1.96.0",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",