diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a08871d7..2dd5ca389 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
# Changelog
+## [1.91.279.0] - 2026-10-27
+
+**A licence line or a changelog entry no longer buys a full paid gate, the review loop stops re-reporting findings the owners have already ruled below the floor, and three follow-ups from the Blast radius ship land.** #3499: every PR that touched `NOTICE.md`, `LICENSE`, `licenses/` or `ETHOS.md` restored every gate case and judge because no paid case was mapped to them (#3493's docs-only PR planned 82 cases, about $30, for a notice and a changelog entry). #3504 and #3485: the same classes of finding (a schema lagging its writer, a helper nobody calls, a re-dispatch, a refusal one step late) reached the owners again on every ship after being judged below the floor once. GSTA-121's ship left three gaps. The Captain's merge-ask template and `docs/skills.md` lacked the `## Blast radius` item the PR body now carries, an 11-digit GitHub Actions run id after a run word was redacted from an approval record as a phone number, and the remote free gate failed with `PREGATE_REMOTE_INFRA` when a reviewer ran git in the checkout while the gate tarred `.git`.
+
+### Added
+- **`RELEASE_METADATA_FILES` in `scripts/test-pr-profile.ts`** (`NOTICE.md`, `LICENSE`, `licenses/**`): release metadata no paid case reads, exempt like `CHANGELOG.md` and `VERSION` already were in `knownNonBehaviorFile`; a PR of only these files selects no paid case and keeps mode `pr`; `captain/templates/**` (the crew templates no paid case reads, found as the one unknown dependency of this PR's own selection) joins `FREE_ONLY_PR_FILES`. `ETHOS.md` is registered under the cases and judge of the two prompts that read it (`/office-hours`, `/plan-ceo-review`) in `test/helpers/touchfiles-data.ts`, so an ETHOS edit selects those cases (three gate cases and one judge today) instead of the whole gate; `SECURITY.md` replaces it as the tests' example of an unregistered file. The "unknown dependency restores the full gate" rule itself is unchanged.
+- **`docs/review-policy.md#rulings-below-the-floor`**: six finding classes put to the floor by #3485 and #3504 and ruled below it (own-artifact hardening, unwired or unread, bounded repeat work, conservative refusal, artifact presentation, test gap), each with what it looks like and why it fails the floor; a finding in one is a note named by its ruling from pass 1 on. The review army's lead judgment (`scripts/resolvers/review-army.ts`, rendered into both `review-army.md` sections) gains the one-line **On record** ground that applies the table; its other grounds were tightened by the same byte count so the generated review SKILL.md stays at its pins. The floor ("would fail a test, break production, or lose data or secrets") is unchanged.
+- **`docs/TESTING_INTERNALS.md` Windows free lane**: the command that shows whether one test is in the curated Windows subset and why not, and which list (`KNOWN_WINDOWS_SAFE`, `WINDOWS_PROBE_SAFE`, `KNOWN_WINDOWS_INCOMPATIBLE`) moves it.
+- **`docs/skills.md` `/ship` "The PR body"**: the four Capy sections in order (What changed, Why, Blast radius, Approvals), how to write the Blast radius item, and what `gstack-prose-lint` gates. `captain/templates/merge-ask.md` carries `## Blast radius` between Why and Approvals; `bin/gstack-pr-shots`'s example caption and the `capy-pr` section heading name it too.
+
+### Fixed
+- **`lib/redact-patterns.ts` `isCiRunIdentifier`**: an 11-digit GitHub Actions run id right after a run word on the same line (`screen 40477638211 GREEN`, `gate=…`, `rerun passed …`, `run #…`) is not `pii.phone.e164`, in every scanner (`gstack-approvals add`, `gstack-decision-trail`, the pregate secrets check, `gstack-post`); the run-word regex moved there from `lib/gstack-post.ts`, which imports it for its chained-id allowlist. A 10-digit national number after a run word, a bare 11-digit number, an E.164 `+` number and `job`, `id`, `workflow` or `attempt` before one still report; a national number written without its `+` right after a run word reads as a run id, the accepted residual of the ruling that a run id is not a phone number (`gstack-post` has read it that way since GSTA-121).
+- **`scripts/ubicloud/ubi-runner.sh pack`**: the live `.git` is never tarred. The refs are fetched into a fresh repository through git's transport (`--update-shallow`, so a shallow CI or Capy checkout packs), the `remote.*` and `branch.*` config is replayed so `origin/` and each branch's upstream resolve on the VM, a detached HEAD gets a `detached-` branch, the index is copied with the blobs staged against HEAD (a VM `gstack-wtree` can read a staged file), and a pack that fails removes its temporary repository; a review subagent's `git diff` or a trial merge's `worktree add` during the first minute of a remote gate no longer makes tar exit 1 (`PREGATE_REMOTE_INFRA`). The linked-worktree path uses the same snapshot.
+- **`lib/pregate/import-graph.ts`**: a relative string literal (`'../ship/sections/pr-body.md.tmpl'` through `new URL(…, import.meta.url)` or `readFileSync`) is a dependency edge from the file that names it, like a relative import; `test/ship-publication-gates.test.ts` is now selected when the pr-body template changes (the remote free gate missed it in GSTA-121's ship). A literal that climbs out of the repository names nothing.
+- **`scripts/lib/windows-curation.ts`**: `test/pregate-remote-stamp-only.test.ts` is `KNOWN_WINDOWS_INCOMPATIBLE` (it expects the Linux-host lanes verdict and skips itself on win32), as #3485's follow-up asked.
+
+### Tests
+- `test/paid-free-boundary.test.ts`: release metadata alone and beside `CHANGELOG.md` keeps mode `pr` with no paid case; `ETHOS.md` with `NOTICE.md` selects the two skills' gate cases and judge and nothing else. `test/pr-dependencies.test.ts`, `test/paid-pr-profile.test.ts`, `test/exercised-map.test.ts`: `SECURITY.md` as the unregistered example.
+- `test/redact-context-exemptions.test.ts`: seven run-word rows with run 40477638211 and five controls; `test/approval-record.test.ts`: an approval whose words cite `screen 40477638211 GREEN` is recorded verbatim, a phone number still refuses the row; `test/pregate-allowlist.test.ts`: its run-id fixture reads `job `, which the engine does not exempt, so the allowlist mechanism it tests still has a finding to silence.
+- `test/ubicloud-runner.test.ts`: pack of a plain checkout under twenty seconds of index, worktree and ref churn, three times; the unpacked snapshot has the same HEAD, `origin/main`, remote URL, status and `gstack-wtree` fingerprint, a detached HEAD lands on `detached-`, a pack with no index exits 1 and leaves nothing in `TMPDIR`, and a branch's upstream survives the snapshot.
+- `test/pregate-import-graph-paths.test.ts`: relative literals through `new URL` and `readFileSync`, one and two directories up, and a literal that escapes the repository.
+
+### Gate integrity
+- No threshold, timeout, trial count or case kind changed. The paid selection narrows only for `NOTICE.md`, `LICENSE`, `licenses/**`, `captain/templates/**` and `ETHOS.md`; every other unmapped file still restores the full gate. Generated SKILL.md sizes stay at their pins and the ship parity union stays under its 1.7500 cap; no allowlist or cap moved.
+
## [1.91.278.0] - 2026-10-27
**The merge train's tooling stops failing the lanes for things the tooling itself could see.** A head whose only conflict with main was the CHANGELOG got its Free Tests through the dispatch route and the paid screen still called it NOT-RUN; the receipt-versus-prose check read the canonical `REVIEW_LOOP` line as a false mismatch; a PR that writes a new `*_KEY_VERSION` constant as a tentative next-minor version (the convention #3511 introduces) could not clear the train's stamp-only receipt; a pooled pin learned that main had moved onto its files only when the train was built; a watcher had no way to leave a stuck non-required check out of its verdict; and a lane that passed the slot call's remaining order to `gstack-restamp` without its own PR got the same stale list back as the fix. Backlog items from GSTA-119, #3485, #3495 and the 2026-10-27 slot calls.
diff --git a/VERSION b/VERSION
index ec63d14a2..057e0ed5e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.91.278.0
+1.91.279.0
diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md
index 109a4f394..cd16452a1 100644
--- a/agents-digest/gstack-AGENTS.md
+++ b/agents-digest/gstack-AGENTS.md
@@ -1,4 +1,4 @@
-# gstack digest v1.91.278.0 — regenerate/re-copy after upgrading gstack
+# gstack digest v1.91.279.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,
diff --git a/bin/gstack-pr-shots b/bin/gstack-pr-shots
index a41052903..653d816d2 100755
--- a/bin/gstack-pr-shots
+++ b/bin/gstack-pr-shots
@@ -33,7 +33,7 @@ const HELP = `gstack-pr-shots — before/after image of a change for a PR body (
[--title ] [--name ] [--out ] [--json]
Example:
- gstack-pr-shots --path ship/SKILL.md --heading "Step 19: Create PR/MR" --caption "PR body opens with What changed, Why and Approvals under Capy"
+ gstack-pr-shots --path ship/SKILL.md --heading "Step 19: Create PR/MR" --caption "PR body opens with What changed, Why, Blast radius and Approvals under Capy"
${exitTableHelp()}`;
diff --git a/captain/templates/merge-ask.md b/captain/templates/merge-ask.md
index b477437f1..568a0a287 100644
--- a/captain/templates/merge-ask.md
+++ b/captain/templates/merge-ask.md
@@ -10,7 +10,10 @@ it before training the PR.
## Why
-
+
+
+## Blast radius
+
## Approvals
diff --git a/capy/skill/gstack/captain.md b/capy/skill/gstack/captain.md
index cd07f93d0..28564d917 100644
--- a/capy/skill/gstack/captain.md
+++ b/capy/skill/gstack/captain.md
@@ -126,9 +126,12 @@ Every PR you send READY on, and the READY report itself, has:
With no browser that line says `unavailable`: screenshot the printed `shot.html` with your own browser.
Upload each PNG with Capy's PR asset tool (`tools.pr.uploadAsset`), only for public repositories, and embed
the URL it returns with the caption as alt text.
-2. **Why.** The cause and the effect the user will see, in two or three sentences.
-3. **Approvals.** `gstack-approvals render`.
-4. **Evidence.** Checks green at the head, gate or train results, flaky cases.
+2. **Why.** The problem in the person's words first, then the cause and the effect the user will see,
+ in two or three sentences.
+3. **Blast radius.** What else the change touches and the one fact that makes that safe, with the
+ evidence-ladder rung that fact reached.
+4. **Approvals.** `gstack-approvals render`.
+5. **Evidence.** Checks green at the head, gate or train results, flaky cases.
The READY report attaches the key image (Capy message attachment), so the Captain can show it inline.
/ship does all of this when `GSTACK_CAPY=1` (its `capy-pr` section). Outside Capy, the PR body is unchanged.
diff --git a/docs/TESTING_INTERNALS.md b/docs/TESTING_INTERNALS.md
index d85b1656d..549999ec0 100644
--- a/docs/TESTING_INTERNALS.md
+++ b/docs/TESTING_INTERNALS.md
@@ -1097,8 +1097,10 @@ PR selection (`scripts/test-pr-profile.ts`) classifies every changed file. A fil
mapped by a paid case's touchfiles selects that case. These files are exempt and
select nothing: docs, any non-paid `*.test.ts(x)`, `FREE_ONLY_PR_FILES` (the
free and paid duration seeds, `scripts/ubicloud/**`, every workflow that runs no
-paid evals, and free-only tools), and the fixtures in `FREE_FIXTURES`
-(`test/helpers/free-fixtures-data.ts`, each fixture with its free consumers).
+paid evals, `captain/templates/**`, and free-only tools), the release metadata
+in `RELEASE_METADATA_FILES` (`NOTICE.md`, `LICENSE`, `licenses/**`), and the
+fixtures in `FREE_FIXTURES` (`test/helpers/free-fixtures-data.ts`, each fixture
+with its free consumers).
Unmapped files under `DERIVABLE_PREFIXES` (bin/, lib/, scripts/, browse/,
design/, make-pdf/, hosts/, extension/, model-overlays/, agents/, test/helpers/,
capy/) are not unknown: `scripts/pr-dependencies.ts` derives the cases whose
@@ -1506,6 +1508,20 @@ summary). Files a probe proved safe despite a `/tmp` literal are force-included
`WINDOWS_CURATED_FLOOR`; when Windows-safe tests are deleted on purpose, lower it
in the same PR with a reason.
+To see whether one test is in the subset, and why not when it is excluded:
+
+```bash
+bun run scripts/test-free-shards.ts --windows-only --list | grep test/.test.ts
+```
+
+A test a content pattern excludes wrongly joins the subset through
+`KNOWN_WINDOWS_SAFE` (with its reason) or, for a `/tmp` literal a probe proved
+safe, `WINDOWS_PROBE_SAFE`. A test that fails on `windows-latest` for a reason
+the patterns cannot see (it expects the Linux-host lanes verdict, skips itself
+on win32, drives a POSIX signal path) leaves it through a
+`KNOWN_WINDOWS_INCOMPATIBLE` entry naming the reason; rerun `--list` to confirm
+the file moved.
+
Native qualification campaigns (Windows cookie extraction, Windows launch
diagnostics, Dia on macOS) run only on dispatch of `native-qualification.yml`:
diff --git a/docs/captain.md b/docs/captain.md
index faba03ec7..deae58020 100644
--- a/docs/captain.md
+++ b/docs/captain.md
@@ -232,7 +232,7 @@ recommendation at full scope. `gstack-owner-brief render --captain` prints it fr
autoplan run.
**C. The merge ask** ([template](../captain/templates/merge-ask.md)): captioned
-before/after images, why, the Approvals table, and the evidence. /ship writes the PR
+before/after images, why, blast radius, the Approvals table, and the evidence. /ship writes the PR
body in this shape on Capy; the controller's merge report shows the key image inline.
## The digest
diff --git a/docs/pregate.md b/docs/pregate.md
index a1bf4619e..975e20f5f 100644
--- a/docs/pregate.md
+++ b/docs/pregate.md
@@ -566,7 +566,10 @@ that reads them ships. The worked gbrain configuration is in
The `secrets` check cannot be downgraded, and that is right for a key. It also
refused 1,800 per-trial dummy tokens in eval receipts (`"key": "sk-trial-"`
-reads as `env.kv`), an 11-digit Actions run id read as a phone number, and
+reads as `env.kv`), an 11-digit Actions run id read as a phone number (one
+after a run word such as `run`, `screen` or `gate` is exempt in the scanner
+since the GSTA-121 follow-up; the allowlist still covers one after any other
+word), and
excalidraw.com's public Firebase key that every rebuild of a bundle re-adds
(harness-evals #81/#83, GSTA-102, GSTA-106). `allowlist` in the policy names
those, each with its scope and its reason:
diff --git a/docs/review-policy.md b/docs/review-policy.md
index a9b44f990..046220eb6 100644
--- a/docs/review-policy.md
+++ b/docs/review-policy.md
@@ -39,6 +39,31 @@ floor above; everything else is INFORMATIONAL. Data loss, a trust-boundary
bypass and uncapped spend stay forced CRITICAL (the sideways sweep rule); no
lane, specialist, dedup or saved decision downgrades them.
+
+### Rulings below the floor
+
+The floor is a test, and these classes have been put to it, each more than
+once, and failed it (#3485 from the /ship of #3484, #3504 from the /ship of
+#3487). A finding in one of them is a **note** from the first pass on, named
+by its ruling in the PR body's `## Review notes`; it never earns a fix round
+or another review pass, and the lead judgment stage drops it from the defect
+count with the pointer. The floor itself does not move, so a finding that also
+loses data, crosses a trust boundary or spends without a cap stays CRITICAL
+whatever class it resembles.
+
+| Ruling | What it looks like | Why it is below the floor |
+|---|---|---|
+| **own-artifact hardening** | A check missing between two things the repository's own tooling writes: a screen verdict not compared with the receipt it belongs to; a receipt schema that does not declare a key the writer records; a policy loader that defaults when the base branch's own file is invalid; a size read that follows a symlink inside the repository. | The only writer is the repository's maintainers through its tools; no caller inside the intended case reaches the gap, and no test fails. A writer outside that trust boundary makes it a bypass, and CRITICAL. |
+| **unwired or unread** | A helper with no caller, a field written and never read, a duplicated helper, a header comment naming a step that does not run it, a key-version constant that moved before any release carried the old value. | Nothing takes the path, so nothing breaks; it is dead code to delete or wire in its own change. |
+| **bounded repeat work** | A re-dispatch on every call, a redundant screen after main moves, an orphaned scratch branch, a refusal that could come one step earlier in the sequence, a settle that could wait one more check. | The operation completes, nothing is lost, and the cost is one more bounded run; uncapped spend is the sideways sweep's CRITICAL, not this class. |
+| **conservative refusal** | Stale state scoped wider than its cause: a RED verdict that keeps refusing a head after main moved, a pending result that keeps refusing after it settled. | A refusal costs a rerun and never passes something wrong; a wrong pass would be the floor. |
+| **artifact presentation** | A string from an artifact printed verbatim into a parsed line (whitespace, control characters); a classifier that reads a non-zero exit code beside a non-RED verdict as RED. | Display and classification of the repository's own output; it forges no parsed field. |
+| **test gap** | A state-matrix cell without a test, a coverage percentage, a weak or flaky test, a fixture that shares the coordinator's environment. | Category `testing`, below the floor by definition above; a gap is work for the test audit, not a defect in the diff. |
+
+A reviewer that wants a class promoted argues it against the floor with a
+reaching input, in the finding, and the owner rules; the table records the
+rulings, it does not close them.
+
## `NO FINDINGS` is a result
A reviewer that finds nothing answers `NO FINDINGS` and nothing else. That is a
@@ -171,7 +196,7 @@ trailers, appended }`.
## Where this is enforced
-- `scripts/resolvers/review-army.ts`: the floor and `NO FINDINGS` in every specialist and Red Team prompt (rendered into `review/sections/review-army.md` and `ship/sections/review-army.md`); in `/ship` every specialist and Red Team prompt it renders carries its diff base as a literal sha the parent writes in (`DIFF_BASE=`: the merge base on pass 1, `RANGE_BASE` from pass 2 on), because a reviewer is its own process and never sees the parent's shell variables; the fan-out batches above.
+- `scripts/resolvers/review-army.ts`: the floor and `NO FINDINGS` in every specialist and Red Team prompt (rendered into `review/sections/review-army.md` and `ship/sections/review-army.md`), and the lead judgment's "On record" line that turns a [ruled class](#rulings-below-the-floor) into a note; in `/ship` every specialist and Red Team prompt it renders carries its diff base as a literal sha the parent writes in (`DIFF_BASE=`: the merge base on pass 1, `RANGE_BASE` from pass 2 on), because a reviewer is its own process and never sees the parent's shell variables; the fan-out batches above.
- `ship/sections/review-army.md` Step 9: passes, `RANGE_BASE`, classify, defer, render, the cap check before any edit, the Step 11 late-defer row, scratch files under `.gstack/tmp/`, the cap decisions; `ship/sections/adversarial.md` Step 11: the outside voice reads the same range and scopes queued fixes the same way.
- `lib/review-scope.ts`, `bin/gstack-review-scope`: `classify`, `defer`, `render`, `summary`; refusals are `REVIEW_SCOPE_INPUT` and `REVIEW_SCOPE_RANGE` ([troubleshooting.md](troubleshooting.md#review-scope-input)).
- `lib/ship-policy.ts`: the `review_scope` key and its `ship: review_scope …` consequence line.
diff --git a/docs/skills.md b/docs/skills.md
index 66827cd33..24ab4a175 100644
--- a/docs/skills.md
+++ b/docs/skills.md
@@ -735,6 +735,17 @@ Every `/ship` run builds a code path map from your diff, searches for correspond
A lot of branches die when the interesting work is done and only the boring release work is left. Humans procrastinate that part. AI should not.
+### The PR body
+
+`/ship` writes the PR body from the diff, the receipt and the review, and a reader who was not in the session decides the merge from it. On Capy (`CAPY_PR: on`) the body opens with four sections, in this order, before the standard Summary, Test plan and Documentation sections:
+
+1. **`## What changed`**: one to three captioned before/after images from `gstack-pr-shots` or a UI screenshot, highest-signal first, with a collapsed text version under each.
+2. **`## Why`**: two or three sentences. The first states the problem in the person's words (the issue, the message, the failing run), then the cause and the user-visible effect.
+3. **`## Blast radius`**: what else the change touches and the one fact that makes that safe, with the evidence-ladder rung that fact reached (a test that exercises the shared path, a grep over every caller, a measured run). A change to a shared helper, a generated file, a selection list or a policy default always has one; a leaf fix says so in one line.
+4. **`## Approvals`**: the table from `gstack-approvals render`, verbatim.
+
+Before the body is composed, `gstack-prose-lint` reads the Why, Summary and Blast radius drafts (`--sections 'Why,Summary,Blast radius' --gate`) and the CHANGELOG entry (`--base origin/ --gate`), and a `PROSE_LINT_FOUND` line names the sentence to rewrite; the rest of the body is advisory. The Captain's merge-ask template (`captain/templates/merge-ask.md`) carries the same four sections, so a READY report and its PR say the same thing.
+
### Versions: where `/ship` looks, and when it ships without one
`/ship` bumps a version only when the project says where the version lives. It checks, in order:
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 04255e7bb..a1089de32 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -2373,7 +2373,12 @@ exact tokens it finds in these positions:
`gates`, `backstop`, `bisect`, `rerun`, `dispatched`, `dispatch`,
`failed`, `passed` (`failed 40269992077 | rerun passed 40287239206`,
`gate 40323989742`, `run #19368712345`); a generic word such as `id`,
- `job`, `attempt` or `workflow` exempts nothing;
+ `job`, `attempt` or `workflow` exempts nothing. This position is exempt in
+ the scanner itself (`isCiRunIdentifier` in `lib/redact-patterns.ts`), so
+ `gstack-approvals add`, `gstack-decision-trail` and the pregate secrets
+ check accept it too, not only `gstack-post`. A national number written
+ without its `+` right after a run word reads as a run id there; that is the
+ accepted residual of the ruling that a run id is not a phone number;
- every 11-digit id chained after an allowlisted one through `, `, ` and `,
` & `, ` or ` or ` / `, optionally with a `(#1234)` between
(`screen 40269992077 (#3434) and 40275443393 (#3453)`).
diff --git a/lib/gstack-post.ts b/lib/gstack-post.ts
index 0bd643598..80e991e09 100644
--- a/lib/gstack-post.ts
+++ b/lib/gstack-post.ts
@@ -21,6 +21,7 @@
*/
import { createHash } from "crypto";
import { scan, type Finding, type RepoVisibility } from "./redact-engine";
+import { CI_RUN_WORD_BEFORE } from "./redact-patterns";
import { RECEIPT_FENCE } from "./ship-receipt";
import { canonicalRemote } from "./remote-identity";
import { parseChangelog } from "./changelog-check";
@@ -329,8 +330,6 @@ function repoReleaseVersions(env: PostEnv): string[] {
return [...versions];
}
-/** A run word with an optional ':' '#' '=' right before an 11-digit token: `run 19368712345`, `gate=40323989742`, `rerun passed 40287239206`. */
-const RUN_WORD_BEFORE = /\b(?:runs?|screens?|gates?|backstop|bisect|rerun|dispatch(?:ed)?|failed|passed)\s*[:#=]?\s*$/i;
/** Between two 11-digit tokens of one list: `, `, ` and `, ` & `, ` or `, ` / `, with an optional `(#1234)` after the first. */
const RUN_ID_CHAIN = /^(?:\s*\(#\d+\))?\s*(?:,|and|&|or|\/)\s*$/i;
@@ -344,7 +343,9 @@ const RUN_ID_CHAIN = /^(?:\s*\(#\d+\))?\s*(?:,|and|&|or|\/)\s*$/i;
* word on the same line (run, runs, screen, screens, gate, gates, backstop,
* bisect, rerun, dispatched, dispatch, failed, passed, with an optional ':'
* '#' '=' between: `failed 40269992077`, `gate 40323989742`,
- * `run #19368712345`; generic words such as id, job, attempt and workflow
+ * `run #19368712345`; the engine's isCiRunIdentifier exempts that position on
+ * its own since the run-id fix for approval records, and the same regex drives
+ * the chain below; generic words such as id, job, attempt and workflow
* exempt nothing, since `customer id 14155551234` is a number), plus every 11-digit id chained
* after an allowlisted one through ", ", " and ", " & ", " or " or " / ",
* optionally with a "(#1234)" between (`screen 40269992077 (#3434) and
@@ -361,7 +362,7 @@ function receiptDigitTokens(texts: string[]): string[] {
let chainFrom = -1;
for (const m of line.matchAll(/(?= 0 && RUN_ID_CHAIN.test(line.slice(chainFrom, at)));
+ const listed = CI_RUN_WORD_BEFORE.test(line.slice(0, at)) || (chainFrom >= 0 && RUN_ID_CHAIN.test(line.slice(chainFrom, at)));
if (listed) tokens.add(m[0]);
chainFrom = listed ? at + m[0].length : -1;
}
diff --git a/lib/pregate/import-graph.ts b/lib/pregate/import-graph.ts
index 22b90e4fb..19176e94b 100644
--- a/lib/pregate/import-graph.ts
+++ b/lib/pregate/import-graph.ts
@@ -49,7 +49,11 @@ const scanner = new Bun.Transpiler({ loader: 'tsx' });
// `( ) [ ] +` appear in framework route paths: Next.js route groups `(app)`,
// dynamic segments `[id]` / `[[...slug]]`, SvelteKit `+page.svelte`. A literal
// that matches more text only gains an edge when it resolves to a repo file.
-const QUOTED = /['"`]([A-Za-z0-9_@][A-Za-z0-9_./@()[\]+-]*)['"`]/g;
+// A relative literal (`'../ship/sections/pr-body.md.tmpl'` read through
+// `new URL(…, import.meta.url)` or `readFileSync`) resolves from the file that
+// names it, like a relative import; without it a test reading a template by
+// relative path had no edge to the template (GSTA-121's test/ship-publication-gates.test.ts).
+const QUOTED = /['"`]((?:\.\.?\/)*[A-Za-z0-9_@][A-Za-z0-9_./@()[\]+-]*)['"`]/g;
const JOIN = /\bjoin\(([^()]*)\)/g;
const RESOLVE_EXT = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts', '/index.ts', '/index.tsx', '/index.js', '/index.jsx', '/index.mjs'];
// A statement starts a line or follows `;` or a compound statement's `:`. The module spec is
diff --git a/lib/redact-patterns.ts b/lib/redact-patterns.ts
index 6ece3ff4b..9c820900c 100644
--- a/lib/redact-patterns.ts
+++ b/lib/redact-patterns.ts
@@ -328,16 +328,28 @@ export function isNumericMetadataValue(span: string, match: RegExpExecArray): bo
* command. Only the position decides: a run/job flag (`--run 37235771700`,
* `--job=111534615007`), a `gh run view|watch|rerun|download|cancel`
* argument, an `actions/runs/` or `/job/` URL segment, or a
- * `run_id` / `job_id` key. The same digits anywhere else still report.
+ * `run_id` / `job_id` key. An 11-digit id (every GitHub Actions run id since
+ * 2025) also counts right after a run word on the same line, the way a ship
+ * receipt, a READY block or an approval record names one (`screen 40477638211
+ * GREEN`, `gate=40323989742`, `rerun passed 40287239206`, `run #19368712345`);
+ * generic words (id, job, attempt, workflow) exempt nothing, since `customer id
+ * 14155551234` is a number. A national number written without its `+` right
+ * after a run word (`failed 14155551234`) is the accepted residual of the
+ * owner's ruling that a run id is not a phone number; gstack-post's chained-id
+ * allowlist reads the same position the same way. The same digits anywhere
+ * else still report.
*/
const CI_ID_POSITION_BEFORE =
/(?:--(?:run|job)(?:-id)?(?:[ \t]+|[ \t]*=[ \t]*)|\bgh[ \t]+run[ \t]+(?:view|watch|rerun|download|cancel)[ \t]+|\/actions\/runs\/|\/jobs?\/|(?:run|job)_id["']?[ \t]*[:=][ \t]*["']?)$/i;
+/** A run word with an optional ':' '#' '=' right before an 11-digit token; shared with gstack-post's chained-id allowlist. */
+export const CI_RUN_WORD_BEFORE = /\b(?:runs?|screens?|gates?|backstop|bisect|rerun|dispatch(?:ed)?|failed|passed)\s*[:#=]?\s*$/i;
export function isCiRunIdentifier(span: string, match: RegExpExecArray): boolean {
if (!/^\d+$/.test(span)) return false;
const input = match.input ?? "";
const { start } = spanBounds(match);
const lineStart = input.lastIndexOf("\n", start - 1) + 1;
- return CI_ID_POSITION_BEFORE.test(input.slice(Math.max(lineStart, start - 80), start));
+ const before = input.slice(Math.max(lineStart, start - 80), start);
+ return CI_ID_POSITION_BEFORE.test(before) || (span.length === 11 && CI_RUN_WORD_BEFORE.test(before));
}
/**
diff --git a/package.json b/package.json
index c42be4052..2b5266344 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gstack",
- "version": "1.91.278",
+ "version": "1.91.279",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
diff --git a/review/sections/review-army.md b/review/sections/review-army.md
index 22a98c1fc..885f3f24c 100644
--- a/review/sections/review-army.md
+++ b/review/sections/review-army.md
@@ -173,20 +173,22 @@ Core findings never earn a specialist confidence boost. Preserve `advisory`,
#### 4. Lead judgment
-Reviewers fill the space they are given. Before any merged finding counts, the
-parent reads each one and drops or downgrades it on these grounds, saying so:
-- **Nit gravity.** When a reviewer's whole return is style, naming and preference,
- the code is probably fine; write that down and count nothing from it as a defect.
+Reviewers fill the space they are given. Before a merged finding counts, the
+parent reads it and drops or downgrades it on these grounds, saying so:
+- **Nit gravity.** A reviewer whose whole return is style, naming and preference
+ found nothing; say so and count no defect.
- **Hypothetical, not actual.** A defect names the call site or input that reaches
- it. One that does not is a note until someone traces it.
+ it; one that does not is a note until someone traces it.
- **Structure for a case that does not exist.** Advice to abstract, generalize or
- guard a path nothing takes is advisory, never a defect.
+ guard a path nothing takes is advisory.
- **A different design is not a defect.** "I would have done it differently" with
- no failing case behind it is dropped.
+ no failing case is dropped.
- **Missing context.** A finding that assumes a rule the repository documents
otherwise is dropped with the pointer.
-Agreement between reviewers that did not see each other's output is the strongest
-signal; the `MULTI-SPECIALIST CONFIRMED` boost above records it. A reviewer that
+- **On record.** A class `docs/review-policy.md` rules below the floor is a note,
+ named by its ruling.
+Independent agreement between reviewers is the strongest signal (the
+`MULTI-SPECIALIST CONFIRMED` boost records it). A reviewer that
returned `NO FINDINGS` was right until a traced defect says otherwise.
#### 5. Apply specialist confidence gates
diff --git a/scripts/harness-version.json b/scripts/harness-version.json
index b651b9656..f71515581 100644
--- a/scripts/harness-version.json
+++ b/scripts/harness-version.json
@@ -17,7 +17,7 @@
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":6},
"scripts/lib/shard-engine.ts": {"blob":"dc714d7d897aed185c0ec3d2c92ad22d954039e5","since":6,"nonBehavioral":"createShardSandbox drops the Capy launch markers (GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL, GSTACK_INSTALL_VERDICT) from shard environments; CI never sets them, so no case's input or verdict changes there, and on a Capy machine fixtures now see the session kind they set themselves instead of the installer's"},
"scripts/test-paid-shards.ts": {"blob":"5439d0a8f6e1569fa7c924af20d67c30c81d5d30","since":6,"nonBehavioral":"credential rejections are classed infra instead of assertion: no case's pass or fail changes; only failed trials whose provider rejected the run's key move class, so no rerun spends on a rejected key"},
- "scripts/test-pr-profile.ts": {"blob":"a80de69f1923afed67127640d68c99e1a8528c5b","since":6,"nonBehavioral":"classify the new weekly-release.yml workflow and scripts/release-cut.ts as free-lane only PR files: the release cut runs on a cron and manual dispatch after merge and inside no case, so no case's selection on a PR, input or verdict changes"},
+ "scripts/test-pr-profile.ts": {"blob":"188710401a08156f585aebcb699151c60e3d6273","since":6,"nonBehavioral":"#3499: RELEASE_METADATA_FILES exempts NOTICE.md, LICENSE and licenses/** and captain/templates/** joins FREE_ONLY_PR_FILES; selection only, no case runs or is judged differently"},
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":6},
"test/helpers/agent-sdk-runner.ts": {"blob":"210bba92d241a90df578b2c7fc943ac5755d0025","since":6},
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":6},
diff --git a/scripts/lib/windows-curation.ts b/scripts/lib/windows-curation.ts
index 772a4d663..7098f3c7b 100644
--- a/scripts/lib/windows-curation.ts
+++ b/scripts/lib/windows-curation.ts
@@ -75,6 +75,7 @@ export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }>
{ file: 'test/pregate-dart.test.ts', reason: 'its dart, flutter and fvm stand-ins are bash scripts on a colon-joined PATH, which Bun.which and CreateProcess cannot use on Windows, and its containment case needs a POSIX symlink; the Dart lane code it drives is POSIX-run like the other pregate fixtures' },
{ file: 'test/pregate-non-js.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin and expects the Linux-host lanes verdict, like test/pregate-docs-lane-review.test.ts and test/pregate-selection.test.ts' },
{ file: 'test/pregate-remote-gbrain.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and runs the gbrain-shaped ci:ubicloud package script through the POSIX lane runner, like test/pregate-remote.test.ts' },
+ { file: 'test/pregate-remote-stamp-only.test.ts', reason: 'expects the Linux-host lanes verdict like test/pregate-remote.test.ts (on Windows the fixture\'s Linux lane is a requires-remote obligation) and skips itself on win32 (describe.skipIf), so the curated lane would run an empty file (#3485, #3486)' },
// C-pregate: these drive bin/gstack-issue-links and bin/gstack-ship-receipt through
// test/helpers/pregate-fixture.ts runBin.
{ file: 'test/stamp-templates.test.ts', reason: 'spawns bin/gstack-tree-receipt through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess)' },
diff --git a/scripts/resolvers/review-army.ts b/scripts/resolvers/review-army.ts
index 67fc6f125..f2a6e2e4a 100644
--- a/scripts/resolvers/review-army.ts
+++ b/scripts/resolvers/review-army.ts
@@ -218,20 +218,22 @@ Core findings never earn a specialist confidence boost. Preserve \`advisory\`,
#### 4. Lead judgment
-Reviewers fill the space they are given. Before any merged finding counts, the
-parent reads each one and drops or downgrades it on these grounds, saying so:
-- **Nit gravity.** When a reviewer's whole return is style, naming and preference,
- the code is probably fine; write that down and count nothing from it as a defect.
+Reviewers fill the space they are given. Before a merged finding counts, the
+parent reads it and drops or downgrades it on these grounds, saying so:
+- **Nit gravity.** A reviewer whose whole return is style, naming and preference
+ found nothing; say so and count no defect.
- **Hypothetical, not actual.** A defect names the call site or input that reaches
- it. One that does not is a note until someone traces it.
+ it; one that does not is a note until someone traces it.
- **Structure for a case that does not exist.** Advice to abstract, generalize or
- guard a path nothing takes is advisory, never a defect.
+ guard a path nothing takes is advisory.
- **A different design is not a defect.** "I would have done it differently" with
- no failing case behind it is dropped.
+ no failing case is dropped.
- **Missing context.** A finding that assumes a rule the repository documents
otherwise is dropped with the pointer.
-Agreement between reviewers that did not see each other's output is the strongest
-signal; the \`MULTI-SPECIALIST CONFIRMED\` boost above records it. A reviewer that
+- **On record.** A class \`docs/review-policy.md\` rules below the floor is a note,
+ named by its ruling.
+Independent agreement between reviewers is the strongest signal (the
+\`MULTI-SPECIALIST CONFIRMED\` boost records it). A reviewer that
returned \`NO FINDINGS\` was right until a traced defect says otherwise.
#### 5. Apply specialist confidence gates
diff --git a/scripts/test-pr-profile.ts b/scripts/test-pr-profile.ts
index a80de69f1..188710401 100644
--- a/scripts/test-pr-profile.ts
+++ b/scripts/test-pr-profile.ts
@@ -235,6 +235,7 @@ export const FREE_ONLY_PR_FILES = [
'scripts/paid-test-durations.json',
'scripts/ubicloud/**',
'capy/skill/**', // The vendored Capy skill (Drive bootstrapper): run by Capy agents, read by no paid case; test/capy-skill-wrapper.test.ts covers it.
+ 'captain/templates/**', // The /captain crew templates (CREW.md, merge-ask.md, slot-call.md, …): copied into a Captain's drive by people and Captains, read by no paid case; free test/captain-skill.test.ts covers them (#3499).
'tsconfig.test.json', // Read only by `tsc -p tsconfig.test.json` (typecheck:test); Bun's runtime reads tsconfig.json.
'.gitignore', // Changes which untracked files git reports, never tracked content a case reads.
'.gstack/generated.json', // gstack's own regen registry: gstack-regen/gstack-pregate read it from origin/ of the repo they run in; paid cases run in fixture repos.
@@ -285,6 +286,14 @@ export const FREE_ONLY_PR_FILES = [
'.github/workflows/windows-setup-e2e.yml',
] as const;
+/**
+ * Release metadata no paid case reads: the third-party notices and licence
+ * texts, and the root LICENSE. A courtesy entry or a licence line never
+ * widens the PR gate (#3499); CHANGELOG.md and VERSION, which the train stamp
+ * rewrites on every slot, are already exempt in knownNonBehaviorFile.
+ */
+export const RELEASE_METADATA_FILES = ['NOTICE.md', 'LICENSE', 'licenses/**'] as const;
+
/**
* Inputs every paid case consumes that must always restore the full gate
* (fail closed, even when a touchfile or a derived reference also names
@@ -317,6 +326,7 @@ export function knownNonBehaviorFile(file: string): boolean {
// by free doc/generation checks and are not copied into paid fixtures.
|| ['AGENTS.md', 'CLAUDE.md', 'agents-digest/gstack-AGENTS.md'].includes(file)
|| FREE_ONLY_PR_FILES.some(pattern => matchGlob(file, pattern))
+ || RELEASE_METADATA_FILES.some(pattern => matchGlob(file, pattern))
|| (PERIODIC_ONLY_PR_FILES as readonly string[]).includes(file)
|| (file.startsWith('test/fixtures/') && Object.keys(FREE_FIXTURES).some(pattern => matchGlob(file, pattern)))
// Free test files anywhere (test/, browse/test/, design/test/, make-pdf/test/): no paid case runs or reads a free test.
diff --git a/scripts/ubicloud/ubi-runner.sh b/scripts/ubicloud/ubi-runner.sh
index 77925713d..19fb9435e 100755
--- a/scripts/ubicloud/ubi-runner.sh
+++ b/scripts/ubicloud/ubi-runner.sh
@@ -587,35 +587,69 @@ cmd_down() {
}
# pack SRC: write a gzipped checkout tarball to stdout (tracked + untracked
-# files that are not ignored, plus .git; a non-git directory is taken whole).
-# A linked worktree's .git is a one-line pointer into the main repository,
-# useless on the VM, so it is replaced by a self-contained clone of the
-# checked-out branch carrying the worktree's own index.
+# files that are not ignored, plus a .git snapshot; a non-git directory is
+# taken whole). The live .git is never tarred: another process running git in
+# the checkout (a review subagent's diff, a trial merge's worktree add) writes
+# index.lock, packed-refs and objects while tar reads, and GNU tar exits 1
+# on a file that changed under it (GSTA-121's PREGATE_REMOTE_INFRA). Instead
+# the refs are fetched into a fresh repository through git's own transport,
+# which reads a consistent view, the remote.* config is replayed so
+# origin/ still resolves, and the index is copied (git writes it
+# atomically through a rename). A linked worktree's .git is a one-line
+# pointer into the main repository, useless on the VM; the same snapshot
+# replaces it, on the worktree's own branch and index. Not carried: submodule
+# repositories (.git/modules) and a partial clone's promisor (the fetch pulls
+# every missing blob first); gstack has neither.
cmd_pack() {
- local src tmp="" gitdir branch
+ local src tmp gitdir branch head entry
src=$(cd "${1:-.}" && pwd)
if git -C "$src" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
- if [ -f "$src/.git" ]; then
- branch=$(git -C "$src" rev-parse --abbrev-ref HEAD)
- [ "$branch" != HEAD ] || die "pack: $src is a linked worktree on a detached HEAD; check out a branch so the VM gets a self-contained .git"
- tmp=$(mktemp -d)
- gitdir=$(cd "$src" && cd "$(git rev-parse --git-common-dir)" && pwd)
- git clone -q --bare --no-local --single-branch --branch "$branch" "$gitdir" "$tmp/.git"
- git -C "$tmp/.git" config core.bare false
- # The worktree's index rides along so the VM's status matches, except a
- # split index, whose shared half lives only in the original git dir.
- [ "$(git -C "$src" config --bool core.splitIndex 2>/dev/null)" = true ] \
- || cp "$(cd "$src" && cd "$(dirname "$(git rev-parse --git-path index)")" && pwd)/index" "$tmp/.git/index"
+ head=$(git -C "$src" rev-parse --verify -q HEAD) || die "pack: $src has no commits; the VM needs a HEAD to fingerprint"
+ branch=$(git -C "$src" symbolic-ref -q --short HEAD || true)
+ tmp=$(mktemp -d)
+ # The path is baked in: a set -e failure unwinds the function before the trap runs, and a local $tmp is unset there.
+ trap "rm -rf $(printf %q "$tmp")" EXIT
+ # No template hooks from a global init.templateDir, and no auto-gc under the tar.
+ git -c gc.auto=0 init -q --bare --template= "$tmp/.git"
+ git -C "$tmp/.git" -c gc.auto=0 fetch -q --no-tags --update-shallow "$src" '+refs/*:refs/*' \
+ || die "pack: could not snapshot the refs of $src"
+ if [ -z "$branch" ]; then
+ branch="detached-${head:0:12}"
+ git -C "$tmp/.git" fetch -q --no-tags --update-shallow "$src" "+HEAD:refs/heads/$branch"
+ fi
+ git -C "$tmp/.git" symbolic-ref HEAD "refs/heads/$branch"
+ git -C "$tmp/.git" config core.bare false
+ gitdir=$(cd "$src" && cd "$(git rev-parse --git-common-dir)" && pwd)
+ while IFS= read -r -d '' entry; do
+ # A key with no value (`[remote "x"] mirror`) is boolean true.
+ case $entry in *$'\n'*) ;; *) entry="$entry"$'\n'true ;; esac
+ git -C "$tmp/.git" config --add "${entry%%$'\n'*}" "${entry#*$'\n'}"
+ done < <(git -C "$gitdir" config --local --null --get-regexp '^(remote|branch)\.' 2>/dev/null || true)
+ # The checkout's index rides along so the VM's status matches, except a
+ # split index, whose shared half lives only in the original git dir. The
+ # index's blobs that HEAD's tree lacks (staged, intent-to-add, the stages
+ # of an unmerged entry) are behind no ref, so they travel as a pack of
+ # their own; without them the VM's write-tree (gstack-wtree) cannot read a
+ # staged file.
+ if [ "$(git -C "$src" config --bool core.splitIndex 2>/dev/null)" != true ]; then
+ cp "$(cd "$src" && cd "$(dirname "$(git rev-parse --git-path index)")" && pwd)/index" "$tmp/.git/index"
+ # Read the copy, not the live index, so the pack matches it; a gitlink's
+ # sha (mode 160000) is a submodule commit this object store lacks.
+ GIT_INDEX_FILE="$tmp/.git/index" git -C "$src" ls-files -s | awk '$1 != "160000" {print $2}' | sort -u \
+ | comm -23 - <(git -C "$src" ls-tree -r HEAD | awk '{print $3}' | sort -u) \
+ | git -C "$src" pack-objects -q --stdout \
+ | git -C "$tmp/.git" unpack-objects -q \
+ || die "pack: could not snapshot the staged blobs of $src"
fi
(
cd "$src"
- { git ls-files -z -co --exclude-standard; [ -n "$tmp" ] || printf '.git\0'; } \
+ git ls-files -z -co --exclude-standard \
| while IFS= read -r -d '' f; do
if [ -e "$f" ] || [ -L "$f" ]; then printf '%s\0' "$f"; fi
done \
- | if [ -n "$tmp" ]; then tar --null -T - -c -C "$tmp" .git -zf -; else tar --null -T - -czf -; fi
+ | tar --null -T - -c -C "$tmp" .git -zf -
)
- [ -z "$tmp" ] || rm -rf "$tmp"
+ rm -rf "$tmp"; trap - EXIT
else
tar -C "$src" -czf - .
fi
diff --git a/ship/sections/capy-pr.md b/ship/sections/capy-pr.md
index 370dfad87..e0d538cec 100644
--- a/ship/sections/capy-pr.md
+++ b/ship/sections/capy-pr.md
@@ -1,6 +1,6 @@
-## Step 19-C: Capy PR body (What changed, Why, Approvals)
+## Step 19-C: Capy PR body (What changed, Why, Blast radius, Approvals)
Read only when Step 19 printed `CAPY_PR: on`. Under Capy a person decides the merge from the PR and
the READY report, through a Captain, so the body shows what changed in pictures, why, and who
diff --git a/ship/sections/capy-pr.md.tmpl b/ship/sections/capy-pr.md.tmpl
index 43f06922f..89598e739 100644
--- a/ship/sections/capy-pr.md.tmpl
+++ b/ship/sections/capy-pr.md.tmpl
@@ -1,4 +1,4 @@
-## Step 19-C: Capy PR body (What changed, Why, Approvals)
+## Step 19-C: Capy PR body (What changed, Why, Blast radius, Approvals)
Read only when Step 19 printed `CAPY_PR: on`. Under Capy a person decides the merge from the PR and
the READY report, through a Captain, so the body shows what changed in pictures, why, and who
diff --git a/ship/sections/review-army.md b/ship/sections/review-army.md
index df0d0dec1..efffdf030 100644
--- a/ship/sections/review-army.md
+++ b/ship/sections/review-army.md
@@ -456,20 +456,22 @@ Core findings never earn a specialist confidence boost. Preserve `advisory`,
#### 4. Lead judgment
-Reviewers fill the space they are given. Before any merged finding counts, the
-parent reads each one and drops or downgrades it on these grounds, saying so:
-- **Nit gravity.** When a reviewer's whole return is style, naming and preference,
- the code is probably fine; write that down and count nothing from it as a defect.
+Reviewers fill the space they are given. Before a merged finding counts, the
+parent reads it and drops or downgrades it on these grounds, saying so:
+- **Nit gravity.** A reviewer whose whole return is style, naming and preference
+ found nothing; say so and count no defect.
- **Hypothetical, not actual.** A defect names the call site or input that reaches
- it. One that does not is a note until someone traces it.
+ it; one that does not is a note until someone traces it.
- **Structure for a case that does not exist.** Advice to abstract, generalize or
- guard a path nothing takes is advisory, never a defect.
+ guard a path nothing takes is advisory.
- **A different design is not a defect.** "I would have done it differently" with
- no failing case behind it is dropped.
+ no failing case is dropped.
- **Missing context.** A finding that assumes a rule the repository documents
otherwise is dropped with the pointer.
-Agreement between reviewers that did not see each other's output is the strongest
-signal; the `MULTI-SPECIALIST CONFIRMED` boost above records it. A reviewer that
+- **On record.** A class `docs/review-policy.md` rules below the floor is a note,
+ named by its ruling.
+Independent agreement between reviewers is the strongest signal (the
+`MULTI-SPECIALIST CONFIRMED` boost records it). A reviewer that
returned `NO FINDINGS` was right until a traced defect says otherwise.
#### 5. Apply specialist confidence gates
diff --git a/test/approval-record.test.ts b/test/approval-record.test.ts
index a38424384..154688981 100644
--- a/test/approval-record.test.ts
+++ b/test/approval-record.test.ts
@@ -76,6 +76,18 @@ describe('gstack-approvals add and render', () => {
expect(fs.existsSync(path.join(state, 'projects'))).toBe(false);
});
+ test('a GitHub Actions run id after a run word is not a phone number, and the record keeps it (GSTA-121)', () => {
+ const cwd = repo(); const state = tmp('appr-state-');
+ const words = 'Rule-19 grant: re-READY with --no-screen citing screen 40477638211 GREEN at 5619199800a2; gate=40477638210 passed';
+ const r = bin(['add', '--ask', 'Third rule 19 grant on the GREEN screen', '--words', words, '--by', 'GStack Minder', '--scope', 'one merge'], { cwd, env: { GSTACK_STATE_ROOT: state } });
+ expect(r.status, r.err).toBe(0);
+ const file = /^APPROVAL_RECORDED: a-[0-9a-f]{12} (.*\.jsonl)$/m.exec(r.out)![1]!;
+ expect(fs.readFileSync(file, 'utf8')).toContain('screen 40477638211 GREEN');
+ const control = bin(['add', '--ask', 'ok', '--words', 'call me on 40477638211 or +14155550123', '--by', 'g', '--scope', 's'], { cwd, env: { GSTACK_STATE_ROOT: state } });
+ expect(control.status).toBe(3);
+ expect(control.err).toContain('pii.phone.e164');
+ });
+
test('missing fields, bad outcomes and impossible dates are usage errors', () => {
const cwd = repo(); const state = tmp('appr-state-');
expect(bin(['add', '--ask', 'x'], { cwd, env: { GSTACK_STATE_ROOT: state } }).err).toContain('APPROVAL_RECORD_INVALID');
diff --git a/test/exercised-map.test.ts b/test/exercised-map.test.ts
index 1a783285a..e3967fce9 100644
--- a/test/exercised-map.test.ts
+++ b/test/exercised-map.test.ts
@@ -161,7 +161,7 @@ describe('the five recorded variant-c misses on this checkout', () => {
describe('shadow output never changes the real selection', () => {
test('mode, cases, judges and reasons equal the selection without a shadow', () => {
- for (const files of [['bin/gstack-config'], ['design/src/evolve.ts'], ['review/SKILL.md.tmpl'], ['lib/fs-utils.ts', 'docs/x.md'], ['ETHOS.md']]) {
+ for (const files of [['bin/gstack-config'], ['design/src/evolve.ts'], ['review/SKILL.md.tmpl'], ['lib/fs-utils.ts', 'docs/x.md'], ['SECURITY.md']]) {
const withShadow = selectPrProfile({ selectedE2E: [], selectedJudges: [], changedFiles: files });
const without = selectPrProfile({ selectedE2E: [], selectedJudges: [], changedFiles: files, exercised: null });
expect(withShadow.shadow, files.join()).toBeDefined();
diff --git a/test/helpers/touchfiles-data.ts b/test/helpers/touchfiles-data.ts
index f76ae9a38..b2c12cb51 100644
--- a/test/helpers/touchfiles-data.ts
+++ b/test/helpers/touchfiles-data.ts
@@ -19,6 +19,10 @@
/**
* E2E test touchfiles — keyed by testName (the string passed to runSkillTest).
* Each test lists the file patterns that, if changed, require the test to run.
+ *
+ * ETHOS.md is registered under the /office-hours and /plan-ceo-review cases,
+ * whose prompts quote it; the tier>=3 preamble (Search Before Building) only
+ * names it as a read-on-demand pointer that no paid case asserts on (#3499).
*/
export const E2E_TOUCHFILES: Record = {
'ship-skipped-queued-finding': [ 'lib/outside-review-result.ts', 'lib/gate-outcomes.ts', 'bin/gstack-codex-probe','bin/gstack-state-root.sh', 'lib/state-root.ts', 'package.json', 'bun.lock', '.github/docker/Dockerfile.ci',
@@ -220,7 +224,7 @@ export const E2E_TOUCHFILES: Record = {
"test/fixtures/plan-scope-target-aw.json",
- 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'scripts/resolvers/review-dashboard.ts', 'scripts/resolvers/plan-gates.ts', 'scripts/resolvers/spec-review.ts', 'scripts/resolvers/outside-voice-steps.ts', 'scripts/resolvers/review-scope.ts', 'scripts/resolvers/outside-voice.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/skill-e2e-plan-ceo-plan-mode.test.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/helpers/native-auto-decide.ts', 'test/fixtures/auto-decide-current-declaration-6aef.json', 'test/fixtures/auto-decide-explanatory-mode-043a.json', 'test/fixtures/auto-decide-explanatory-mode-749df.json', 'test/fixtures/auto-decide-structured-77.json', 'test/helpers/auto-decision-state.ts', 'test/fixtures/auto-decide-state-cab3.json', 'bin/gstack-question-log', 'bin/gstack-question-preference', 'test/helpers/fake-plan-seed.ts', 'test/helpers/plan-seed-submission.ts', 'test/fixtures/plan-seed-cli.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**',
+ 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'ETHOS.md', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'scripts/resolvers/review-dashboard.ts', 'scripts/resolvers/plan-gates.ts', 'scripts/resolvers/spec-review.ts', 'scripts/resolvers/outside-voice-steps.ts', 'scripts/resolvers/review-scope.ts', 'scripts/resolvers/outside-voice.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/skill-e2e-plan-ceo-plan-mode.test.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/helpers/native-auto-decide.ts', 'test/fixtures/auto-decide-current-declaration-6aef.json', 'test/fixtures/auto-decide-explanatory-mode-043a.json', 'test/fixtures/auto-decide-explanatory-mode-749df.json', 'test/fixtures/auto-decide-structured-77.json', 'test/helpers/auto-decision-state.ts', 'test/fixtures/auto-decide-state-cab3.json', 'bin/gstack-question-log', 'bin/gstack-question-preference', 'test/helpers/fake-plan-seed.ts', 'test/helpers/plan-seed-submission.ts', 'test/fixtures/plan-seed-cli.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**',
'test/fixtures/design-scope-announcement-ao.json',
'test/fixtures/design-scope-declaration-ak.json',
@@ -310,7 +314,7 @@ export const E2E_TOUCHFILES: Record = {
"test/fixtures/plan-scope-recovery-av.json",
"test/fixtures/design-scope-checkpoint-at.json",
- 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/skill-e2e-plan-mode-no-op.test.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/helpers/native-auto-decide.ts', 'test/fixtures/auto-decide-current-declaration-6aef.json', 'test/fixtures/auto-decide-explanatory-mode-043a.json', 'test/fixtures/auto-decide-explanatory-mode-749df.json', 'test/fixtures/auto-decide-structured-77.json', 'test/helpers/auto-decision-state.ts', 'test/fixtures/auto-decide-state-cab3.json', 'bin/gstack-question-log', 'bin/gstack-question-preference', 'test/helpers/fake-plan-seed.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**',
+ 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'ETHOS.md', 'plan-eng-review/**', 'plan-design-review/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/skill-e2e-plan-mode-no-op.test.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/helpers/native-auto-decide.ts', 'test/fixtures/auto-decide-current-declaration-6aef.json', 'test/fixtures/auto-decide-explanatory-mode-043a.json', 'test/fixtures/auto-decide-explanatory-mode-749df.json', 'test/fixtures/auto-decide-structured-77.json', 'test/helpers/auto-decision-state.ts', 'test/fixtures/auto-decide-state-cab3.json', 'bin/gstack-question-log', 'bin/gstack-question-preference', 'test/helpers/fake-plan-seed.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**',
'test/fixtures/design-scope-announcement-ao.json',
'test/fixtures/design-scope-declaration-ak.json',
@@ -332,7 +336,7 @@ export const E2E_TOUCHFILES: Record = {
'office-hours-auto-mode': [ 'ETHOS.md','bin/gstack-state-root.sh', 'lib/state-root.ts', 'test/fixtures/auto-decide-recommendation-361c.json',
'test/fixtures/auto-decide-target-361c.json',
- 'test/fixtures/native-auto-decide-ag.json', 'test/helpers/native-auto-decide.ts', 'test/fixtures/auto-decide-current-declaration-6aef.json', 'test/fixtures/auto-decide-explanatory-mode-043a.json', 'test/fixtures/auto-decide-explanatory-mode-749df.json', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'office-hours/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/skill-e2e-office-hours-auto-mode.test.ts', 'test/helpers/plan-mode-evidence.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt',
+ 'test/fixtures/native-auto-decide-ag.json', 'test/helpers/native-auto-decide.ts', 'test/fixtures/auto-decide-current-declaration-6aef.json', 'test/fixtures/auto-decide-explanatory-mode-043a.json', 'test/fixtures/auto-decide-explanatory-mode-749df.json', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'office-hours/**', 'ETHOS.md', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/skill-e2e-office-hours-auto-mode.test.ts', 'test/helpers/plan-mode-evidence.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt',
'test/fixtures/pty-companion-cli.ts', 'lib/fs-atomic.ts', 'test/helpers/plan-skill-questions.ts', 'test/fixtures/eng-auq-validation-error.json', 'test/fixtures/bash-directory-permission.json', 'test/fixtures/design-tasks-bash-permission.json', 'test/fixtures/read-permission.json', 'test/fixtures/ceo-split-e5-numbered-description-491.json', 'test/helpers/plan-skill-question-events.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/skill-census.ts', 'test/helpers/auto-decision-state.ts', 'test/helpers/autoplan-artifact-digest.ts', 'test/helpers/autoplan-artifact-permission.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/helpers/dx-selected-navigation.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/eng-cache-writer-decision.ts', 'test/helpers/owned-claude-transcript.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/plan-count-file-permission.ts', 'test/helpers/plan-count-fixture.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-count-pending-question.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-floor-review.ts', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-scope-selection.ts', 'test/helpers/plan-seed-submission.ts', 'test/helpers/pty-screen.ts', 'bin/gstack-context-recovery'],
'office-hours-phase4-fork': [ 'ETHOS.md','bin/gstack-state-root.sh', 'lib/state-root.ts', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'office-hours/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'scripts/resolvers/question-tuning.ts', 'test/helpers/llm-judge.ts', 'test/skill-e2e-office-hours-phase4.test.ts', 'bin/gstack-context-recovery' ],
'llm-judge-recommendation': ['codex/**', 'test/helpers/llm-judge.ts', 'test/llm-judge-recommendation.test.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'codex/SKILL.md.tmpl', 'scripts/resolvers/review-dashboard.ts', 'scripts/resolvers/plan-gates.ts', 'scripts/resolvers/spec-review.ts', 'scripts/resolvers/outside-voice-steps.ts', 'scripts/resolvers/review-scope.ts', 'scripts/resolvers/outside-voice.ts'],
@@ -350,7 +354,7 @@ export const E2E_TOUCHFILES: Record = {
'test/fixtures/autoplan-public-narration-ad.json',
'test/fixtures/auto-decide-completed-mode-f359.json',
'test/helpers/plan-count-transcript.ts', 'test/fixtures/plan-count-cross-cwd-ancestry-0bcd.json',
- 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'bin/gstack-session-kind', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-preamble-bash.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'plan-ceo-review/**', 'bin/gstack-question-preference', 'bin/gstack-config', 'bin/gstack-slug', 'bin/gstack-remote-identity.sh', 'hosts/claude/hooks/question-preference-hook.ts', 'lib/bin-context.ts', 'lib/remote-identity.ts', 'hosts/claude/hooks/spawned-directive.ts', 'lib/is-conductor.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/skill-e2e-auto-decide-preserved.test.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/native-auto-decide.ts', 'test/fixtures/auto-decide-current-declaration-6aef.json', 'test/fixtures/auto-decide-explanatory-mode-043a.json', 'test/fixtures/auto-decide-explanatory-mode-749df.json', 'test/fixtures/auto-decide-structured-77.json', 'test/helpers/auto-decision-state.ts', 'test/fixtures/auto-decide-state-cab3.json', 'bin/gstack-question-log', 'test/helpers/fake-plan-seed.ts', 'test/helpers/plan-seed-submission.ts', 'test/fixtures/plan-seed-cli.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**',
+ 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'bin/gstack-session-kind', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-preamble-bash.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'plan-ceo-review/**', 'ETHOS.md', 'bin/gstack-question-preference', 'bin/gstack-config', 'bin/gstack-slug', 'bin/gstack-remote-identity.sh', 'hosts/claude/hooks/question-preference-hook.ts', 'lib/bin-context.ts', 'lib/remote-identity.ts', 'hosts/claude/hooks/spawned-directive.ts', 'lib/is-conductor.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/skill-e2e-auto-decide-preserved.test.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/native-auto-decide.ts', 'test/fixtures/auto-decide-current-declaration-6aef.json', 'test/fixtures/auto-decide-explanatory-mode-043a.json', 'test/fixtures/auto-decide-explanatory-mode-749df.json', 'test/fixtures/auto-decide-structured-77.json', 'test/helpers/auto-decision-state.ts', 'test/fixtures/auto-decide-state-cab3.json', 'bin/gstack-question-log', 'test/helpers/fake-plan-seed.ts', 'test/helpers/plan-seed-submission.ts', 'test/fixtures/plan-seed-cli.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**',
'test/helpers/plan-count-fixture.ts',
'test/fixtures/pty-companion-cli.ts', 'lib/fs-atomic.ts', 'test/fixtures/auto-decide-mode-selector-749df.json', 'lib/redact-engine.ts', 'lib/redact-patterns.ts', 'test/helpers/ceo-finding-fixture.ts', 'test/helpers/owned-claude-transcript.ts', 'test/helpers/plan-skill-questions.ts', 'test/fixtures/eng-auq-validation-error.json', 'test/fixtures/bash-directory-permission.json', 'test/fixtures/design-tasks-bash-permission.json', 'test/fixtures/read-permission.json', 'test/fixtures/ceo-split-e5-numbered-description-491.json', 'test/helpers/plan-skill-question-events.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/skill-census.ts', 'scripts/resolvers/tasks-section.ts', 'test/helpers/autoplan-artifact-digest.ts', 'test/helpers/autoplan-artifact-permission.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/helpers/dx-selected-navigation.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/eng-cache-writer-decision.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/plan-count-file-permission.ts', 'test/helpers/plan-count-pending-question.ts', 'test/helpers/plan-floor-review.ts', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-scope-selection.ts', 'hosts/claude/hooks/hook-log.ts', 'bin/gstack-context-recovery'],
@@ -380,7 +384,7 @@ export const E2E_TOUCHFILES: Record = {
'test/fixtures/ceo-mode-pending-submit.json',
"test/fixtures/plan-count-cross-cwd-ancestry-0bcd.json",
- "test/fixtures/ceo-mode-colon-at.json", 'plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/helpers/ceo-mode-option.ts', 'test/fixtures/ceo-expansion-disposition-77.json', 'test/helpers/plan-count-transcript.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-fixture.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/fixtures/eng-prerequisite-77.json', 'test/skill-e2e-plan-ceo-mode-routing.test.ts', 'test/fixtures/ceo-hold-posture-l.json', 'test/fixtures/ceo-mode-labels-l.json', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/ceo-mode-prerequisite-o-calls.json', 'test/fixtures/ceo-mode-prerequisite-q-call.json', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/fixtures/design-preview-v-screen.txt', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/ceo-mode-preview-aa-screen.txt', 'test/fixtures/ceo-count-mode-preview-aa-screen.txt', 'test/fixtures/ceo-expansion-auq-ac.json', 'test/helpers/plan-count-pending-question.ts', 'test/fixtures/ceo-barless-submit-ac.json', 'test/fixtures/pending-question-completion-ad.json', 'test/fixtures/ceo-mode-posture-ad.json', 'test/fixtures/ceo-mode-full-ad.json', 'test/fixtures/ceo-prerequisite-ad-v2.json', 'test/fixtures/ceo-hold-posture-ag.json',
+ "test/fixtures/ceo-mode-colon-at.json", 'plan-ceo-review/**', 'ETHOS.md', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/helpers/ceo-mode-option.ts', 'test/fixtures/ceo-expansion-disposition-77.json', 'test/helpers/plan-count-transcript.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-fixture.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/fixtures/eng-prerequisite-77.json', 'test/skill-e2e-plan-ceo-mode-routing.test.ts', 'test/fixtures/ceo-hold-posture-l.json', 'test/fixtures/ceo-mode-labels-l.json', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/ceo-mode-prerequisite-o-calls.json', 'test/fixtures/ceo-mode-prerequisite-q-call.json', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/fixtures/design-preview-v-screen.txt', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/ceo-mode-preview-aa-screen.txt', 'test/fixtures/ceo-count-mode-preview-aa-screen.txt', 'test/fixtures/ceo-expansion-auq-ac.json', 'test/helpers/plan-count-pending-question.ts', 'test/fixtures/ceo-barless-submit-ac.json', 'test/fixtures/pending-question-completion-ad.json', 'test/fixtures/ceo-mode-posture-ad.json', 'test/fixtures/ceo-mode-full-ad.json', 'test/fixtures/ceo-prerequisite-ad-v2.json', 'test/fixtures/ceo-hold-posture-ag.json',
"test/fixtures/ceo-hold-commitment-ar.json",
'test/fixtures/pty-companion-cli.ts', 'lib/fs-atomic.ts', 'test/helpers/ceo-finding-fixture.ts', 'test/helpers/plan-skill-questions.ts', 'test/fixtures/eng-auq-validation-error.json', 'test/fixtures/bash-directory-permission.json', 'test/fixtures/design-tasks-bash-permission.json', 'test/fixtures/read-permission.json', 'test/fixtures/ceo-split-e5-numbered-description-491.json', 'test/helpers/plan-skill-question-events.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/skill-census.ts', 'test/helpers/owned-claude-transcript.ts', 'scripts/resolvers/tasks-section.ts',
'test/fixtures/ceo-expansion-pacing-77.json', 'test/helpers/auto-decision-state.ts', 'test/helpers/autoplan-artifact-digest.ts', 'test/helpers/autoplan-artifact-permission.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/helpers/dx-selected-navigation.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/eng-cache-writer-decision.ts', 'test/helpers/native-auto-decide.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/plan-count-file-permission.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-floor-review.ts', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-scope-selection.ts', 'test/helpers/plan-seed-submission.ts'],
@@ -417,7 +421,7 @@ export const E2E_TOUCHFILES: Record = {
'ship-section-loading': [ 'ship/**', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-ship-section-loading.test.ts',
'scripts/resolvers/testing.ts', 'test/helpers/agent-sdk-runner.ts', 'test/helpers/auq-native-capture.ts', 'test/helpers/auto-decision-state.ts', 'test/helpers/autoplan-artifact-digest.ts', 'test/helpers/autoplan-artifact-permission.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/helpers/dx-selected-navigation.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/eng-cache-writer-decision.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/llm-judge.ts', 'test/helpers/native-auto-decide.ts', 'test/helpers/owned-claude-transcript.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/plan-count-file-permission.ts', 'test/helpers/plan-count-fixture.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-count-pending-question.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-floor-review.ts', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-scope-selection.ts', 'test/helpers/plan-seed-submission.ts', 'test/helpers/plan-skill-question-events.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/plan-skill-questions.ts', 'test/helpers/pty-screen.ts', 'test/helpers/pty-trust-dialog.ts', 'test/helpers/skill-census.ts'],
'plan-ceo-section-loading': [ 'ETHOS.md',
- 'test/fixtures/ceo-fill-lifetime.json', 'plan-ceo-review/**', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/helpers/ceo-section-loading-fixture.ts', 'test/helpers/ceo-stale-fill-decision.ts', 'test/skill-e2e-plan-ceo-review-section-loading.test.ts', 'test/fixtures/ceo-section-loading-l-report.md', 'test/fixtures/ceo-section-loading-q-report.md', 'test/fixtures/ceo-section-r-rejected-report.md', 'test/fixtures/ceo-section-s-trace-report.md', 'test/fixtures/ceo-section-u-report.md', 'test/fixtures/ceo-section-y-report.md', 'test/fixtures/ceo-section-aa-report.md', 'test/fixtures/sdk-stale-table-ad-v3.json', 'test/fixtures/sdk-ordering-ae.json', 'test/fixtures/sdk-columnar-af.json', 'test/fixtures/sdk-order-b-ag.json', 'test/fixtures/sdk-schedule-continuation-ah.json', 'test/fixtures/sdk-original-order-ai.json', 'test/fixtures/sdk-compact-sequence-aj.json',
+ 'test/fixtures/ceo-fill-lifetime.json', 'plan-ceo-review/**', 'ETHOS.md', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/helpers/ceo-section-loading-fixture.ts', 'test/helpers/ceo-stale-fill-decision.ts', 'test/skill-e2e-plan-ceo-review-section-loading.test.ts', 'test/fixtures/ceo-section-loading-l-report.md', 'test/fixtures/ceo-section-loading-q-report.md', 'test/fixtures/ceo-section-r-rejected-report.md', 'test/fixtures/ceo-section-s-trace-report.md', 'test/fixtures/ceo-section-u-report.md', 'test/fixtures/ceo-section-y-report.md', 'test/fixtures/ceo-section-aa-report.md', 'test/fixtures/sdk-stale-table-ad-v3.json', 'test/fixtures/sdk-ordering-ae.json', 'test/fixtures/sdk-columnar-af.json', 'test/fixtures/sdk-order-b-ag.json', 'test/fixtures/sdk-schedule-continuation-ah.json', 'test/fixtures/sdk-original-order-ai.json', 'test/fixtures/sdk-compact-sequence-aj.json',
"test/fixtures/sdk-reported-coordination-ar.md", "test/fixtures/sdk-ordered-schedule-ar.md",
'scripts/resolvers/review-dashboard.ts', 'scripts/resolvers/plan-gates.ts', 'scripts/resolvers/spec-review.ts', 'scripts/resolvers/outside-voice-steps.ts', 'scripts/resolvers/review-scope.ts', 'scripts/resolvers/outside-voice.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/tasks-section.ts', 'test/helpers/agent-sdk-runner.ts', 'test/helpers/auq-native-capture.ts', 'test/helpers/auto-decision-state.ts', 'test/helpers/autoplan-artifact-digest.ts', 'test/helpers/autoplan-artifact-permission.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/helpers/dx-selected-navigation.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/eng-cache-writer-decision.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/llm-judge.ts', 'test/helpers/native-auto-decide.ts', 'test/helpers/owned-claude-transcript.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/plan-count-file-permission.ts', 'test/helpers/plan-count-fixture.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-count-pending-question.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-floor-review.ts', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-scope-selection.ts', 'test/helpers/plan-seed-submission.ts', 'test/helpers/plan-skill-question-events.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/plan-skill-questions.ts', 'test/helpers/pty-screen.ts', 'test/helpers/pty-trust-dialog.ts', 'test/helpers/skill-census.ts'],
// Data-driven behavioral guard for the 'plan'/'prompt' carves: one case per
@@ -477,7 +481,7 @@ export const E2E_TOUCHFILES: Record = {
'test/fixtures/plan-floor-routing-361c.json',
- 'test/fixtures/ceo-report-permission-fb10.json', 'test/fixtures/plan-floor-permission-fb10.json', 'test/helpers/plan-count-file-permission.ts', 'test/fixtures/plan-edit-cropped-permission-1579.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/fixtures/design-crop-gutter-ap.json', 'test/fixtures/plan-count-crop-ak.json', 'test/fixtures/plan-count-long-edit-0bcd.json', 'test/fixtures/plan-count-cropped-wrap-6714.json', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/review-dashboard.ts', 'scripts/resolvers/plan-gates.ts', 'scripts/resolvers/spec-review.ts', 'scripts/resolvers/outside-voice-steps.ts', 'scripts/resolvers/review-scope.ts', 'scripts/resolvers/outside-voice.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-ceo-finding-floor.test.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-count-fixture.ts', 'test/helpers/plan-count-artifacts.ts',
+ 'test/fixtures/ceo-report-permission-fb10.json', 'test/fixtures/plan-floor-permission-fb10.json', 'test/helpers/plan-count-file-permission.ts', 'test/fixtures/plan-edit-cropped-permission-1579.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/fixtures/design-crop-gutter-ap.json', 'test/fixtures/plan-count-crop-ak.json', 'test/fixtures/plan-count-long-edit-0bcd.json', 'test/fixtures/plan-count-cropped-wrap-6714.json', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'ETHOS.md', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/review-dashboard.ts', 'scripts/resolvers/plan-gates.ts', 'scripts/resolvers/spec-review.ts', 'scripts/resolvers/outside-voice-steps.ts', 'scripts/resolvers/review-scope.ts', 'scripts/resolvers/outside-voice.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-ceo-finding-floor.test.ts', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-count-fixture.ts', 'test/helpers/plan-count-artifacts.ts',
'test/fixtures/pty-companion-cli.ts', 'lib/fs-atomic.ts', 'test/helpers/plan-skill-questions.ts', 'test/fixtures/eng-auq-validation-error.json', 'test/fixtures/bash-directory-permission.json', 'test/fixtures/design-tasks-bash-permission.json', 'test/fixtures/read-permission.json', 'test/fixtures/ceo-split-e5-numbered-description-491.json', 'test/helpers/plan-skill-question-events.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/skill-census.ts', 'scripts/resolvers/tasks-section.ts', 'test/helpers/auto-decision-state.ts', 'test/helpers/autoplan-artifact-digest.ts', 'test/helpers/autoplan-artifact-permission.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/helpers/dx-selected-navigation.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/eng-cache-writer-decision.ts', 'test/helpers/native-auto-decide.ts', 'test/helpers/owned-claude-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-count-pending-question.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-scope-selection.ts', 'test/helpers/plan-seed-submission.ts', 'bin/gstack-context-recovery'],
@@ -567,7 +571,7 @@ export const E2E_TOUCHFILES: Record = {
'test/fixtures/plan-count-cropped-wrap-6714.json',
"test/fixtures/plan-count-cross-cwd-ancestry-0bcd.json",
- 'test/fixtures/design-crop-gutter-ap.json', 'bin/gstack-config', 'plan-ceo-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'bin/gstack-question-preference', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/helpers/plan-count-transcript.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/fixtures/eng-prerequisite-77.json', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-ceo-split-overflow.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/helpers/plan-count-file-permission.ts', 'test/fixtures/plan-edit-cropped-permission-1579.json', 'test/fixtures/plan-count-edit-permission-t.json', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/fixtures/plan-count-permission-ah.json',
+ 'test/fixtures/design-crop-gutter-ap.json', 'bin/gstack-config', 'plan-ceo-review/**', 'ETHOS.md', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'bin/gstack-question-preference', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/fixtures/design-ui-boxed-question.json', 'test/helpers/pty-screen.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/pty-trust-dialog.ts', 'test/helpers/plan-count-transcript.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/fixtures/eng-prerequisite-77.json', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-ceo-split-overflow.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/helpers/plan-count-file-permission.ts', 'test/fixtures/plan-edit-cropped-permission-1579.json', 'test/fixtures/plan-count-edit-permission-t.json', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/fixtures/eng-d2-truncated-border-0bcd.json', 'test/fixtures/eng-d1-clipped-elision-1579.json', 'test/fixtures/eng-d2-planning-prelude-4d.json', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/fixtures/plan-count-permission-ah.json',
'test/fixtures/plan-count-crop-ak.json',
@@ -618,7 +622,7 @@ export const E2E_TOUCHFILES: Record = {
'plan-ceo-review-prosons-cadence': [ 'ETHOS.md', 'test/helpers/prosons-posture.ts',
"test/fixtures/plan-scope-recovery-av.json",
- "test/fixtures/design-scope-checkpoint-at.json", 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'model-overlays/opus-4-7.md', 'test/skill-e2e-plan-prosons.test.ts',
+ "test/fixtures/design-scope-checkpoint-at.json", 'plan-ceo-review/**', 'ETHOS.md', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'model-overlays/opus-4-7.md', 'test/skill-e2e-plan-prosons.test.ts',
"scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts",
@@ -627,7 +631,7 @@ export const E2E_TOUCHFILES: Record = {
'plan-review-prosons-format': [ 'ETHOS.md', 'test/helpers/prosons-posture.ts',
"test/fixtures/plan-scope-recovery-av.json",
- "test/fixtures/design-scope-checkpoint-at.json", 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'model-overlays/opus-4-7.md', 'test/skill-e2e-plan-prosons.test.ts',
+ "test/fixtures/design-scope-checkpoint-at.json", 'plan-ceo-review/**', 'ETHOS.md', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble-hoist.ts', 'model-overlays/opus-4-7.md', 'test/skill-e2e-plan-prosons.test.ts',
"scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts",
@@ -764,7 +768,7 @@ export const E2E_TOUCHFILES: Record = {
"test/fixtures/plan-scope-recovery-av.json",
"test/fixtures/disabled-dated-record-at.json",
- 'plan-eng-review/**', 'plan-ceo-review/**', 'hosts/claude.ts', 'hosts/define-host.ts',
+ 'plan-eng-review/**', 'plan-ceo-review/**', 'ETHOS.md', 'hosts/claude.ts', 'hosts/define-host.ts',
'scripts/gen-skill-docs.ts', 'scripts/resolvers/index.ts', 'scripts/resolvers/sections.ts',
'scripts/resolvers/review-dashboard.ts', 'scripts/resolvers/plan-gates.ts', 'scripts/resolvers/spec-review.ts', 'scripts/resolvers/outside-voice-steps.ts', 'scripts/resolvers/review-scope.ts', 'scripts/resolvers/outside-voice.ts', 'scripts/resolvers/constants.ts',
'bin/gstack-config', 'bin/gstack-codex-probe', 'bin/gstack-review-log', 'bin/gstack-review-read', 'lib/review-evidence.ts', 'bin/gstack-slug', 'bin/gstack-remote-identity.sh', 'bin/gstack-wtree', 'bin/gstack-brain-enqueue', 'test/helpers/session-runner.ts',
@@ -906,7 +910,7 @@ export const E2E_TOUCHFILES: Record = {
'test/fixtures/outside-async-task-m-events.json',
'test/fixtures/autoplan-amend-input-77.json',
'test/fixtures/autoplan-phase-handoff-6714.json','scripts/resolvers/learnings.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/preamble/generate-preamble-bash.ts', 'test/fixtures/plan-scope-recovery-av.json', 'test/fixtures/design-scope-checkpoint-at.json', 'scripts/resolvers/composition.ts', 'autoplan/**', 'lib/claude-journal-records.ts', 'lib/claude-owned-journal.ts', 'codex/**', 'bin/gstack-codex-probe', 'scripts/resolvers/review-dashboard.ts', 'scripts/resolvers/plan-gates.ts', 'scripts/resolvers/spec-review.ts', 'scripts/resolvers/outside-voice-steps.ts', 'scripts/resolvers/review-scope.ts', 'scripts/resolvers/outside-voice.ts', 'scripts/resolvers/design.ts', 'test/skill-e2e-autoplan-dual-voice.test.ts', 'bin/gstack-autoplan-snapshot.ts', 'test/fixtures/autoplan/t-ceo-omitted-obligations.json', 'test/fixtures/autoplan/u-ceo-original-loss.json', 'test/fixtures/autoplan/v-ceo-dangling-references.json',
- 'scripts/resolvers/design-doc-discovery.ts', 'bin/gstack-design-doc-find', 'bin/gstack-ceo-plan-find', 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/testing.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/tasks-section.ts', 'test/helpers/autoplan-phase-observer.ts', 'scripts/resolve-codex-generation-model.ts', 'scripts/resolvers/constants.ts'],
+ 'scripts/resolvers/design-doc-discovery.ts', 'bin/gstack-design-doc-find', 'bin/gstack-ceo-plan-find', 'plan-ceo-review/**', 'ETHOS.md', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/testing.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/tasks-section.ts', 'test/helpers/autoplan-phase-observer.ts', 'scripts/resolve-codex-generation-model.ts', 'scripts/resolvers/constants.ts'],
// Multi-provider benchmark adapters — live API smoke against real claude/codex/gemini CLIs
'benchmark-providers-live': ['bin/gstack-model-benchmark', 'test/helpers/providers/**', 'test/helpers/benchmark-runner.ts', 'test/helpers/pricing.ts', 'test/skill-e2e-benchmark-providers.test.ts'],
diff --git a/test/paid-free-boundary.test.ts b/test/paid-free-boundary.test.ts
index 87c208ebc..f66ffec4a 100644
--- a/test/paid-free-boundary.test.ts
+++ b/test/paid-free-boundary.test.ts
@@ -106,6 +106,25 @@ describe('paid/free dependency boundary', () => {
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
+ test('release metadata and crew templates never widen the PR gate; ETHOS.md selects only the prompts that quote it (#3499)', () => {
+ // Release metadata no paid case reads keeps the PR profile, alone and beside a mapped prompt (#3499).
+ for (const file of ['NOTICE.md', 'LICENSE', 'licenses/MIT-pstack.txt', 'captain/templates/merge-ask.md']) {
+ expect(fs.existsSync(path.join(ROOT, file)), file).toBe(true);
+ const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [file, 'CHANGELOG.md'] });
+ expect(result.coverage?.mode, file).toBe('pr');
+ expect(result.coverage?.unknownFiles, file).toEqual([]);
+ expect(result.selection, file).toEqual({ e2e: [], judges: [] });
+ }
+ // ETHOS.md is read by the /office-hours and /plan-ceo-review prompts, so it selects their cases and judge, not the whole gate.
+ const ethos = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: ['ETHOS.md', 'NOTICE.md'] });
+ expect(ethos.coverage?.mode).toBe('pr');
+ expect(ethos.coverage?.unknownFiles).toEqual([]);
+ expect(ethos.selection.e2e).toContain('plan-ceo-review-plan-mode');
+ expect(ethos.selection.e2e).toContain('office-hours-auto-mode');
+ expect(ethos.selection.judges).toEqual(['plan-ceo-review/SKILL.md modes']);
+ expect(ethos.selection.e2e!.length).toBeLessThan(10);
+ });
+
test('only audited free-only dependencies avoid unknown-dependency fallback', () => {
for (const file of [...FREE_ONLY_PR_FILES, 'scripts\\test-free-shards.ts']) {
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [file] });
@@ -114,7 +133,7 @@ describe('paid/free dependency boundary', () => {
expect(result.selection).toEqual({ e2e: [], judges: [] });
}
// Tracked files outside every mapping and outside the derivable directories restore the full gate.
- for (const file of ['LICENSE', 'SECURITY.md', 'conductor.json', 'NOTICE.md']) {
+ for (const file of ['SECURITY.md', 'conductor.json']) {
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [FREE_ONLY_PR_FILES[0], file] });
expect(result.coverage?.mode, file).toBe('full-fallback');
expect(result.coverage?.unknownFiles).toContain(file);
diff --git a/test/pregate-allowlist.test.ts b/test/pregate-allowlist.test.ts
index 5c5ea19a9..5cafdb346 100644
--- a/test/pregate-allowlist.test.ts
+++ b/test/pregate-allowlist.test.ts
@@ -84,17 +84,17 @@ describe('allowlist: the secrets check', () => {
expect(secrets(shape).status).toBe(EXIT.fail);
});
- // Value: protects=a category entry covers an advisory pii hit (a run id read as a phone number) and the WARN count drops;
+ // Value: protects=a category entry covers an advisory pii hit (a run id after a generic word, which the engine's run-word exemption does not cover, read as a phone number) and the WARN count drops;
// fails_when=category matching is id-only; why_new=new; seam=none; no_claim=none
test('a category entry silences the advisory pii finding for a run id', () => {
const repo = fixture({ ...BASE_PREGATE, allowlist: [{ finding: 'pii', pattern: '\\d{11}', reason: 'GitHub Actions run ids in reports' }] });
- commit(repo, { 'docs/runs.md': 'run 40302077688 passed\n' });
+ commit(repo, { 'docs/runs.md': 'job 40302077688 passed\n' });
expect(secrets(repo).status).toBe(EXIT.ok);
const r = row(repo);
expect([r.status, r.code]).toEqual(['pass', undefined]);
expect(r.detail).toContain('1 allowlisted (pii.phone.e164)');
const control = fixture(BASE_PREGATE);
- commit(control, { 'docs/runs.md': 'run 40302077688 passed\n' });
+ commit(control, { 'docs/runs.md': 'job 40302077688 passed\n' });
secrets(control);
expect([row(control).status, row(control).code]).toEqual(['warn', 'PREGATE_SECRETS']);
expect(row(control).detail).toContain('pii.phone.e164');
diff --git a/test/pregate-import-graph-paths.test.ts b/test/pregate-import-graph-paths.test.ts
index 500226105..4631a3662 100644
--- a/test/pregate-import-graph-paths.test.ts
+++ b/test/pregate-import-graph-paths.test.ts
@@ -30,6 +30,24 @@ describe('import graph: route-group / dynamic-segment paths and tsconfig path al
expect(testsFor(g, 'app/[[...slug]]/page.tsx').tests).toEqual(['test/pages.test.ts']);
});
+ test('a relative literal a test reads through new URL or readFileSync maps from the file that names it', () => {
+ const repo = fixture();
+ commit(repo, {
+ 'ship/sections/pr-body.md.tmpl': '## Summary\n',
+ 'ship/sections/other.md.tmpl': '## Other\n',
+ 'docs/guide.md': '# guide\n',
+ 'test/publication.test.ts': "import { readFileSync } from 'node:fs';\nreadFileSync(new URL('../ship/sections/pr-body.md.tmpl', import.meta.url), 'utf8');\nreadFileSync('./../docs/guide.md', 'utf8');\n",
+ 'test/nested/deep.test.ts': "import { readFileSync } from 'node:fs';\nreadFileSync('../../ship/sections/other.md.tmpl', 'utf8');\n",
+ });
+ const g = buildGraph(repo.work);
+ expect(testsFor(g, 'ship/sections/pr-body.md.tmpl').tests).toEqual(['test/publication.test.ts']);
+ expect(testsFor(g, 'docs/guide.md').tests).toEqual(['test/publication.test.ts']);
+ expect(testsFor(g, 'ship/sections/other.md.tmpl').tests).toEqual(['test/nested/deep.test.ts']);
+ // A relative literal that climbs out of the repository names nothing.
+ commit(repo, { 'test/escape.test.ts': "import { readFileSync } from 'node:fs';\nreadFileSync('../../../ship/sections/pr-body.md.tmpl', 'utf8');\n" });
+ expect(testsFor(buildGraph(repo.work), 'ship/sections/pr-body.md.tmpl').tests).toEqual(['test/publication.test.ts']);
+ });
+
test('a relative import of a bracketed path already maps (control: the import scanner is not the bug)', () => {
const repo = fixture();
commit(repo, {
diff --git a/test/redact-context-exemptions.test.ts b/test/redact-context-exemptions.test.ts
index 0fd131100..39dd75a6c 100644
--- a/test/redact-context-exemptions.test.ts
+++ b/test/redact-context-exemptions.test.ts
@@ -225,6 +225,14 @@ describe("pii.phone.e164: GitHub Actions run and job ids in their id positions",
"https://github.com/garrytan/gstack/actions/runs/37235771700/job/111534615007",
"GITHUB_RUN_ID=37235771700",
'{"run_id": 37235771700}',
+ // An 11-digit id after a run word, as a READY block, a ship receipt or an approval record names one (GSTA-121's `screen GREEN`).
+ "citing screen 40477638211 GREEN at 5619199800a2",
+ "gate=40477638211",
+ "rerun passed 40477638211",
+ "run #40477638211",
+ "Screens: 40477638211",
+ "dispatched 40477638211 at the pin",
+ "cleared by run 19368712345",
],
[
"call 37235771700 tomorrow",
@@ -232,6 +240,12 @@ describe("pii.phone.e164: GitHub Actions run and job ids in their id positions",
"https://example.com/contact/37235771700",
'{"phone_id": 37235771700}',
"run 4155550123 laps",
+ // A run word before a 10-digit national number, a bare 11-digit number and a generic word exempt nothing.
+ "gate 4155550123",
+ "the id is 40477638211",
+ "job 40477638211",
+ "workflow 40477638211",
+ "40477638211 failed",
],
);
});
diff --git a/test/ubicloud-runner.test.ts b/test/ubicloud-runner.test.ts
index 43085e731..8adacf278 100644
--- a/test/ubicloud-runner.test.ts
+++ b/test/ubicloud-runner.test.ts
@@ -311,6 +311,43 @@ test('pack ships a linked worktree with a self-contained .git on the checked-out
} finally { rmSync(d, { recursive: true, force: true }); }
}, 60_000);
+ test('pack snapshots a plain checkout without reading its live .git: remotes resolve, a detached HEAD gets a branch, and git running beside it cannot fail the tar', () => {
+ const d = mkdtempSync(join(tmpdir(), 'ubi-pack-live-'));
+ try {
+ const sh = (cmd: string, cwd: string) => { const r = Bun.spawnSync(['bash', '-c', cmd], { cwd, timeout: 60_000, env: { ...process.env, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' } }); expect(r.exitCode, r.stderr.toString()).toBe(0); return r.stdout.toString(); };
+ sh('git init -q --bare origin.git && git clone -q origin.git src 2>/dev/null && cd src && echo a > a && git add a && git commit -qm one && git push -q origin HEAD:main && echo b > b && git add b && git commit -qm two && git push -q origin HEAD:main && git checkout -q --detach && echo c > c && echo s > staged && git add staged && echo i > inta && git add -N inta', d);
+ const src = join(d, 'src');
+ const pack = `bash ${JSON.stringify(join(DIR, 'ubi-runner.sh'))} pack .`;
+ // Twenty seconds of index.lock, worktree and ref churn in the checkout while pack runs three times.
+ const churn = Bun.spawn(['bash', '-c', 'end=$((SECONDS+20)); while [ $SECONDS -lt $end ]; do git update-index -q --refresh; git worktree add -q ../wt HEAD >/dev/null 2>&1; git worktree remove --force ../wt >/dev/null 2>&1; git update-ref refs/heads/churn HEAD; git update-ref -d refs/heads/churn; done'], { cwd: src, stdout: 'ignore', stderr: 'ignore' });
+ try {
+ for (const i of [1, 2, 3]) sh(`${pack} > ../pack-${i}.tgz`, src);
+ } finally { churn.kill(); }
+ sh('mkdir vm && tar -xzf pack-3.tgz -C vm', d);
+ const vm = join(d, 'vm');
+ expect(lstatSync(join(vm, '.git')).isDirectory()).toBe(true);
+ const head = sh('git rev-parse HEAD', src).trim();
+ expect(sh('git rev-parse HEAD && git rev-parse --abbrev-ref HEAD && git rev-parse origin/main && git config --get remote.origin.url && git status --short', vm).split('\n').filter(Boolean))
+ .toEqual([head, `detached-${head.slice(0, 12)}`, head, join(d, 'origin.git'), ' A inta', 'A staged', '?? c']);
+ // The staged blob and the intent-to-add entry's empty blob are behind no ref; the snapshot carries them, so the VM can read and write-tree them.
+ expect(sh('git fsck --no-progress 2>&1; git diff --cached --name-only', vm).trim()).toBe('staged');
+ const wtree = (cwd: string) => sh(`bash ${JSON.stringify(join(ROOT, 'bin/gstack-wtree'))}`, cwd).trim();
+ expect(wtree(vm)).toBe(wtree(src));
+ // The snapshot carries no hook path or other local config from the source checkout.
+ expect(sh('git config --local --list', vm)).not.toContain('hooksPath');
+ // A failed pack leaves no object copy behind in TMPDIR, and a branch's upstream rides along with its remote.
+ sh('mkdir ../tmpd && git checkout -q -b topic && git branch -q --set-upstream-to=origin/main topic', src);
+ const broken = Bun.spawnSync(['bash', '-c', `rm -f .git/index && TMPDIR=../tmpd ${pack} > ../pack-broken.tgz; echo rc=$?; ls ../tmpd`], { cwd: src, timeout: 60_000 });
+ expect(broken.stderr.toString()).toContain('.git/index');
+ expect(broken.stdout.toString().trim()).toBe('rc=1');
+ // A gitlink in the index (a submodule commit this object store lacks) is not packed, and a valueless
+ // remote key (`[remote "origin"] mirror`) replays as true.
+ sh('git reset -q && git update-index --add --cacheinfo 160000,1111111111111111111111111111111111111111,sub && printf \'[remote "origin"]\\n\\tmirror\\n\' >> .git/config', src);
+ sh('mkdir vm2 && ' + pack.replace('pack .', 'pack src') + ' | tar -xzf - -C vm2', d);
+ expect(sh('git rev-parse --abbrev-ref @{u} && git config --bool --get remote.origin.mirror && git ls-files -s sub | cut -c1-6', join(d, 'vm2')).split('\n').filter(Boolean)).toEqual(['origin/main', 'true', '160000']);
+ } finally { rmSync(d, { recursive: true, force: true }); }
+ }, 90_000);
+
test('up records the VM in the scratch file before the create request is sent', () => {
const runner = read('scripts/ubicloud/ubi-runner.sh');
const create = runner.indexOf('cli "${args[@]}" >/dev/null 2>"$dir/create.err"');