# Changelog ## [1.91.180.0] - 2026-10-22 **The /cso docs say what the qualified catalogs now allow.** AGENTS.md, the README and `docs/cso-release-qualification.md` still said that no qualified runtime or scanner image existed, so /cso ran static assessment only. The scanner catalog has been qualified since #3042. The runtime catalog becomes qualified with the runtime promotion PR that ships alongside this one. ### Changed - **AGENTS.md**, **README.md** and **`docs/cso-release-qualification.md`**: the /cso entries name the ten qualified runtime images and the twelve scanner images, and say when /cso still falls back to static assessment: without Docker, or where no profile matches. ## [1.91.179.0] - 2026-10-22 **docsync-fault-interface fits its budgets under load again.** On 2026-10-18 it timed out under load on a Ubicloud full run. Its nested 36-case suite spent most of its time starting processes: each fault actor spawn loaded about 36 ms of imports (an empty bun takes 5.5 ms), inspect repeated Git reads, and the whole nested run sat under one 120 s outer test. With real Git on 4 cores, inner cases now take a 0.70 s median (max 1.25 s) unloaded, against 1.07 s before. Under 8 busy loops the max is 3.44 s, inside bun's default 5 s; before, cases reached 4.45 s. The worst outer test went from about 100 s of its 120 s cap to 33 s. No timeout, budget, retry, assertion or case changed. ### Fixed - **`test/helpers/docsync-fault-actor.ts`**: imports only light helper modules, hashes with `Bun.CryptoHasher` instead of loading `node:crypto`, and loads the redaction engine only on the snapshot-failure path. inspect makes 8 Git calls instead of 11, and the snapshot base check reuses the `--base` it just passed. Each actor spawn now costs 15–19 ms. - **`test/helpers/docsync-repo.ts`, `test/helpers/docsync-interface.ts`** (new): the fixture's Git and file helpers and `docsNativeInterface` moved out of the fixture and observer modules, so the actor stops loading the eval store and the QA observer. `docsync-fixture.ts` and `docsync-observer.ts` re-export them, so no importer changed. The fixture writes its Git identity with one append to `.git/config` instead of three `git config` calls. - **`bin/gstack-docs-candidate`**: snapshot lists `ls-files` once instead of twice and hashes with `Bun.CryptoHasher`. A snapshot spawn went from 68 ms to 46 ms, and the record it writes is byte-identical. - **`test/docsync-fault-interface.test.ts`**: the nested callback run is split into three outer tests by test-name pattern (10, 12 and 14 cases). Each part asserts its exact pass count, its exact filtered-out count and `0 fail`, so all 36 cases still run and the patterns are disjoint. ## [1.91.178.0] - 2026-10-22 **A /qa run no longer gets a hint that another report root could change its verdict.** Train gate 39646086155 failed qa-functional-webhook-fix once, and gate 39166935531 failed qa-functional-cli-fix the same way on 2026-10-18. Both times the agent ran `gstack-qa-evidence materialize`, read a fail verdict that ended "this verdict is final for this report root", and its very next call wrote a stray placeholder file in the fixture's parent directory (`qa-reports-placeholder-unused`, `qa-Q3vLqZ-placeholder`) before writing `report.md`. Each agent then reported the stray file itself. The out-of-fixture write detector was right both times and is unchanged. ### Fixed - **`lib/qa-evidence.ts`**: materialize's `next` instruction now ends "this verdict is final. Next, write the Markdown report beside evidence.json." It names the next write and where it goes, instead of qualifying the verdict by report root. - **`test/helpers/qa-functional-eval.ts`**: the functional fixture's declared interface says that every Write/Edit targets an owned path inside the working directory, and that a write anywhere else, including the parent directory, fails acceptance. That was already the acceptance rule; the prompt now states it. ### Gate integrity - No detector, threshold, trial count or case kind changed. `test/qa-evidence.test.ts` pins the new `next` wording and checks that it no longer mentions a report root. ## [1.91.177.0] - 2026-10-22 **The /cso runtime promotion workflow can finish.** Promote run 39691485727 failed after its `cso-runtime-release` approval. Its "Generate a fail-closed catalog candidate" step calls `gh api …/compare` to prove the staged commit is an ancestor of the qualification head, but the step had no `GH_TOKEN`. On a hosted runner `gh` then exits 4 ("set the GH_TOKEN environment variable"). The run stopped before it signed, pushed or opened anything. ### Fixed - **`.github/workflows/cso-runtime-promote.yml`**: the candidate step gets `GH_TOKEN: ${{ github.token }}`, like every other step there that calls `gh`. - **`test/workflow-gh-token.test.ts`** (new): fails when any step in any workflow invokes `gh` with no `GH_TOKEN` or `GITHUB_TOKEN` in its step, job or workflow env. actionlint does not check this. This step was the only one in the repository missing a token. ## [1.91.176.0] - 2026-10-21 **A qualified /cso runtime set can be released again after the S1 shebang change.** The release contract fingerprints every source byte compiled into a runtime image (`preparationSha256`). S1 (#3312) changed only line 1 of the two compiled helpers, `lib/cso/verifier.ts` and `lib/cso/preparation-container.ts`, from `#!/usr/bin/env bun` to the isolated shebang. That line is embedded in the compiled binary as an unused string and never runs in the image, but it moved the fingerprint. Qualify run 39535948607 therefore refused the run-5 statements, staged at 133cb36, with `STALE_RUNTIME_QUALIFICATION: preparationSha256`. Garry chose this narrow rule change over a re-stage and a fresh paid qualification (2026-10-21, option 2). ### Changed - **`scripts/cso-requalification.ts`**: `preparationSha256` hashes a compiled helper entrypoint's leading `#!` line as absent. Every other byte of the helpers, and every byte of every other image source including its shebang lines, still counts. 133cb36 and main both measure `c721b585…`. - **`scripts/cso-runtime-promotion.ts`** (`releaseCandidate`): when the staging record's `preparationSha256` differs from the checkout's, the release contract re-measures it at the staged commit from git history. The record must match that commit under the current rule or the earlier one, which also hashed the shebangs; otherwise it fails with `REQUALIFICATION_TRIGGERS_MISMATCH`. The current-rule value is compared and recorded, so both sides use the same rule. - **`cso-runtime-qualification.yml`, `cso-runtime-promote.yml`**: check out full history so the staged commit is readable. ### Gate integrity - Gate edit, a narrowing with Garry's approval: the release contract's `preparationSha256` comparison ignores the leading `#!` line of the two compiled helpers on both sides. Nothing else gets a pass. `test/cso-runtime-promotion.test.ts` runs the contract on the real 133cb36 record (`b069f7ef…`) against a main tree. These still fail with `STALE_RUNTIME_QUALIFICATION`: a line-2 edit to either helper, an image Dockerfile edit, and a shebang edit to the image `entrypoint`. A record that matches neither rule at the staged commit fails with `REQUALIFICATION_TRIGGERS_MISMATCH`. ## [1.91.175.0] - 2026-10-21 **A free-suite flake now keeps the assertion that failed.** Four free-test flakes in the flaky ledger (plan-seed-submission, native-paste, qa-only preparation-late and the gstack-upgrade fresh-shell positive control) could not be diagnosed: each failed once on a Ubicloud or remote-pregate run, passed on the serial retry, and its log died with the machine that pulled it. The runner's console printed only the failing test's name, and the flake ledger recorded only the file. ### Changed - **`scripts/test-free-shards.ts`**: the result reporter keeps the error bun printed before each `(fail)` line: from its first `error:` line, without blanks or CI annotations, at most 8 lines of 240 characters. The FAIL epilogue prints that excerpt under each `✗ — ` line, so it reaches the console stream that remote pregates relay. A FLAKY-PASS ledger entry now records `tests: [{ name, excerpt }]` for each file. Existing ledger readers ignore the new field. Verdicts, retries and their cap are unchanged. ### Tests - `test/test-free-shards.test.ts` replays the Windows team-init PowerShell failure as bun printed it (run 39448730953, attempt 1) and checks the excerpt and the epilogue. The real CLI flaky-retry routes (`retry-reader`, `retry-exclusive`) now check that the ledger entry names the failed test with `Expected: 2` / `Received: 1`. ## [1.91.174.0] - 2026-10-21 **Two paid eval flakes from the 2026-10-20 trains are repaired at their cause, with no threshold, trial count or case kind changed.** ship-local-hook-preservation failed in train gate 39440559183 even though the agent told the user "you'll need to set it up by hand": the manual-integration detector knew "set up" and "wire in" only with the particle right after the verb. ship-docsync-current lost a focused trial to an unquoted `grep … --include=*`, which the shared read-only grammar refuses on purpose because the shell expands the glob first; the grammar's declared interface said "unquoted glob matches included" and never said that an option's glob has to be quoted. ### Fixed - **`test/helpers/ship-hook-actor.ts`**: the manual-integration sentence now accepts a pronoun object inside the phrasal verb (`set it up`, `set both up`, `wire them in`). The rest of the rule is unchanged: the sentence still needs "yourself", or "by hand" plus an addressee. - **`test/helpers/read-only-commands.ts`**: the declared interface now says that an unquoted glob may stand only as a path operand, and shows the quoted option form (`--include='*.md'`, `-name '*.md'`). A refused globbed option names the quoted form in its reason (`globbed option: --include=*; quote the glob: --include='*'`). The grammar accepts and refuses exactly the same commands as before. ### Gate integrity - Gate edit: the ship-local-hook-preservation detector (above). `test/ship-hook-actor.test.ts` replays all 8 Opus 5.5 train and backstop gate replies for the case (`test/fixtures/ship-local-hook-train-replies.json`, runs 39363705692–39515353585). Without the fix, the 39440559183 reply fails. New negative controls keep these refused: a first-person "I set it up by hand", a passive "It was set up by hand", "Whoever … needs to work out how to combine them" (no by-hand step), and "I wired them in myself". - No grammar acceptance change. `test/read-only-command-grammar.test.ts` replays the captured `--include=*` command: it is still refused, its reason names the quoted form, the docsync interface carries the new sentence, and the quoted form and both examples are accepted. ## [1.91.173.0] - 2026-10-20 **Merge trains can merge slot 2 onward.** After slot 1 merges, main carries the new stamp, so every later pin conflicts with main on the stamp files and GitHub can't squash it. A lane makes its PR mergeable by merging main into the pin before it stamps. `verify --pr --train ` refused that head as moved off its pin. Now it also accepts a head whose tree equals the slot's train prefix (T) modulo stamps: exactly the tree the train's gate covered, and what main becomes once the PR squash-merges. The first live train stopped at slot 2 on this (gate.train-ac4baf961759-43d6ed28f2bf-9a1dbb). Plan GSTA-49. ### Fixed - **`lib/ship-queue-train-verify.ts`** accepts a slot head that is either its pin plus stamps (as before) or T modulo stamps, and the `head:` line says which. A head that matches neither is still `QUEUE_TRAIN_MOVED`, and the queue still freezes when earlier slots have merged. - **`docs/ship-policy.md`** gives the per-slot lane procedure from slot 2 on: merge `origin/main` into the pin, resolve the CHANGELOG conflict with `mergeChangelogs`, then run `gstack-restamp --next`. ### Gate integrity - `test/ship-queue-train.test.ts` builds a lane head with main merged into the pin and T2's tree stamped 1.2.5.0. It verifies, with `MERGE_CONDITION` at that head. The same head plus one file outside the train fails with `QUEUE_TRAIN_MOVED`. Without the fix, the positive case fails. ## [1.91.172.0] - 2026-10-20 **gstack now works the way Capy Captains run work: every decision goes to the Captain (or the person) in plain words, plan approvals arrive in a fixed before/after shape, each thread keeps a record of who approved what, and /ship's PR body shows before/after images of what changed.** Garry asked for this on 2026-10-20 ("make it much more obvious a/ how approvals work per thread, b/ for each plan approval i want a before/after, why it matters, what caused us to work on this, and big decisions you had to make, c/ when asking for merge guidance i want the PRs to show screenshots of what changed and why"). Everything below applies only when `GSTACK_CAPY=1` (the Capy launch line sets it). Other hosts keep their output, apart from one new `CAPY: off` STATUS line. ### Added - **`capy-asks` runtime block (`bin/gstack-skill-start`).** Every workflow started in Capy prints `CAPY: on` and one instruction block. A crew thread asks its Captain with a needs-you report, and a thread with no Captain asks the person with a Capy question; never as a bare question or gate codes. Each ask states what it is, why it matters, what each answer does and costs, the pick, and the one-word reply. Plan approvals use the Captain brief. Every answer is recorded. Standing approvals the Captain lists are cited instead of asked again. A fixed list always needs a fresh ask: product or behaviour decisions, spend over a stated cap, publishing, threshold, budget or trial-count changes, closing someone's contribution, and anything destructive. `GSTACK_CAPY_ASKS=0` drops the block for one session. No generated SKILL.md grows. - **Captain brief (`gstack-owner-brief render --captain --out `, `lib/captain-brief.ts`).** It prints the plan ask in this order: a header with "Reply **rec**", then Before → after, Why it matters, What caused this work, Big decisions (read from the reviewed plan's sections, each capped with a pointer to the plan), and What we need from you, which lists every choice `rec` takes, in words. Gate ids, `gate_rev`, `rec_hash` and the exact `answer` command appear only on the last line, which is for the relaying agent. A plan missing a section renders "Not ready for approval" with no reply invitation and exits 1 (`CAPTAIN_BRIEF_INCOMPLETE`; `--allow-incomplete` previews). Under Capy, `gstack-autoplan` prints a `RELAY: gstack-owner-brief render --captain …` line next to every `GATE:` line and after `recommend`. - **Per-thread approval record (`gstack-approvals`, `lib/approval-record.ts`).** `add --ask --words --by --scope [--pending] [--outcome approved|rejected|pending|standing]`, `import --autoplan --by ` (one row per answer event, typed or relayed, idempotent), `render` (the `## Approvals` table), `list`, `sync`, `path`. Rows live at `/projects//approvals/.jsonl` (a readable prefix plus a hash of the exact ref). Under Capy they are mirrored to the private project-user Drive folder (or `GSTACK_APPROVALS_DIR`), because the machine's state root is thrown away. Every field passes the secret scanner before anything is written (`APPROVAL_RECORD_SECRET` names the field and rule, never the text). The table escapes mentions, links, images, HTML and citation-shaped text, so the ship receipt never reads it as an approval citation. A fresh Capy machine with no record and no mirror says "Record unavailable", not "No approvals recorded" (`APPROVAL_RECORD_UNAVAILABLE`). - **Before/after images (`gstack-pr-shots`, `lib/pr-shots.ts`).** Takes a tracked file (optionally one heading's section, via the same fence-aware parser as the brief, `lib/markdown-sections.ts`) at the merge base and in the working tree, or two captures inside the repository. It writes a stacked Before/After page at GitHub's 880 px column, with changed hunks only, a −/+ gutter as well as colour, and "⋯ N unchanged lines" gaps, so a change deep in a file stays visible. It also writes `text.md` (a collapsed unified diff for the PR body) and renders `pair.png` through `lib/aside-render.ts`. It reads text only and runs nothing. Inputs are size-capped, ANSI-stripped, refused when binary or outside the repository and state root, and secret-scanned before rendering; refs are resolved without option parsing. With no browser it prints `SHOT_PNG: unavailable (…)` and exits 0, so the caller can screenshot `shot.html` itself. - **/ship Capy section (`ship/sections/capy-pr.md`).** When Step 19 prints `CAPY_PR: on`, the PR body opens (after the receipt) with `## What changed`: one to three captioned before/after images uploaded with Capy's PR asset tool, public repositories only, each with its text version under it. Then come `## Why` and `## Approvals` (`gstack-approvals render`), and the READY report attaches the key image. Outside Capy the section is never read. - **Capy skill: "Working under a Captain" plus `capy/skill/gstack/captain.md`.** The skill now explains in one section how approvals flow, what standing approvals look like, what always needs a fresh ask, the record, the plan-ask shape and the merge-ask shape. `captain.md` has the worked commands. `autoplan/DRIVER.md`'s "On Capy" rules name the four plan sections, the brief and the import. ### Changed - **Owners refresh their Drive skill copy by hand.** The skill folder gained guidance ("Working under a Captain", `captain.md`); the bootstrapper is unchanged, so the wrapper stays at 2 and no stale-copy notice fires. Copy the folder over the Drive skill to pick it up: `cp -R ~/.local/share/gstack/capy/skill/gstack/. /skills/gstack/`. - **Gate integrity (a gate edit): context-budget ceiling for `capy/skill/gstack` 2923 → 3498 tokens (`test/fixtures/context-budget.json`, that entry only)** for the new section; no other ceiling moved. - **Gate integrity (a gate edit): ship's parity union cap `maxSizeRatio` 1.6488 → 1.6690 (`test/helpers/carve-guards.ts`).** The only growth is the on-demand `capy-pr` section plus a three-line `CAPY_PR` check and one manifest row, measured at 313,265 / 187,706. A non-Capy ship never loads the section. The skeleton stays under 81,000 bytes. No eval threshold, trial count, case kind or budget changed. ### Tests - `test/approval-record.test.ts`, `test/captain-brief.test.ts`, `test/pr-shots.test.ts`: branch keys and containment, secret refusal with nothing written and nothing echoed, citation-proof escaping checked against `parseCitations`, idempotent import of typed and relayed answers, mirror restore and the unavailable state, brief order, gate ids only on the agent line, `rec_hash`, incomplete and answered states, change-centred hunks (a change at line 240 of 300), HTML escaping and CSP, section extraction at a base commit, option-like refs, outside files and the no-browser path. `test/gstack-skill-start.test.ts` pins `CAPY` in the STATUS contract and the block's on/off/quiet behaviour. Ship goldens are regenerated. ## [1.91.171.0] - 2026-10-20 **A restarted `/ios-qa` daemon resumes the device session instead of relaunching the app.** The app's boot token works once, so a new daemon process used to have no way back in except `devicectl process launch --terminate-existing`, which wiped the app's QA state. The daemon now writes the rotated StateServer bearer to `$GSTACK_STATE_ROOT/ios-qa-session.json` (mode 0600, through a fresh temp file) after every successful bootstrap, bound to the device UDID, bundle ID and StateServer port, and a new daemon tries it once on its first device request. The bearer goes only to the tunnel address `devicectl` reports for the bound UDID, after `/healthz` names the same bundle; there is no mDNS or remembered-address fallback. The daemon bootstraps normally, without sending the bearer, when the target UDID, app or port differs, another device or app answers, `devicectl` reports no address, or the file is not private to you (the file is removed unused). A bearer the app rejects is cleared before the bootstrap and never presented again, and the boot token is never written to disk. `/ios-qa` Phase 0 now describes this file instead of a cache no code wrote. Draft until verified on a real iPhone. - Thanks @Bmathews721 (#1796) for the diagnosis, the hardware repro and the reuse scenarios the new tests carry. ## [1.91.170.0] - 2026-10-20 **/autoplan no longer stops a healthy run with a false "event order" or "init" denial when Claude Code is slow to write its session journal.** Daily flaky-fix pass, group 5 (other paid cases): the same pass clarifies how the office-hours reviewer recovers from a seal error and fixes two plan-review floor harness gaps in reading cropped permission menus. ### Fixed - **/autoplan publication guard (`autoplan/bin/phase-publication-hook.ts`).** If you ran /autoplan on Claude Code 2.1.292, the first Phase 1 step could be blocked with "The native parent event order is unavailable or inconsistent" (code `event_order`) or "needs its own successful init" (code `init_own`), even though the init had already run. Claude Code can run the guard's hook before it writes that turn's earlier steps to the session journal, so the guard judged a journal that did not yet show them. The guard now keeps reading inside its existing 2-second window while the journal shows no step yet, or nothing since your latest message. If Claude Code still has not written it, a journal with no steps lets the call proceed with the existing "unverified" warning (`journal_lag`). A journal that ends at your message gets the normal check, so a run that really skipped init is still denied. Seen on PR gate 39333263804 (`autoplan-guard-pty`, attempt 1). - **Office-hours spec reviewer (`lib/office-hours-review.ts`).** The reviewer prompt's "Do not use Edit" sat in the sentence about not changing the design, so a reviewer could patch its own verdict file with Edit after a seal error. The prompt now forbids Edit on every file, the verdict file included, and says to Write the complete corrected JSON to the same path and run the seal command again. ### Tests - **Plan-review floor harness (`test/helpers/plan-count-file-permission.ts`).** A cropped owned plan-file Edit menu whose top row is the cwd-relative file row (a bare basename for a file in the working directory) now binds to its pending Edit. Before, only an absolute or `./` row was accepted, so `plan-ceo-finding-floor` sat on the menu until its 600 s timeout (PR gate 39293281037, attempt 1). The row must name the menu's own file and still resolve to the owned path. - **Same harness, quote guard.** The guard that keeps a quoted AskUserQuestion from passing as a permission menu matched any row that began with whitespace and `>`, including a soft-wrapped preview row that continued a table cell with `>activation->paid)`. It now matches a quote or fence only within four columns. Wrapped continuations start at five columns or more, and every preview row is still bound to the owned file's bytes. This stalled focused trial t3 on its owned Create menu (`permission_unanswered` at 92 s). - `test/autoplan-publication-payload.test.ts` replays the captured journal shape (slash turn and meta skill body, no public event). Flushed inside the window, it gets the normal verdict; never flushed, it is `journal_lag`. Negative controls: a late flush that shows an unpublished phase is still `publication_missing`, an `/autoplan` turn whose init never appears is still `init_own`, and the evaluation denies empty, foreign-session and out-of-order events as `event_order`. Without the fix, four of these fail with `event_order` and `init_own`. - `test/plan-edit-cropped-permission.test.ts` adds the captured gate viewport as fixture case `ci-39293281037-a1` (Edit input reconstructed from its diff rows, as for t07 and t08). It is granted once from its own PreToolUse input. Negative controls: a foreign directory, a foreign basename, a numbered diff row, a changed diff row and an unwitnessed Edit are refused. - `test/plan-create-prepublication.test.ts` adds trial t3's viewport as `test/fixtures/plan-ceo-floor-create-quote-wrap.json`. It is granted once from its own PreToolUse Write input. Negative controls: a quoted row, a fence and a checkbox above the menu, and a changed continuation row, are refused. - `test/office-hours-review.test.ts` pins the reviewer's seal-error recovery. `test/office-hours-completion.test.ts` checks that the validator accepts a seal error recovered by a complete re-Write and rejects the same recovery done with Edit. - Gate integrity: no threshold, trial count, case kind, quarantine or budget changed. ### Gate integrity ## [1.91.169.0] - 2026-10-20 **The docs-sync fixture now accepts joined Git reads, `for` loops over literal file names and a pipe into `tr` as reads, and tells the actor that the Claude config dir is off-limits. The late-result fixture now says what its one repair corrects.** In the last 20 Opus gates, `ship-docsync-current` went red 3 times. Two of those were pure reads the validator rejected: `cat personal-note.txt; echo ---; git diff main HEAD` (run 39347827595) and `for f in …; do echo "=== $f"; cat $f; done` (run 39344306426). The third (run 39338691964) was a real deviation: a stray `for_none=1;` assignment, plus the parent reading its child's session transcript under the fixture's CLAUDE_CONFIG_DIR. Both still fail. Focused trials then hit `git ls-files -z … | tr '\0' '\n'` twice, a form Fable also used in run 39186278010. `ship-docsync-late-result` went red once (run 39367310001): the parent stopped the hung child and judged that no concrete correction was available, because the prompt never said what the declared repair corrects. Focused Opus 5.5 trials: on the final tree, `ship-docsync-current` 4/5 and `ship-docsync-completion` 5/5. The current red was an unquoted `--include=*` that the shared grammar rejects as a globbed option, which is outside this change, and it passed its rule-16 rerun. Earlier trees: `ship-docsync-current` 5/5 (before `tr`), `ship-docsync-late-result` 5/5 (first repair), and one trial each of the other nine docs-sync cases, all passing. ### Changed - **Gate integrity (a gate edit): joined Git reads, reversing #3288's choice.** #3288 (v1.91.143.0) kept Git reads out of grammar lines and told the actor to run each one alone, after Fable joined them in run 39186278010. Opus kept joining them anyway (run 39347827595), and a joined pure read is no less read-only than the same reads run one by one. So `test/helpers/docsync-observer.ts` now accepts a declared Git read joined with `;`, `&&` or `||` to the shared grammar's reads or to another Git read. The Git read is still checked literally against the declared list and stands in the line as a neutral read for the grammar. It is refused after a `cd` and as a pipe sink, and Git global options, Git writes, lifecycle, candidate, Preamble and script commands still never join. Controls that pinned `git rev-parse HEAD; git status`, `git rev-parse HEAD && git status`, `git rev-parse HEAD | cat`, `git check-ignore -v .qa-state; ls`, `git ls-tree -r HEAD | head`, `git status; ls`, `git log | head -5` and `git status; git branch --show-current; git rev-parse HEAD` as rejected now pin a joined Git write, bypass or shell sink instead (`test/docsync-command-grammar.test.ts`, `test/read-only-command-grammar.test.ts`, `test/fixtures/docsync-observer-commands.json`). The test "docsync keeps Git reads out of grammar lines" is now "docsync accepts joined Git reads". - **Gate integrity (a gate edit): loops.** A `for` loop anywhere in a line counts as its unrolled passes, but only with a lowercase variable, words that have no quotes, globs, braces, tilde or `$`, no other variable in the body and no nested loop. The completed-read check credits a `cat` of the document inside such a loop. - **Shared read-only grammar: `tr` (docs-sync and qa-only).** `test/helpers/read-only-commands.ts` accepts `tr` only after a pipe, with `-c`, `-C`, `-d`, `-s` or `-t` and one or two literal sets. It rejects `tr` as a first command, a third operand, a set holding `$`, a backtick, a brace or an unquoted glob, a redirect, and `tr … | sh`. The grammar's interface text says so, and qa-only's deadline fixture has its own accepted and rejected `tr` controls. - **Interface text.** The declared docs-sync interface now says joined Git reads and loops are accepted. It also names `actor-state.json` and the Claude config dir (except `settings.json`) as unreadable, because the dir holds credentials and every agent's session transcript, and it says a child is judged by the output it returns. - **Late-result fixture prompt.** The repairable launcher's `repair` now says it corrects the spawned-marker setup and the child transport, the faults behind a missing marker or a child that returns nothing by the Collect deadline. `test/docsync-fault-interface.test.ts` checks that only repairable prompts say so. ### Fixed - **Free regressions.** The captured composed reads of runs 39347827595 and 39344306426, and of the focused trials, run in a fixture and leave the repository unchanged. Negative controls keep the run 39338691964 forms rejected, along with joined Git writes, `-c`/`-C` options, pipe sinks, `cd`, redirects, loop redirects, globbed, quoted, braced or tilde loop words, uppercase loop variables, stray variables, nested loops, and `tr` used outside a pipe or with a non-literal set. ## [1.91.168.0] - 2026-10-20 **Four QA paid cases stop failing on valid or unclear agent behavior: `qa-only-no-fix` accepts reads joined with its exact bookkeeping calls, and the QA-callers fixture says where the handoff check goes and stops counting the explicit plan check as a wasted rerun.** Daily flaky-fix pass (GSTA-57), from the captured events of Opus gate runs 39313931713, 39332134395, 39336730429, 39320737890, 39181697029 and 39351737002. ### Changed - **Gate integrity, widening (`test/helpers/qa-browser-deadline-evidence.ts`):** `qa-only-no-fix` failed three times with `QA deadline: unsupported outer shell composition`. Each time the agent joined grammar reads with one of the fixture's exact bookkeeping calls outside the guard, for example `ls -la; git -C branch --show-current`. The deadline check now accepts a `;`, `&&` or `||` join in which every command is an exact listed bookkeeping call or a shared-grammar read checked from the fixture root, and the fixture prompt declares that join. Still rejected, each with a negative control: a joined guard call, a browser command before or after the join, a git call outside the exact list (`-c` options, other subcommands), `cd` inside a join, a pipe into `sh`, `&`, substitutions, newlines, escaped `;`, a dangling `&&`, a read outside the root, the readiness block with an appended command, and a `QA_DEADLINE ` receipt in the output. The existing "unbatched metadata" control now joins the metadata call with an unguarded browser command, since two exact calls joined are accepted. A free replay runs all 100 non-guard Bash commands from 11 captured attempts (3 failed, 8 passed) inside a real guarded attempt; the three failed attempts fail on the previous helper with the CI message. No threshold, kind, trial or quarantine change. The replay fixture `test/fixtures/qa-only-bookkeeping-joins-public.json` is registered in the `qa-only-no-fix` touchfiles. - **Gate integrity, widening (`validateCallerEvidence`):** `ship-exploratory-plan-checks` failed with `duplicate unchanged passing probe` after the agent ran `bun scripts/probe.ts 9` as a guarded smoke probe and then as PLAN.md's release check with `--timeout-ms 10000`, the form the prompt reserves for an explicit plan check outside the smoke budget. One explicit plan check beside the smoke probe is no longer a duplicate. A guarded repeat of the same input, a second plan check, and ordinary input under the plan-check form still are. A free replay runs the captured capture sequence through the real helpers in a real fixture. - **QA-callers prompt (`qaCallerSessionOptions`):** the handoff freshness check is one rule. Read HANDOFF.md and reports/HANDOFF.md and wait for the results in an earlier message than each of three things: `materialize` (it runs once per report root and freezes the verdict), every `completed:true` review record including `adversarial-review`, and the completion report. `review-exploratory-small-cli` failed twice when the agent wrote the adversarial-review record in the same response as its first HANDOFF Read (39320737890) or before any Read (39332134395); the detector was right, and a free replay of both captured event sets keeps them failing and passes them reordered. `ship-exploratory-late-input` failed twice when the agent materialized before reading the late reports/HANDOFF.md, so a refused second `materialize` left a stale `pass` verdict beside an honest `inconclusive` receipt. - **Focused paid runs (Opus 5.5, gate tier, this tree):** `qa-only-no-fix`, `review-exploratory-small-cli`, `ship-exploratory-plan-checks` and `ship-exploratory-late-input` each passed 5 of 5 ($14.98 total). One `qa-only-no-fix` trial used two bookkeeping joins that the previous helper would have failed. ## [1.91.167.0] - 2026-10-20 **Two paid detectors now accept the rewordings Opus 5.5 actually uses, with their negative controls kept (daily flaky-fix pass, GSTA-57).** `plan-design-with-ui-scope` went red three times on the ledger with "no acknowledged Design focus" while the agent had asked Step 0D and a numbered finding; `tpa-absent-darwin` went red once on a valid download pitch. ### Fixed - **`plan-design-with-ui-scope` focus detector (test/skill-e2e-plan-design-with-ui.test.ts).** The Step 0A rating now counts as any sentence that scores design completeness out of 10 (run 39363705692 wrote "I rated this plan 4/10 for design completeness"; the 0A example says "This plan is a 3/10 on design completeness"), and the all-seven option may name the seven any way ("All 7 areas (recommended)"). The title, focus and option-order requirements are unchanged. Replays of all three ledger failures (runs 39315041557, 39344306426, 39363705692, attempt 1) and their passing reruns now pass the detector; 39363705692's captured calls are a fixture (`test/fixtures/design-ui-focus-capture-39363705692.json`). New negative controls: a score and "design completeness" in different sentences, a score out of 7, a non-design completeness score, an all-seven option without a focus title, a non-leading or issue-labelled all-seven option, a bare "All 7" option, and the run without its focus question all still fail. - **`tpa-absent-darwin` pitch detector (test/helpers/third-party-actions.ts).** The canonical phrase may name Aside as "it" once the macOS 15+ floor follows ("If you want to use Aside: download it (macOS 15+) at aside.com", run 39315041557, now a capture in `test/fixtures/third-party-actions-pitch-captures.json`). It keeps the canonical phrase's guards: clause start, no negation before it, no quoted or reported mention. The short forms without the floor still count only at a sentence start. - **Gate integrity:** test-only detector edits. No threshold, trial count, case kind, quarantine or budget changed. ## [1.91.166.0] - 2026-10-20 **Two load-sensitive free tests from the flaky ledger now measure what they test.** Daily flaky-fix pass, 2026-10-20, free and load-sensitive group. ### Fixed - **`test/setup-ps1.test.ts` PowerShell 5.1 cases (windows-setup, 2026-10-17 and 2026-10-20).** The workflow runs the file with plain `bun test`, so each case has Bun's 5 s default. The first `powershell.exe` launch on a runner pays a cold start that later launches do not. Over 94 windows-setup jobs, the first legacy launch (the space-in-TMPDIR case) took p50 1.46 s, p95 3.42 s and max 5.04 s, while the second launch (the empty-argument case) had p95 1.41 s. Both reds were that first launch killed at 5 s with exit null. A `beforeAll` now launches the legacy interpreter once on the stub checkout, with a 60 s hook budget matching the spawns' own bound. No per-test timeout changed. On Linux, a pwsh wrapper that adds 6 s to the first launch reproduces the CI symptom on main and passes with the warm-up. One that adds 6 s to every launch still fails three cases (negative control). - **`browse/test/xvfb.test.ts` spawn → cleanup round trip (Ubicloud full run, 2026-10-18).** It picked a display from `:99-:110` and then spawned on it, inside the default range that headed daemons in parallel shards allocate from. Six default-range allocators reproduced "X display :N was reserved by another X server during startup" in 9 of 20 and 5 of 20 runs. The case now uses `:25000-:25100`, a range no other allocator uses, like the file's other private-range cases. It now fails instead of silently returning when no display is free. 30 of 30 runs pass under the same load. ## [1.91.165.0] - 2026-10-20 **The Version Gate accepts an unstamped READY head under merge trains.** Under the train policy a lane's head keeps VERSION, package.json and the agents digest at main's version and writes its CHANGELOG entry under `[Unreleased]`. The gate still demanded a bump, so every unstamped PR showed "VERSION not bumped", and `gstack-ship-queue ready` refused it with `QUEUE_NOT_READY` (#3320, #3321). That blocked every train. Plan GSTA-49, S1 for the queue. ### Fixed - **`scripts/compare-pr-version.ts`** passes an unstamped head when the base's `.gstack/ship-policy.json` declares `train`. A head counts as unstamped (`lib/version-gate-train.ts`) when, against its merge base with main, it changes neither `VERSION`, the `package.json` version nor any release output, and its CHANGELOG change is confined to one `[Unreleased]` section at the top. An older merge base is fine, because the train merges the head onto current main and rule 14 forbids lane main merges. - **A stamped head under a train policy must carry exactly base + bump**, the version `gstack-restamp --next` allocates when the slot is called. Without a train block the gate is unchanged. - **`.github/workflows/version-gate.yml`** runs the gate's code from the PR's merge commit (`github.sha`) rather than its head. A PR that edits the gate runs its own edit, and an open PR picks up a gate fix from main at its next `pull_request` event. It also fetches the base with full history so the merge base resolves. ### Gate integrity - The unstamped pass is new and applies only under a train policy. Negative controls in `test/compare-pr-version.test.ts` cover: - a stamped head at any version other than base + bump; - a changed `package.json` version or release output, which is treated as a stamp; - a CHANGELOG edit to a released section; - a second `[Unreleased]` section, or one that isn't at the top; - an unstamped head without a train block, which still fails hard rule 1. ## [1.91.164.0] - 2026-10-19 **Merge trains are on for gstack: `.gstack/ship-policy.json` now carries the `train` block.** READY PRs merge in trains of up to 5 (or once the oldest READY is 3 hours old) behind one full gate on the train head, instead of one full gate per PR. Plan GSTA-49, go-live after lanes T1 and T1b. Garry approved going live once one dry-run train passed. ### Changed - **`.gstack/ship-policy.json` declares `train`** (`gate_workflow` `evals.yml`, `free_workflow` `free-tests.yml`, `full_gate_check` `gstack/full-gate`, `record_artifact` `gstack-train-record`, `max_size` 5, `fill_hours` 3, `screen_selection` `gstack-paid`, `trial_reservation_usd` 1.5) and `min_gstack_version` 1.91.146.0, which `gstack-ship-policy validate` requires alongside `train`. From this merge on, a PR push routes to `pr-summary` instead of a paid PR gate, `ready` records the screen choice, `train build` dispatches the full gate, `verify --pr --train ` is the merge check, and the main backstop gates any tree no green train covered. - **Dry-run evidence.** Train `gate.train-24672a1f5b96-b67778cdbbd3-58805a` merged two unstamped scratch docs PRs (#3313 and #3314, closed unmerged) onto a scratch base: main 85b0f531f plus this policy block plus one scratch-only `evals.yml` line that read the policy from that base. A `workflow_dispatch` reads it from the default branch, which had no `train` block yet. The real `train build` refused with `QUEUE_TRAIN_WAITING` until the 3-hour fill passed, then ran Free Tests green (run 39377354548) and dispatched one full gate (run 39377781282: `VERDICT GREEN`, rule 108/108, judge 24/24, behavior 0/0, `gstack/full-gate` success on the train head). `verify --pr 3313` printed `MERGE_CONDITION: sha=33fb98512123…`. Not exercised: the default-branch policy read itself (the scratch line replaced it), PR-push routing to `pr-summary`, a stamp at merge, a screen, a bisect and the main backstop. - **`docs/ship-policy.md`** now says gstack's own policy carries the block, replacing "does not carry it yet". - **`test/gstack-ship-policy.test.ts`** validates gstack's own `.gstack/ship-policy.json` and checks that its train workflows exist, so the policy file has a test lane. ## [1.91.163.0] - 2026-10-19 **A gate red on one test now reruns that test once instead of the whole gate, and a pass is green with a flaky note.** Garry, 2026-10-19 (CREW rule 16): "you can just run the failed test again and note that it is flaky... You don't have to do the whole test pass again for one failed test." Plan GSTA-49, lane F1; the #3308 reuse miss is GSTA-53. ### Added - **In-run flaky rerun (`rerun-plan` and `eval-rerun` in `evals.yml`, `scripts/lib/paid-rerun.ts`).** After the slices, a red rule, behavior or judge case verdict reruns once at the same head in the same run attempt: a rule or judge case on its own shard (only the rerun cases count), a behavior case as its whole 3-trial panel, a judge case with its 3 samples. It runs fresh (no receipts in or out). If it passes, the gate is GREEN with a `FLAKY N` headline line and rerun spend, a `⚑` line per case in the report and PR comment, and one record per case in the run's `flake-records-a` artifact for the daily flaky-fix pass. If it fails again, it is a real red (`FAILED TWICE`). - **Manual `gh run rerun --failed` attempts follow the same rule.** An attempt that passes every earlier red is GREEN with the same flaky annotation. A red that first appears in a later attempt stays red unless its own next run passes, so a later attempt never hides a new red. - **Flaky GREENs are reusable (GSTA-53).** `gate-reuse` binds a flaky GREEN with its `flaky_cases`, reads each run's newest attempt binding, and R2a and R2 reuse it with `reused_flaky` named in the log and PR comment. On #3308 (run 39315041557), both attempts bound RED because the report kept attempt 1's verdict, so the next push paid for a full gate. A run still red after its rerun binds RED and is never reused. ### Changed - **Gate integrity (a gate edit):** harness, INFRA, INCOMPLETE and unattributed reds never rerun and are never flaky. Neither is a case already red twice, or a case whose rerun could not turn the gate green. No threshold, trial count, case kind, judge model, `EVAL_POLICY` constant or `HARNESS_VERSION` changed (recorded non-behavioral). Pass-rate history still records each run's first execution unchanged; a rerun's trials stay in its `paid-rerun-*` artifacts and the flake record. On the gate lane, a rule shard that ran to completion with a red case no longer fails its slice job. The report still fails closed on it, so the run's conclusion follows the report and a flaky GREEN run concludes `success`. Execution failures (timeouts, runner errors, hollow or unfinished shards) still fail the slice. Train gates take the same rerun path. A bisect reruns a VERDICT FAIL once on its prefix (`scripts/train-gate.ts`), and a pass there is a flaky PASS naming both runs. The PR comment's verified-summary check also bounds `flakyCollectorFailures`, so a failed collector record that is not a flaky case still fails the comment. Negative control for the slice-exit change: a case red on its first run and red again on its rerun keeps the real report CLI at exit 1 and `VERDICT RED`; `slices-report` still runs after every rerun path and its last step fails the job on reconcile exit 1, no job carries `continue-on-error`, so the run concludes `failure`; the binding is RED and `gate-reuse` refuses it; and the train record is RED, which T1 classifies RED whatever the run concluded (`test/paid-rerun.test.ts`, `test/evals-train-workflow.test.ts`). - **Free regressions:** `test/paid-rerun.test.ts` replays a trimmed capture of run 39293281037 through the real `--report` and `--rerun-plan` CLI. Its negative controls cover red twice, harness, INCOMPLETE and checkpoint reds, a new red on a later attempt, a first-attempt pass, rerun results with a missing or mismatched manifest, and reruns of unplanned cases. `test/gate-reuse.test.ts` covers R2a and R2 reuse of a flaky GREEN, a manual-attempt binding, and a run still red after its rerun. `test/train-gate.test.ts` covers the train record and the bisect rerun. Workflow wiring tests are in `test/evals-train-workflow.test.ts` and `test/ci-paid-coordination.test.ts`. - **Rerun artifact layout:** the first gate of this change (run 39363705692) reran its one red case and passed, but `download-artifact` unpacked the lone `rerun-plan-a1` match into the report root, over the plan's own `manifest.json`, so the report read every case as unplanned and stayed red. The plan artifact now holds `rerun-plan-a/manifest.json`, rerun results download under `reruns/`, the rerun logs artifact is `rerun-logs-*` so it no longer matches `paid-rerun-*`, and the report finds the rerun manifest in either layout `download-artifact` produces. That run's artifacts, replayed in the new layout, report `VERDICT GREEN` with `FLAKY 1`. ## [1.91.162.0] - 2026-10-19 **The evals workflow carries merge trains: a train's own gate publishes its record and the `gstack/full-gate` check, bisects run one executor per prefix, `ready --screen` dispatches a real screen, and a main backstop gates any tree no green train covered.** All of it stays off unless the base revision's `.gstack/ship-policy.json` declares a `train` block. Without one, a PR push and every eval-bg dispatch run today's jobs and steps with today's values. The one addition is a free `route` job that reads the policy with git and jq. gstack's own policy still has no `train` block; turning it on is a separate one-line policy PR. Plan GSTA-49, lane T1b. ### Added - **Train gate runs** (`train/` with a train nonce): the full census runs without verdict reuse or reuse binding. The report job writes the train record (`scripts/train-gate.ts train-record`) from this run's plan manifest, one panel per planned case, plus the prefix trees modulo stamps. It uploads the record as the policy's `record_artifact`. A separate `train-check` job, with `checks: write` and no repository code, publishes `full_gate_check` as success only for a complete GREEN census whose nonce and head match the dispatch. A train nonce dispatched on main is refused before any paid work. - **Bisect inputs `cases` and `prefix_shas`.** Case ids must be in the run's plan, and each prefix must be an ancestor of the train head on `train/`, or the run refuses. A `train-bisect` matrix runs one executor per prefix, checks out and verifies that prefix, and uploads `-bisect-`, the name `train bisect` reads. - **Dispatched screens** (`screen_pr`, `screen_merge_sha`, `screen_policy`): the run checks out and verifies the pinned merge commit. It waits on the PR head's Free Tests, cancels an older screen of the same PR, and comments on that PR. Verdict reuse (R2a/R2) authenticates a screen by PR number, head SHA, repository and tested base (the merge commit's first parent). It keeps every selection and newer-run veto, and lists pull_request and screen runs filtered by PR. A broad screen never adopts a narrow run. - **`.github/workflows/evals-main-backstop.yml`** runs on every push to main, one at a time. It does nothing without a `train` block. With one, it waits until no train is merging. It runs the full gate on main only when no green train record lists main's tree modulo stamps, and writes the freeze record `gstack-ship-queue` reads. A five-PR train fires zero backstop gates. - **The free screening summary:** under a `train` block, a PR push runs readiness and prints the screen selection and an estimated cost instead of starting paid slices. ### Changed - **Gate integrity (train block only):** under a `train` block, PR pushes no longer start paid slices; trains, screens and the backstop gate instead. Train and bisect runs never reuse a verdict and never bind one for reuse. No threshold, trial count, case kind or selection changed. - **Gate integrity (always, GSTA-48 P3-1):** when the readiness job blocks a PR head, a new `readiness-comment` job replaces an existing `## E2E Evals` comment with "NOT RUN on ", so an earlier PASS no longer stands beside a head that never ran. It posts nothing when no such comment exists. A slice killed before its upload step counts as incomplete. - **Gate integrity, widening (`plan-design-with-ui-scope` focus detector, GStack Minder-approved):** the Step 0D focus question also counts when its title names the focus (the title ends at the first line break or before `Project/branch/task:`), its body carries the step's own rating sentence ("I('ve) rated this plan N/10 on design completeness"), and it offers at least two options, one of which starts "All 7" or "All seven" (design or review) dimensions or passes. Opus 5.5 titled the question only "Review focus" in both attempts of run 39298269902, so the detector missed it. In the second attempt the finding detector also counted the focus question as the review finding, because its one-line title names hierarchy and panels. A recognized focus question can no longer count as a finding. Free replays of both captured attempts (`test/fixtures/design-ui-focus-capture-39298269902-a1.json` and `-a2.json`, registered in the case's touchfiles) pass. Every existing negative control still fails, as do these new ones: an issue-labeled finding that carries the rating, a single-option focus question, a focus question without a leading all-7 option, and a 4/7 rating. No threshold, trial count, case kind or ceiling changed. ## [1.91.161.0] - 2026-10-19 **`gstack-pregate` works on Django, Dart and Flutter repos, and on branches that only touch assets or tool config.** Before, a Django migration, an `admin.py`, a template, a font, a vendored licence or `.pre-commit-config.yaml` stopped `/ship` with `PREGATE_NO_LANE` in a repo that had never opted in, and every touched `_test.dart` file did the same. `gstack-pregate init` now drafts a policy from the repo instead of writing a fixed example. Contributed by @arlophoenix (#3285) and @szsunyuan (#3290); reported by @dgrant (#3266, #3267, #3268, #3269). ### Fixed - **Django files loaded by name select tests (#3266).** In a Django project (a manage.py or code importing django, plus Python code that assigns `INSTALLED_APPS` or `ROOT_URLCONF`, or a config file naming `DJANGO_SETTINGS_MODULE`), every Python test depends on the settings module; a dotted string in settings and an `include("app.urls")` argument count as imports; an app's migrations, `admin.py`, `templates/`, `static/`, `templatetags/`, `management/`, `fixtures/` and `locale/` files select that app's tests and the tests that reach its `models.py`; a project-level template selects the suite. - **Files no test runner loads take an empty lane (#3267).** Images, fonts, media and PDFs (outside snapshot, golden and fixture trees) and allowlisted tool config (`.pre-commit-config.yaml`, `.editorconfig`, lint and format config, `.vscode/`, `.idea/`, `.impeccable/`, GitHub issue templates and CODEOWNERS) print `no-tests (assets and tool config)`, and licence text under any vendored name (`roboto-LICENSE.txt`, `LICENSE-MIT`) is documentation, when no test names the file. Stylesheets, unknown dotfiles (`.rspec`, `.husky/` hooks), CI workflows, `.gstack/`, `.env*` and version pins still need a lane, and so does unmapped code. - **Dart and Flutter lanes (#3285, @arlophoenix).** The import graph reads `import`/`export`/`part` directives and resolves `package:` URIs through every tracked `pubspec.yaml`. `lanes` runs each package's tests from its own root with `flutter test` or `dart test`, through fvm when the package pins it. flutter, dart, melos and very_good CI jobs are lanes, and a run step's test paths resolve against its `working-directory` and literal `cd`. A missing Dart SDK or a `.dart` file outside any package is `incomplete`, never a pass; device tests are not unit lanes. - **`--base ` gets a branch suggestion, not an impossible fetch (#3269, #3290, @szsunyuan).** ### Changed - **`gstack-pregate init` drafts from the repo (#3268).** It prints the languages it found and the runner each language's tests get, maps each file the import graph cannot place to the tests of the nearest directory above it, and lists the rest as `TODO` (as it does past the 256-entry schema limit). It never writes `[]`, which would clear a file without a test. - **Review fixes on the Dart port.** A `cd` inside a pipeline, an alternative, a background job, shell control flow, a command substitution or a redirected subshell now leaves a CI lane's test set unknown (it may run any test) instead of resolving later paths against the wrong directory, which could drop a platform obligation. Dart directives are read to their `;` whatever the line indentation, metadata before a directive and semicolons inside a URI are handled, and nested block comments are skipped, so an unindented conditional import or a long `show` list no longer loses its edges. - **Gate integrity:** new free regressions in `test/pregate-non-js.test.ts` (Django, assets and licence, init, and the two review fixes, each with a negative control) and `test/pregate-dart.test.ts`. Each new positive case fails on the tree before this change. No existing test, threshold, trial count or case kind changed; `test/helpers/pregate-fixture.ts` only reports more about a failed git spawn. `test/pregate-dart.test.ts` and `test/pregate-non-js.test.ts` join the other pregate fixture files in `scripts/lib/windows-curation.ts`'s known-incompatible list: the Dart stand-ins are bash scripts on a colon-joined PATH, and the bin cases expect the Linux-host lanes verdict. ## [1.91.160.0] - 2026-10-19 **Dart and Flutter projects get the right reviewers and the right test command.** A Dart change matched no diff-scope category except its test file, so a Flutter widget PR ran only the testing pass and skipped design, performance and security. Contributed by @arlophoenix (#3284). ### Fixed - **`gstack-diff-scope` counts `.dart` as both frontend and backend.** The extension cannot tell a Flutter widget from a repository class or a pure-Dart server, and a skipped reviewer is invisible, so both specialists run. `pubspec.lock` (at the root or in a workspace package) is config. `lib/frontend-scope.ts` mirrors the frontend half, and `gstack-design-detect` still never hands a `.dart` file to the impeccable engine, which reads web sources only. - **Test detection knows Dart.** `/ship`'s bootstrap and coverage audit, `/design-review` and `/plan-eng-review` print `RUNTIME:dart` for a `pubspec.yaml` and `FRAMEWORK:flutter` for an indented `sdk: flutter` dependency (a commented-out one or an `environment.sdk` constraint does not count). They treat `dart_test.yaml` and `*_test.dart` files anywhere as existing tests and offer `flutter test` or `dart test`. Review-army reports `STACK: dart` and `TEST_FW: dart-test` or `flutter-test`. - **Adversarial `DIFF_SIZE` sums `git diff --numstat`,** the same count review-army's `DIFF_LINES` uses, instead of parsing the `--stat` summary line. Binary files count as zero. ### Changed - The test-bootstrap prose is shorter so the generated skills stay inside the parity size cap and `/design-review` stays at its SKILL.md size pin. The rules it states are unchanged. `/design-review`, `/plan-eng-review` and `/review` shrink, so their pins in `test/fixtures/skill-md-size-allowlist.json` go down (31 entries). - `test/frontend-scope.test.ts`'s bash parity test gets a 30 s budget. It spawns git six times and the scope script once per sample, and it already ran past Bun's 5 s default on a 4-core machine before this change. ## [1.91.159.0] - 2026-10-19 **Security: the repository gstack runs in can no longer configure gstack's own programs.** Bun loads `.env`, `.env.local` and `.env.` from the directory it starts in. It also runs any `preload` that a `bunfig.toml` there names, before the program's first line. gstack runs inside the repository it audits, so a committed or branch `.env` could set `GSTACK_STATE_ROOT`, `GSTACK_PROJECT_SLUG`, `GSTACK_CLAUDE_BIN` (an attacker-chosen binary then ran as "claude"), `GSTACK_SPEND_CAP_USD` or a provider base URL for every gstack bin, hook and skill step. A `bunfig.toml` preload ran arbitrary code in all of them. Every Bun that gstack starts now passes `--no-env-file` and an empty `--config`. Your shell environment works exactly as before. Found in review by GSTA-54. ### Security - **Every bun entry point starts isolated:** all 123 bun shebangs are now `#!/usr/bin/env -S bun --no-env-file --config=/dev/null`. `bun run ` ignores the shebang, so the callers carry the flags too: skill templates, generated skill docs, resolvers, hook wrappers, `setup` and the bash bins pass `bun --no-env-file --config=/dev/null` for every `run`, `-e`, `-p` and script path. - **TypeScript spawns use `bunIsolationArgs()`** from `lib/bun-isolation.ts`, which follows browse's `emptyBunConfig()`. This covers `scriptInvocation` (how TypeScript launches gstack's own bins), restamp, qa-deadline, the timeline hook's repair child, gbrain sync's ingest child, the agents-digest regen and the Claude Code migration. Native Windows has no `/dev/null`, so there the helper passes a zero-byte bunfig in a fixed directory under the per-user temp root. It is never removed, because the timeline hook's detached repair child reads it after the hook exits. Under Git Bash, MSYS rewrites the shebang's `/dev/null` to `nul`, which Bun 1.4.2 reads as an empty config. A windows-latest run verified both. - **Compiled builds:** `build:gates` and `scripts/build-app.sh` now pass `--no-compile-autoload-dotenv --no-compile-autoload-bunfig`, as `scripts/build.sh` already did. Without them, compiled binaries load both files. - **A ratchet and regressions:** - `test/bun-entry-isolation.test.ts` fails on any bun shebang, shell or skill caller, TypeScript spawn or `build --compile` that drops the flags. - `test/bun-dotenv-isolation.test.ts` runs the real bins in a fixture repo with a hostile `.env`, `.env.local` and `bunfig.toml`. The state root, slug, claude binary and pregate spend cap still come from the shell. - `test/bun-isolation-launch.test.ts` runs the shebang, bash and TypeScript launch forms on every free lane, Windows included. ### Changed - **`gstack-evidence run` no longer scrubs dotenv values from the child environment,** and `GSTACK_EVIDENCE_KEEP_DOTENV` is gone. Bun no longer loads those values into the wrapper, so the child sees your shell's environment, as it would if you ran the command by hand. The old value-equality scrub (#2652, @namtrok) also dropped shell-exported variables whose value matched the file. - **Raised SKILL.md size pins:** 80 pins in `test/fixtures/skill-md-size-allowlist.json` grew by 33 to 330 characters each, from the isolation flags alone (8,253 characters in total). - **Directly executed `./scripts/*.ts` files no longer read gstack's own `.env`.** `bun run