Commit Graph
10 Commits
Author SHA1 Message Date
garrytan 10ab8559b9 E1: /gstack-upgrade checks the incoming Bun floor before pulling
gstack_bun_incoming_hold (bin/gstack-bun-version.sh) is now the one
pre-advance check: the auto-updater calls it before its fast-forward and
/gstack-upgrade's git path calls it after fetching. Below the incoming
release's floor the upgrade stops with BUN_TOO_OLD and the checkout,
VERSION and installs stay untouched. Vendored installs were already safe:
setup refuses before writing and the swap restores the previous install.
2026-10-04 18:31:33 +00:00
garrytan 850ac83997 E1: one Bun version source; setup refuses below the 1.3.3 floor, updater checks before advancing
bin/gstack-bun-version.sh holds the security floor (1.3.3) and the tested
version (1.4.0, the CI pin and engines.bun). setup refuses Bun below the
floor before writing anything, warns between floor and tested, warns and
continues on an unparseable version, and prints a 1.4.0 install hint.
Prereleases sort below their release (semver).

gstack-session-update now fetches, reads the incoming release's floor and
checks the bun setup will run before fast-forwarding to that exact
revision; below it the update is held with reason bun-too-old and a
resume command, leaving the revision and installs untouched.

Fixture copies of setup (team-mode, claude-code migration) now include the
sourced file; the slow-git lock test sleeps on fetch, the updater's
network step.
2026-10-04 17:24:49 +00:00
+44 2db0b3adc8 v1.91.19.0 fix: severe fix wave (truthful gates, no lost memory, every host finds gstack) (#3025)
* docs: approved severe fix wave plan (Oct 2026)

* D0: keep a project's .env and bunfig.toml out of compiled binaries and daemon children

Compiled binaries are built with --no-compile-autoload-dotenv and
--no-compile-autoload-bunfig; the browse server and terminal agent are
spawned with --no-env-file and an empty config, so a repo's .env cannot set
GSTACK_CHROMIUM_NO_SANDBOX and a bunfig preload cannot run code in them.
Verified on Bun 1.4.0.

* A9: --no-write is a dry run and leaves ingest state untouched

--no-write stamped every prepared page ingested_at and saved state, so a
later real run skipped those pages forever. It now prepares and reports
pages without importing or changing the state file.

Tests that used GSTACK_MEMORY_INGEST_NO_WRITE / --no-write to seed state
move to an explicit seam (a fake gbrain that accepts every staged page),
because the --no-write contract deliberately changes.

* H #2885 #2827: vector geometry is not phone or card PII

pii.phone.e164 accepts "." as a group separator, so a single float
(viewBox="0 0 581.66796875 ...") and a run of path coordinates
("37.6188 101.694") read as phone numbers. pii.cc's \b stops at a decimal
point, so the long fraction of an .excalidraw float is a Luhn candidate.
Excalidraw's seed/versionNonce/updated integers read as bare phone numbers.
A rendered /diagram triplet raised 18 (svg) and 97 (excalidraw) MEDIUM
findings on main; it now raises none.

- looksLikeDecimalCoordinates (ported from #2885, extended to a single
  decimal and refusing a leading "+", the E.164 marker)
- insideDecimalNumber: a cc span glued to "<digit>." or ".<digit>"
- isNumericMetadataValue: an unquoted JSON value under a seed, nonce or
  timestamp key; the same digits under "phone" still report

Controls: E.164 in prose, dotted and spaced phones, "+"-prefixed runs,
"phone"/"contact" JSON numbers, Luhn-valid cards in prose and at sentence end.

Co-authored-by: Lewis <lewispeel@hotmail.com>

* D1: pin the terminal-agent /ws Origin to the gstack extension (#1324)

GSTACK_EXTENSION_ID moves to browse/src/extension-id.ts, shared by
/extension-token and the terminal agent's /ws gate. /ws now returns 403 for
every origin except the pinned extension. Forks override the ID with
gstack-config set browse_extension_id <id>, honored by both routes; the
BROWSE_EXTENSION_ID env var is no longer read. Test sites that used fake
extension IDs now use the pinned origin.

* H #2899: env.kv skips a function call assigned to a credential name

`session = _FlakySession(...)`, `token = make_token(user, ...)` and
`password = getpass.getpass()` raised env.kv MEDIUM, which blocks the
fail-closed decision log. The exemption needs the line's evidence, not the
span's shape: the value is unquoted, is an identifier or dotted path
followed by "(", and the call closes on the line or opens a multi-line
argument list. Quoted values, unclosed "(" inside an unquoted secret
(API_SECRET=Abc123xyz(9Qm) and calls carrying a high-entropy literal
argument still report. The literal check is shared with the #2912 env-read
exemption.

Co-authored-by: Yugz <yannduzelier@gmail.com>

* H #2962: per-machine config filenames are not internal hostnames

`CLAUDE.local.md` and `settings.local.json` reported internal.hostname
MEDIUM (the pattern's \b stops at the dot before the extension), which fails
closed in the decision log. A span immediately followed by a known config
extension (.md .json .jsonc .yaml .yml .toml .ini .conf .txt) and a word
boundary is a filename. Controls: `ssh build.local`, `printer.local.` at a
sentence end, `printer.local/md`, `printer.local.mdx`, a host with a port,
`api.corp`, `build-7.internal`. The `.env.local` vectors (#2740) pass on
main already via isDotenvFilename and are pinned here as regressions.

Co-authored-by: Florian <florian.cleyetmerle@gmail.com>

* H #2784: four-part versions need a version declaration, not a shape

`"version": "1.128.1.0"`, `<Version>`, `AssemblyVersion("1.0.0.0")` and
Keep a Changelog headings raised pii.ip_public MEDIUM on every release,
because a four-part version is byte-for-byte a dotted quad. Exempt only when
the same line declares it a version immediately before the value (a version
key or word, an XML tag, an attribute call, or a `## [x]` heading). The same
digits after host:/server =/"upstream", bare on a line, or after prose that
merely mentions a version still report. Unlike #2784, this does not exempt a
value because the same string is declared a version elsewhere in the text:
that let a real address sharing a diff with a matching version slip. The
pre-push hook's VERSION-file rule (#2856) is unchanged.

Co-authored-by: cdecook23 <decook23@gmail.com>

* H #2913: postgres:postgres is exempt only on a loopback host

The postgres image's default pair in compose files and CI DATABASE_URLs
blocked pushes as db.url_with_password HIGH. It is now exempt only for
postgres/postgresql URLs with exactly postgres:postgres on localhost or
127.0.0.1 (optional port). A bare single-label host (`db`, `postgres`), any
other host, a host that merely starts with localhost (`localhost.evil...`,
`localhost@prod-db`), any other user or password, and other schemes still
block. #2913 exempted the pair on any host; this keeps the plan's narrower
rule.

Co-authored-by: mb1810 <michael.berger1810@gmail.com>

* G1: ship without a version when no version source is configured

Resolve the version source to four outcomes (valid, absent, ambiguous,
broken) in lib/version-source.ts and read that one signal in
gstack-version-bump (new NO_VERSION state; write refuses to invent a
VERSION), gstack-next-version (no_version, no slot), /ship's bump,
CHANGELOG and title steps, /land-and-deploy's drift check and
/document-release. A configured version file that is missing, empty,
unreadable or malformed stops with its path; 0.0.0.0 is never
substituted. docs/skills.md documents .gstack/version-path.

Migrates two tests that pinned the old fabricated-0.0.0.0 behavior.

Co-authored-by: Koda <koda@edgeviewfinance.com.au>
Co-authored-by: Merit Blake <merit@newsprint.com>

* H #2949: eval credential screen checks every name segment

The hermetic eval env admits GITHUB_* and EVALS_* metadata but rejects
credential-shaped names, judged only by the last underscore segment. A
trailing qualifier carried a credential through: GITHUB_APP_PRIVATE_KEY_BASE64,
GITHUB_TOKEN_1, EVALS_API_KEY_FALLBACK reached eval children. The screen
now rejects a name when any segment is a credential word; segments, not
substrings, so GITHUB_PATH / GITHUB_TOKENIZER / GITHUB_KEYRING stay
metadata, and explicit runner admissions still win.

Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>

* H #2915: cover chunked scanning of large multi-file push diffs

Test-only coverage for behavior already on main: a clean two-file pushed
diff over the engine's 1 MiB cap passes without engine.input_too_large, and
a credential at the end of the second file (a later scan chunk) still
blocks the push.

Co-authored-by: pneumorea-ai <pneumorea@gmail.com>

* A3: keep a project's DATABASE_URL away from a PGLite brain

With a PGLite config, buildGbrainEnv returned the caller env unchanged, so
a project's .env DATABASE_URL (autoloaded by Bun) reached gbrain: a false
broken-db at best, gbrain talking to the app's own Postgres at worst.
Inherited DATABASE_URL and GBRAIN_DATABASE_URL are now blanked for PGLite
configs (GSTACK_RESPECT_ENV_DATABASE_URL=1 still keeps them), and every
gbrain child runs from gbrain's config dir instead of the user's project,
so it cannot reload the project's .env / .env.local.

The old test that pinned pass-through for an engine:pglite config is
migrated: pass-through now applies only to configs that name neither a
database_url nor PGLite.

Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com>

* C9: guard destructive gstack-upgrade and spec fences against unset or wrong paths

Fresh-shell hosts (and Claude Code, which keeps cwd but not shell variables)
ran cd "$INSTALL_DIR" with an empty variable, so git checkout/stash/reset
targeted the user's project. Step 2 now prints an absolute path; every
destructive fence starts with cd -- "${VAR:?...}" || exit 1 and checks it is
gstack's own checkout before any git command. /spec's worktree and spawn
fences get the same guard.

* F4: keep every paid design variant across regenerate rounds

$D variants now reserves a collision-safe round-<N>/ directory per run
(atomic mkdir) and writes a manifest.json of what each round saved. The
paid image request and the local write retry separately: a write failure
retries the write (temp + rename) and, if the path stays unwritable,
keeps the bytes in the temp dir instead of buying the image again.
Responsive variants land in their round directory too.

Readers updated: design-shotgun reserves a round dir and a private temp
dir for its parallel agents and records approved_path/round in
approved.json; design-html resolves approved_path (legacy letters still
work) and finds round-dir variants; design-consultation checks the
printed paths; the gallery shows round-dir variants. Variants that were
already overwritten before this change cannot be recovered.

Migrates the consultation contract test that pinned the flat path.

Co-authored-by: Matt Van Horn <mvanhorn@gmail.com>

* A2: stage dot-leading slugs gbrain can collect; network errors are db-unreachable

A page slug segment starting with a dot (a .claude project) staged a file
gbrain's import walker prunes, so the batch was refused on every run.
Leading dots become "dot-" per segment, and slugs recorded in state under
the old name map through the same function so no page is duplicated.

A DNS or connection failure (ENOTFOUND, EAI_AGAIN, ECONNREFUSED, ...) no
longer classifies as broken-db, which routed users to move a working
config aside. The new db-unreachable status carries the reason
"database host unreachable (<code> <host>); your gbrain config is
unchanged. Fix: check network or VPN, then re-run /sync-gbrain.", skips
local sync stages with that reason, and counts as usable for
gstack-gbrain-detect --is-ok like timeout.

Co-authored-by: Infiniteyieldai <infiniteyield.ai@gmail.com>

* D2: one address classifier for navigation literals, DNS answers, redirects and page-driven navigations (#2811)

classifyAddress() in url-validation.ts blocks 169.254.0.0/16 (metadata and
container credential endpoints), 100.100.100.200, fe80::/10 and fc00::/7,
including IPv4-mapped, IPv4-compatible and NAT64 spellings and decimal,
octal and hex IPv4. validateNavigationUrl, the A/AAAA answer check and
session-persist's cookie-domain guard all use it. BrowserManager now checks
every navigation request (redirect hops, link clicks, scripted navigations):
a blocked target resets the tab to about:blank and the write command fails
with the reason. Connection-time DNS rebinding remains out of scope.

Co-authored-by: Gabriel Rondon <grondon@gmail.com>

* C4: external hosts point at carved sections relative to the installed skill

Publishes the pointer form setup matches: on every non-Claude host a carved
section is `sections/<file>` relative to the installed gstack-<skill> SKILL.md
directory (QA's existing form), never $GSTACK_ROOT/<skill>/sections/, which
does not exist in external runtime roots. No render changes yet: only QA is
carved on external hosts today.

* INV-1: gate outcome contract for outside reviews

classifyOutsideReview() returns separate execution, findings and verdict
fields from text, stderr and exit status; reason codes, anchors and fix
actions live in lib/gate-outcomes.ts. validateOutsideReview() stays as the
text-only wrapper and the two-argument CLI keeps exit 0/1/2 for importers
and installed skills that predate a re-render. The --verdict form (implied
by --stderr/--exit) prints VERDICT/FINDINGS/REASON and exits 0/3/4/1; the
generated outside-voice caller moves to it and branches on each status
(unverified prints OUTSIDE_STATUS: unverified, never completed).

Adds docs/troubleshooting.md with one anchored section per reason code, the
anchor test, and the gate-tail lint (query scripts may not swallow failures,
gate calls may not end in || true, other best-effort tails say why).

* G1: quote the base placeholder in document-release title sync

The title-sync block is checked with bash -n; an unquoted <base> parses
as a redirection. Migrates the test that pinned the old root VERSION
read to the shared classifier call.

* G3: give every logging-bin mktemp a ${TMPDIR:-/tmp} template

Bare mktemp, mktemp -t and literal /tmp templates in the learnings,
question and preference logs (plus jsonl-merge, distill, the two
dashboards, /ship's PR body and /document-release's run dir) now use
"${TMPDIR:-/tmp}/<name>.XXXXXX", the form the INV-3 lint accepts. When
the temp file cannot be created, the learnings, question and preference
logs now say the entry was not recorded and how to fix TMPDIR instead of
exiting with only mktemp's own error. Three tests that pinned the bare
PR_BODY_FILE=$(mktemp) and one that pinned the /tmp run dir migrate.

GNU and BSD mktemp already honor TMPDIR without a template, so the
macOS-sandbox drop in #2952 is unverified here; the template makes the
temp location explicit on every mktemp.

Co-authored-by: Davon Johnson <d.johnson@dayofshow.net>

* B1b: structured outside-review gate fails on P0 as well as P1

validateOutsideReview() derived its structured pass/fail from a [P1] match,
so a review reporting only [P0] findings passed. The wrapper now reads the
classifier's findings verdict (P0 or P1), and the /review and /ship
structured-gate prose requests [P0]-[P3] tags and fails on P0/P1; untagged
output (OUTSIDE_STATUS: unverified) is missing coverage. The ship clarity
pin moves from 'structured P1' to 'structured P0/P1' with the behavior.

* E1: freeze normalizes boundary and target the same way on Windows; hooks spawn through cmd.exe (#2876, #2354)

gstack_hook_normalize_path (careful/bin/hook-extract.sh) maps C:\..., c:/...,
UNC \\server\share (leading // kept) and /cygdrive/c to one form; check-freeze
runs both the saved boundary and file_path through it before the absolute
test, after symlink resolution, and compares case-insensitively on Windows
bash. freeze, guard and careful register hooks as
bash -c "exec bash \"$HOME/...\"" so cmd.exe, which never expands $HOME,
no longer turns the hook into a silent exit 127.

Co-authored-by: Kevin Grasso <kevin@biblingo.com>
Co-authored-by: Xuan-Ce Wang <261591073+xwang4-svg@users.noreply.github.com>

* G4: keep CLI-to-daemon loopback calls off HTTP_PROXY (#494)

The CLI appends 127.0.0.1, localhost and ::1 to the user's NO_PROXY (and
no_proxy), keeping existing entries, before its first fetch. Verified on
Bun 1.4.0: fetch honors NO_PROXY when it is set before the first request
and ignores later changes, so it is set at the top of main().

* F1: bind the plan audit to this branch's plan, never the newest file

Plan File Discovery now prefers an explicit binding (the plan-mode file
or /autoplan ACTIVE_PLAN from the conversation, then a "Plan: <path>"
line in the PR body), then lists candidates: repo-committed
docs/designs/ files this branch changed or that name it, and personal
plan files that name the branch. Candidates are offered, never picked
silently. The repo-name match and the newest-file-of-the-day fallback,
which audited PRs against unrelated plans, are gone. With nothing bound
or chosen it prints the exact line 'Plan completion audit: not run (...).
Fix: add "Plan: <path>" to the PR body, or run /autoplan.'

/ship's parent now binds the plan before dispatching the audit
subagent ({{PLAN_COMPLETION_GATE_SHIP:discovery}}), so the child never
searches. Migrates three tests that pinned the old markers and notice.

Co-authored-by: loulanyue <260355617@qq.com>

* G5: browse type --selector <sel> <text> (#2936)

--selector focuses the element (sel or @ref) and types into it. The bare
form still types its text literally into the focused element; text that
starts with -- goes after a -- separator. When the first word looks like a
CSS selector, the result adds a hint to use --selector without echoing the
typed text, and never refuses based on page contents. Help and command
usage updated.

Co-authored-by: Thomas Reuser <thomasreuser@gmail.com>

* F2: design-doc discovery reads docs/designs, never a root DESIGN.md

A root DESIGN.md is the design system /design-consultation writes, so
preferring it unconditionally reviewed plans against a token file while
printing "Design doc found" and never looked at docs/designs/ feature
docs. Discovery now considers only docs/designs/*.md as the repo-local
design doc (still freshness-compared with the private ~/.gstack copy).
The CEO finding fixture seeds its supplied design as a docs/designs doc
and its test follows.

Co-authored-by: Bit0ps <alex.dreamscaper@gmail.com>

* C4: rename the external carve test (typecheck:test union-order artifact)

tsc prints a union's members in type-creation order; a test file sorting
before ceo-mode-option.test.ts reordered one baseline diagnostic's text.

* C1: every env-var host fence resolves its own gstack root

Codex and the other env-var hosts run each block in a fresh shell, so
GSTACK_ROOT/BIN, B and D set by the preamble were empty in later blocks
(/bin/gstack-slug, B=/browse). scripts/resolvers/runtime-root.ts now owns
root resolution: a valid exported GSTACK_ROOT (bin/ and lib/), else the
per-install literal root (no git call), else repo-local, else the global root
(CODEX_HOME on Codex); no root prints the setup fix line and exits. One
post-render pass inserts it (<=400 bytes) into every fence that uses these
variables without assigning them; B/D come from the same helper as BROWSE
and DESIGN SETUP. Claude output is byte-identical.

Migrated pins of the old preamble lines (gen-skill-docs.test.ts) and a
fixture whose exported GSTACK_ROOT lacked lib/ (skill-positional-literals).

* E8: headless browse honors GSTACK_CHROMIUM_PATH; extensions load in new headless mode (#2771, #1968, #2281)

The headless launch passes GSTACK_CHROMIUM_PATH as executablePath, scopes
the XProtect self-heal out for that bundle, and names the path in a launch
failure with the fix. BROWSE_EXTENSIONS_DIR now launches a persistent
context under the project state dir with --headless=new, so the extension
runs and no window appears (the off-screen window was visible on macOS and
the extension never ran in an isolated newContext). The GPU source guard
test moves from the removed useHeadless flag to launch() vs launchHeaded().

Co-authored-by: oli548 <312288720+oli548@users.noreply.github.com>
Co-authored-by: Slava <slava.istomin@tuta.io>
Co-authored-by: ryanlaiwy <129729476+ryanlaiwy@users.noreply.github.com>
Co-authored-by: 李翊楠 <yinanli1917@gmail.com>

* C2: link design/dist and make-pdf/dist into every host runtime root and the agents sidecar

Codex design and make-pdf skills fall back to $GSTACK_DESIGN and
$GSTACK_MAKE_PDF under the runtime root, but setup linked only browse/dist,
so both preflights reported NOT_AVAILABLE on a fresh install. One helper
links dist/ only (make-pdf/ ships a Claude SKILL.md) into the Codex,
Factory, OpenCode, Cursor, Copilot and Kiro roots, the .agents sidecar and
the Cursor sidecar.

Co-authored-by: Mammad M. <mammad@digit.az>

* C3: install router and nested SKILL.md files as real copies; back up hand edits; --status checks

Codex skips a symlinked SKILL.md file, so the gstack router linked into
~/.codex/skills/gstack was invisible. Every host runtime root (Codex,
Factory, OpenCode, Cursor, Copilot, Kiro) and Kiro's per-skill installs
now get real copies, rewritten on every run (the auto-update path runs
setup --refresh-registered, so it refreshes them too). setup records each
copy's hash in <state root>/skill-copies.tsv; a copy that no longer matches
is saved to a uniquely named backup under <state root>/backups/skill-copies/
before _activate_runtime_root replaces the tree, and a failed backup keeps
the old tree. setup prints what it refreshed or removed.

./setup --status now prints, per registered install, whether the router is a
real file and whether every section pointer resolves, with the fix command.

Test migration: setup-codex-model pinned the Kiro router/upgrade links as
_link_or_copy; C3 deliberately changes them to copies.

* INV-3: install-level host conformance checks (no symlinked SKILL.md, dist present, section pointers resolve)

For codex, kiro, factory, opencode, cursor and copilot, the real-setup
conformance kit now fails when an installed SKILL.md is a symlink, when
design/dist or make-pdf/dist is missing from the runtime root, or when a
'Read `.../sections/<f>.md`' pointer in an installed SKILL.md does not
resolve (bare pointers in carved skills, relative-to-installed-skill
pointers, and $GSTACK_ROOT/~/absolute/../ pointers everywhere), and
./setup --status must report 'all resolve'. No new setup runs.

Expected red until the generator lane lands: design-html's install-offer
pointer and plan-eng-review's review-sections pointer name
$GSTACK_ROOT/<skill>/sections/... on every external host.

* B11: auto-update tracks pull, setup and migrations; announces only on success; retries with backoff

gstack-session-update wrote the 'just upgraded' marker after SETUP_FAILED or
SETUP_SKIPPED bun_missing, and re-ran setup only when HEAD moved, so a failed
install was announced as an upgrade and never retried. Stages are now
persisted in <state root>/session-update-pending; the marker is written only
when every stage succeeded; a pending setup or migrations stage is retried
even with HEAD unchanged after the update-check interval, then 6h, then 24h;
and each session start prints one line naming the stage, the reason and the
repair command while it is pending. Best-effort tails carry a same-line
reason.

* D1: gstack-config key browse_extension_id for forks and self-built extensions

The browse daemon trusts one Chrome extension ID on /extension-token and the
terminal-agent /ws. The override now lives only in gstack-config (never an
env var a project .env could set): empty by default (gstack's published
extension), a 32-letter a-p Chrome ID when set, "" clears it, and anything
else is rejected with the stored value kept. The browse lane reads it.

gstack-config-defaults: browse/src counts as a reader directory; the key is
listed prose-only until the browse lane's reader lands (the stale-entry check
then asks for the entry's removal).

* G2: key the sticky slug cache on the git common dir

A linked worktree used to resolve its slug fresh from the remote while
the main checkout kept its sticky pre-remote identity, so learnings,
checkpoints and timelines split into two buckets. gstack-slug and its
TS twin in lib/bin-context.ts now key the cache on the git common dir
(read from .git files, no git spawn, so cache hits stay git-free) and
read entries in one deterministic order: the repository entry, then the
main checkout's pre-upgrade entry, then this directory's own. Both the
repository and per-path entries are written.

When a worktree's own old entry named a different bucket that still
holds data, that slug is recorded in projects/<slug>/.former-slugs and
reported by --get FORMER_SLUGS. New --adopt-legacy --from <slug> merges
it: JSONL logs append missing lines with de-duplication, missing files
copy, differing files are listed as conflicts and never overwritten,
the source bucket is kept, and it prints migrated/pending/conflicting
counts; reruns are idempotent. --adopt-legacy --dismiss <slug> drops
the reminder.

Co-authored-by: loulanyue <260355617@qq.com>

* C5: OpenCode /gstack-* commands load the skill by its frontmatter name

OpenCode's skill tool knows gstack-review as `review` (its frontmatter
name), and OpenCode's builtin /review shadows that name as a command. The
managed /gstack-review command told the agent to load `gstack-review`,
a name the skill tool does not list. Every managed command now names the
frontmatter name. OpenCode's AskUserQuestion/ExitPlanMode mapping (#2759)
already shipped in v1.91.16.0.

Test migration: the #2629 commands test pinned the directory name.

Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com>

* E4: Windows node-server bundle keeps sharp and socks external; CLI resolves its server script lazily (#2260, #2439)

build-node-server.sh passes --external sharp and --external socks (Bun on
Windows refuses to inline them; on Linux they were silently inlined). The
CLI no longer resolves server.ts or server-node.mjs at module load:
resolveServerLaunch() runs when a server starts, so a Windows minimal
runtime with only server-node.mjs works and a missing script is reported
only when one is needed.

Co-authored-by: Spooks444 <193448029+Spooks444@users.noreply.github.com>
Co-authored-by: punksterlabs <77217235+punksterlabs@users.noreply.github.com>

* B1: Codex reviews whose sandbox could not start are unavailable, not a pass

In containers without unprivileged user namespaces every Codex command
fails, yet the review exits 0 and concludes 'no findings' (captured in
test/fixtures/codex-sandbox/). The validator now takes execution evidence:
'codex exec --json' command events and the 'codex review' stderr transcript
(a command that succeeded proves execution); sandbox failure lines on
stderr or in failed command output return unavailable(sandbox_unavailable);
exact 'could not run commands / read the diff' phrases are the last
fallback. Review text that merely discusses bwrap or namespaces still passes.

A free 'codex sandbox ... true' preflight (Linux, 10 s) runs before the paid
model probe and every automatic outside review; CODEX_MODE gains
sandbox_unavailable, and an inconclusive model probe reports unverified
instead of ready (exit code kept for stale callers). Every Codex site
(outside-voice resolver, /codex review, challenge, consult) takes its sandbox
from _GSTACK_CODEX_SANDBOX and pipes its result through the validator; a
static test parses rendered command positions for both. GSTACK_CODEX_NO_SANDBOX=1
(exactly 1, shell env only) switches to danger-full-access with a warning on
every use. Sandbox pins in existing tests move from the literal read-only to
the selected-sandbox variable; captured autoplan probes are judged against
the probe block they executed.

* C6: gstack-relink discovers skills structurally, so /browse survives a skill_prefix toggle

relink skipped a hardcoded list of support-directory names that included
browse, a real skill, so toggling skill_prefix removed /browse. A top-level
directory is now a skill exactly when it carries a SKILL.md.

Co-authored-by: Jonathan Zhang <topcoder1@Mac-Studio.local>

* C4: carve ship and plan-ceo-review on every host; enforce the 160 KB byte gate

The generated ship skill was 202-241 KB on all 10 non-Claude hosts (and
Factory plan-ceo-review 160,039 bytes), over the 160,000-byte ceiling,
because section carving ran only for Claude. Both skills now carve on every
host, pointing at sections relative to the installed skill directory (literal
$GSTACK_ROOT/<skill>/sections refs are rewritten the same way). The ceiling
is measured in UTF-8 bytes and fails generation in repo mode; under
--install-root it warns so setup never leaves a user without skills.

qa-lazy-sections.test.ts pinned ship as inline-on-external-hosts; migrated to
the carved policy and kept the inline check on review (Claude-only carve).

* B5: learnings and timeline reads fail loudly when bun is missing

gstack-learnings-search and gstack-timeline-read ended in
'2>/dev/null || exit 0', so a missing bun or a crashed embedded script
printed nothing and exited 0, identical to 'nothing recorded'. Both now
exit 127 with '<script>: bun not found on PATH, so ... could not be
read. Fix: install Bun (https://bun.sh), then re-run ./setup.' and let
an embedded-script failure exit non-zero with its stderr. A successful
run with no matches still exits 0 with empty output. The generated
callers that print 'LEARNINGS: unavailable (...)' belong to the
generator lane.

Co-authored-by: loulanyue <260355617@qq.com>

* B2: a retired-model 404 reports MODEL_UNUSABLE, not CODEX_MODE: ready

The model probe matched only 400 / 'is not supported', so a retired model's
404 ('does not exist or you do not have access to it') and a custom
provider's bare 404 fell through to the inconclusive branch and reported
ready. Both now classify as MODEL_UNUSABLE with a hint naming the two causes
(retired model, or a wrong base_url) and log codex_model_retired; a
'requires a newer version of Codex' 400 points at upgrading the CLI and logs
codex_cli_outdated. Ported from #2843 onto the current probe.

Co-authored-by: Marek Cermak <marek.cermak@strv.com>

* B6: rank learnings by matched query tokens and disclose truncation

A more specific query used to drop the exact match: the token-OR filter
recalled every entry sharing one token, the sort used confidence alone,
and the default --limit cut silently. Entries now rank by how many
distinct query tokens they match, then confidence, then recency, and a
query that matched more than fit says '-- N more matched, raise --limit
to see them'. The hit count lives in a local map so a field stored on
disk can never become a sort key, and the type grouping uses a
null-prototype map so a row typed 'constructor' or '__proto__' cannot
crash the run. Ranking code ported from #2796; regression vectors
adapted from #2799 (its stemmer is not needed).

Co-authored-by: loulanyue <260355617@qq.com>
Co-authored-by: y$un_ <forrest.sun527@gmail.com>

* C7: required team mode also blocks Copilot CLI's lowercase skill tool

Copilot CLI applies Claude-format matchers by tool name; its skill tool is
`skill`, which no Claude tool name maps to, so the 'Skill' matcher never
fired and Copilot ran protected skills without gstack. The hook entry now
matches 'Skill|skill', an entry an older team-init wrote is upgraded in
place, and the deny is emitted in both decision formats (Copilot reads the
top-level permissionDecision, Claude Code reads hookSpecificOutput) with
exit 0 so both parse it.

Test migration: the settings test pinned matcher 'Skill'.

Co-authored-by: neallee <neallee@gmail.com>

* B3: Codex auth probe accepts custom providers' env_key credentials

The auth probe only knew CODEX_API_KEY, OPENAI_API_KEY and auth.json, so a
working self-hosted or OpenAI-compatible provider configured with
[model_providers.<id>] env_key = "X" reported AUTH_FAILED and /codex
stopped. The probe now reads env_key from model_providers tables only
(comments ignored, the name must be a shell identifier before it is
dereferenced) and accepts a set variable; when none is set it names the
missing key on stderr. Ported from #2192 with the table scoping added.

Co-authored-by: hardy <932821449@qq.com>

* E8: keep the extensions profile under the state dir at a path lock cleanup accepts

cleanSingletonLocks only cleans directories named chromium-profile, so the
BROWSE_EXTENSIONS_DIR profile moves to <state>/extensions/chromium-profile.

* B10: plan-tune never reports CALIBRATED with zero signal events

Stats printed CALIBRATED: true from logged-answer counts alone, while
every answer could miss SIGNAL_MAP and leave each dimension at its 0.5
seed (#2623). --derive now records inferred.signal_events, the answers
that actually moved a dimension; calibration requires at least one, and
the summary prints 'not calibrated: no recorded signals' verbatim when
there are none (or says the count is unknown for a profile derived
before this change). The psychographic redesign stays out of scope.

* A1: verify gbrain landing before stamping transcripts, plus --reconcile

gbrain import exits 0 while skipping pages (returned per-file failures,
malformed filenames, managed-import Pending receipts), and its failure
ledger exists only for git import dirs, so a skipped page was stamped
ingested and never retried (field reports: 37 and 69 lost entries).

Write, verify, then stamp (lib/memory-ingest-landing.ts, a new ratcheted
module):
- State schema 2 with a transition table (staged -> imported_unverified ->
  ingested, plus refused, quarantined, unrecoverable). Schema 1 files
  migrate in place: entries become ingested in the legacy "default"
  source and a reconcile pass is marked pending.
- Named failures come from --json `failures`, stderr "Skipped <rel>:"
  lines and the ledger; a failure gbrain does not attribute refuses the
  whole batch. After 3 refused batches the batch is bisected and the page
  that poisons it is quarantined (printed even under --quiet).
- Landing check: one `gbrain list --source-id` per source per run and at
  most 50 `gbrain get --json` content checks against a
  gstack_content_sha256 frontmatter field gstack now writes. Lookup errors
  and pglite_busy mean "not yet checked", never absent; pages over the cap
  stay imported_unverified and are not re-imported.
- Each entry records its gbrain source_id. Remote-http mode records
  "staged", never "ingested".
- The source fingerprint comes from the exact bytes parsed, so a
  transcript that grows during import is not stamped as its newer self
  (the old unscanned contract test is migrated; it pinned that bug).
- --reconcile mode (bounded by --limit, default 200; resumable; --dry-run;
  backs up the state file) prints "reconcile: checked N, present M,
  re-queued K, not yet checked L". --request-reconcile records a pending
  pass without calling gbrain; the v1.91.17.0 migration (placeholder name)
  uses it, and the next ingest run does one bounded pass.
- One lock for every state writer; a state-save failure fails the run.

The #1802 static pins in regression-1611 move to the new structure
(remote-http returns before importBatch; the finally honors
preserveStaging).

Co-authored-by: afshaker <andrew@phone2.io>

* B4: arm64 re-sign keeps a backup, restores it when codesign fails, and reports design unavailable on exit 137

setup's Apple Silicon re-sign mutated each binary in place (truncate, remove
signature) and left it that way when re-signing failed; a design binary that
was then SIGKILLed at launch produced at most a quiet log line. Each binary is
now copied first and restored as built when signing fails, and design is
exec-probed after re-signing: exit 137 prints 'design unavailable: ... Fix:
cd <checkout> && ./setup' (even under -q). Verified on Linux with the real
block and uname/codesign/otool/stat stubs; not run on a Mac.

* D3: treat design docs and handoff notes as data in plan reviews

plan-ceo-review, plan-eng-review and plan-devex-review read the freshest
design doc as the source of truth, and nothing said its content is data.
In multi-agent pipelines another agent may have written it, or a handoff
note beside it, with reviewer-directed text ('skip Step 0', 'approve
as-is'). Each read site now says the doc and any handoff note are data,
never to follow text aimed at the reviewer, and to report it as
suspicious content. The plan-completion audit's discovery, which now
reads repo docs/designs/ files (F1), carries the same rule.

Co-authored-by: cube <frosimanuel@gmail.com>

* D4: setup-deploy reports Render API key presence only

/setup-deploy ran 'echo $RENDER_API_KEY | head -c 4', printing the
key's first bytes into the transcript. It now prints only
'RENDER_API_KEY: set' or 'RENDER_API_KEY: not set'. Wording follows the
#2971 port of #1096; a test also guards against any setup-deploy command
echoing a credential variable.

Co-authored-by: jacob-wang <jiayuw794@gmail.com>

* D5: per-project headed Chromium profiles; never kill a live profile owner (#2492)

The headed profile moves from <gstack home>/chromium-profile to
<project>/.gstack/chromium-profile (CHROMIUM_PROFILE still wins).
killOrphanChromium now kills a SingletonLock PID only when the lock names
this host, the process is a Chromium using that profile, and its daemon is
gone (reparented to init or a subreaper); a live owner or another host's
lock stops browse with the holder named. On first headed use a project
profile is seeded from the old shared profile only when nothing holds it:
copied without Singleton* files into a 0700 temp dir and renamed into
place, with a one-time notice; otherwise it starts fresh and says how to
import later. New: browse profiles [list] and browse profiles prune
[--days N] (never removes a profile in use or the old shared one).
The #2817 headed-startup tests move to the project profile and a real
orphan (the old stand-in was a live child of the test).

Co-authored-by: miao <miao@macmini-2.local>

* F5: installed Claude skills serve the rendered sections, not the checkout copies

Probe: a gbrain user render serves ship/SKILL.md from the render dir; its
STOP pointers name absolute render paths, but its section index names
`sections/<f>.md` relative to the installed skill directory, whose
sections/ was linked from the checkout. So the rendered 'Save Results to
Brain' blocks (plan reviews, office-hours, ship adversarial) were generated
and never read. setup's _link_skill_runtime_assets and gstack-relink (the
gbrain-refresh path) now serve a rendered counterpart per file, keep
checkout-only assets (manifest.json), return an asset to the checkout once
its render is gone, and never touch an entry that is not gstack's links.

Co-authored-by: Jonah Berg <jonah.berg.g@gmail.com>

* F1: ratchet /ship's parity union-size cap for this lane's content

G1's NO_VERSION arms, F1's parent-side plan binding and D3's data line
add 2,126 bytes to /ship's skeleton-plus-sections union, taking it from
1.4022x to 1.4135x of the v1.64.1.0 baseline (cap was 1.405). The cap
moves to 1.414 with the measurement recorded, as earlier growth did; no
other invariant changes.

* E5: feed every Codex prompt on stdin instead of argv

Prompts went to Codex as one argv string ('codex exec "$(cat file)"'), which
fails over 32 KB on Windows (and over 128 KB on Linux, E2BIG) and mangles
quotes. Every codex exec site (outside-voice resolver, /codex review custom
instructions, challenge, consult new and resume) now runs 'codex exec -'
with the prepared prompt file on stdin; challenge and consult write the
prompt with a quoted heredoc so it is never shell-parsed. The bash-native
timeout watchdog (stock macOS) started the command in the background, which
gives it /dev/null as stdin; it now passes the caller's stdin through.

The 'byte for byte' dispatch test drops its old argv trailing-LF exception,
the consultation and autoplan fixtures follow the stdin form, and the static
site test requires stdin at every rendered codex exec.

* G6: prettyscreenshot --hide keeps a trailing image path as the output path (#1419)

--hide collected every following non-flag argument as a selector, so the
output path was hidden instead of written to. A trailing argument that looks
like a path or ends in .png/.jpg/.jpeg/.webp is now the output path.

Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>

* INV-3: render-level host conformance and generated-bash lint on every host

One render per host checks every fence of every SKILL.md and section file:
bash -n with angle-bracket placeholders normalized; the env-var host prelude
and its B/D consumers run under env -i and set -u with stub binaries (no
empty value, no /bin/, /browse, /design or /gstack- path); and a quote-aware
lint (test/helpers/generated-bash-lint.ts) fails on a ~/ word inside double
quotes, mktemp without a TMPDIR-respecting template, and cd/rm -r/mv/git -C
on a variable the fence never assigned unless written ${VAR:?...} (cd must
also handle failure). Negative controls plant each failure.

Templated the bare mktemp calls in generator-owned resolvers and the upgrade
fence; migrated the two tests that pinned the bare form.

* G6: analytics averages quoted duration_s values (#1728)

gstack-skill-end writes duration_s as a quoted string, and the per-skill
average stripped everything from the leading quote, so every average
read 0s. The awk now drops a leading quote before extracting digits.

Co-authored-by: Ryan Alberts <ryan.a.alberts@gmail.com>

* G6: gstack-ios-qa-daemon --help prints usage instead of starting the daemon (#1932)

Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com>

* G1: tighten /ship's NO_VERSION wording to fit the union-size cap

Same behavior in fewer bytes: the NO_VERSION arm, the broken-source
exit, the unprefixed-title rule, the Step 20 null and the CHANGELOG
skip. The cap ratchet commit measured this wording.

* G6: rejected taste preferences gain confidence (#1777)

bumpPref computed every entry's confidence from approved_count, so a
rejected entry, which never gains approvals, stayed at 0: show ranked
rejections by insertion order and the taste-drift warning could not
fire from real use. Confidence now uses the entry's own bucket count.
Also salvaged by Garry's #2971.

Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>

* E2: Windows /cso build disables only C5105, and a failed MSVC compile is not reported as a missing toolchain

The Windows SDK's winbase.h raises C5105, which /W4 /WX promotes to error
C2220, so every MSVC build of the native /cso helper failed, and setup
mapped every probe failure to 'install Visual Studio 2022 Build Tools'.
Both compile lines add /wd5105 (only that diagnostic), and setup keeps the
probe's output: a probe that found MSVC but failed to compile reports
windows-msvc-compile with the compiler's first error. Not run on Windows:
the mapping test stubs powershell.exe; the ps1 assertion runs everywhere.

Co-authored-by: AlinValentin7 <AlinValentin7@users.noreply.github.com>

* E6: gbrain-refresh hands the detection file to Python on stdin

Under Git Bash, a native Windows Python first on PATH cannot open an MSYS
/c/... path, so gbrain-refresh always summarized the detection as
'local-status: unknown' and brain-aware blocks were suppressed. bash now
opens the file and Python reads stdin; the grep fallback without Python is
unchanged. Verified on Linux with a stub Python that refuses path opens; not
run on Windows.

Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com>

* G6: context-save computes session duration on Linux (#2704)

The duration step parsed 'ps -o lstart=' with BSD-only 'date -jf', so on
Linux session_duration_s was always unknown. It now falls back to GNU
'date -d', and never parses an empty start (GNU reads that as midnight).
Ported onto the current block instead of #2704's ~/.gstack/sessions
mtime, which bypassed the state root.

Co-authored-by: Evgenii Lopatin <e75533@gmail.com>

* Q2: one-time notice naming the Codex provider and account for outside reviews

Outside reviews are on by default and use whatever Codex login exists
(#965). The first automatic Codex review on a machine now prints one
non-blocking notice: the provider (config.toml model_provider, else openai),
where the credential comes from (CODEX_API_KEY, OPENAI_API_KEY, the ChatGPT
login or the key saved by codex login; never the value), that reviews stay
on, and how to turn them off (gstack-config set codex_reviews disabled). A
marker in the gstack state root keeps it to once per machine. Tests that run
the generated command now use private state roots. Also shortens the
no-sandbox provenance line.

* Q1: validate browser output paths the way the kernel resolves them (#2971 salvage)

validateOutputPath walks the destination one component at a time, resolving
each existing component before the next, so link/../x, a dangling symlink
and a dangling symlinked parent can no longer route a write outside the
safe directories, while a new multi-level directory inside them is allowed.
scrape validates its manifest and every downloaded file, not only --dir.
Ported from PR #2971 onto current code.

* G6: retro test census counts non-JS suites, skips generated output

The census recognized only JS/TS-style names, so pytest and minitest
(test_*.py|rb), XCTest (Tests/ dirs, FooTests.swift), Terraform
(*.tftest.hcl) and Bats (*.bats) suites counted as zero, and a committed
build directory counted as authored code (and its Test/ subdirs as test
LOC). is_test now knows those conventions and never treats prose as a
test, generated output is skipped in the numstat pass, TEST_FILES_CHANGED
uses is_test, and TEST_FILES_TOTAL uses the same filename conventions
minus generated paths. Merges #2812, #2809 and #2037's patterns (#2037's
retro template edits target a block the metrics bin replaced).

Co-authored-by: Jino <dgfrty00@gmail.com>
Co-authored-by: snig-17 <santosh.m.tiwari@gmail.com>
Co-authored-by: Sholto McNeilage <sholto@nodenarrative.com.au>

* Q3: setup stops auto-installing the AskUserQuestion preference hook in Conductor and removes one it added

The PreToolUse question-preference hook breaks Conductor's native
AskUserQuestion round-trip even when it only defers (#2207), and setup
auto-installed it on every Conductor workspace setup. Now a Conductor setup
without an explicit opt-in never installs it and removes an existing one
(the question-log and AUQ-error hooks stay), printing how to keep it
(gstack-config set plan_tune_hooks yes). An explicit opt-in still installs
it. gstack-settings-hook remove-source gains --event to scope the removal.

Test migration: setup-plan-tune-hooks-noninteractive pinned the retired
Conductor auto-opt-in.

Co-authored-by: Phil Hie <philhie@outlook.com>

* E5: watchdog stdin test feeds the wrapper through a redirect

Callers run '_gstack_codex_timeout_wrapper ... codex exec - < file'. Bash keeps
stdin for a backgrounded command inside a pipeline but not behind a
redirect, so the pipe form passed on the old watchdog; the redirect form
reproduces the lost prompt.

* F4: type the variant-round fetch stub like the existing stubs

The test-typecheck ratchet flagged the zero-argument stub cast to
typeof fetch; it now takes the fetch arguments like
variants-retry-after's stub. Test behavior unchanged.

* A4: import transcripts into per-repository, non-federated gbrain sources

`gbrain import` ran without --source-id, so transcripts from every project
landed in the shared (often federated) default source.

- Each transcript partition imports with --source-id into its originating
  repository's own source (gstack-transcripts-<repo>, built with
  constrainSourceId, now exported from lib/gbrain-sources.ts). The source
  is registered machine-local and --no-federated before its first import,
  and the registration is cached in state. Partitions import one after
  another (PGLite is a single writer); a failing source no longer blocks
  the others, except a timeout, whose checkpoint must survive.
- If gbrain cannot register the source, that partition stays local and
  unstamped, and the run says why (even under --quiet).
- --include-unattributed sends repo-less transcripts only to the
  machine-local gstack-transcripts-unattributed source, keeps them on the
  machine when the brain is remote (Postgres, Supabase or remote-HTTP),
  and never writes them to the publishable ~/.gstack/transcripts staging.
- A page's recorded source is sticky: pages stamped before this release
  ("default") keep importing there, so no source gains a duplicate.
- The staging dir name carries -src-<id> so a resumed import keeps its
  source.

Test fakes that recorded only the last import now record every import of
one run, and answer `gbrain sources`; remote-http scan tests use an
attributed transcript, since unattributed ones no longer stage there.

Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>

* F3: load the taste profile, and report a failed slug lookup

The taste-profile probe double-quoted ~/.claude/.../gstack-slug, so the tilde
never expanded and /design-consultation and /design-shotgun always printed
NO_TASTE_PROFILE. Use quoteSafePath(); stop hiding gstack-slug's stderr and
print TASTE_PROFILE_UNAVAILABLE with the fix when the slug cannot be resolved.

* B4: DESIGN_READY only when the design binary actually starts

DESIGN SETUP (and office-hours' visual exploration) printed DESIGN_READY for
any executable binary, so one SIGKILLed at launch by an invalid macOS code
signature still passed. Launch it under the portable 10-second deadline
(gtimeout, timeout, or perl alarm); otherwise print DESIGN_NOT_AVAILABLE with
the exit code, 'timed out after 10s', or on 137 the code-signature hint and
'Fix: cd <checkout> && ./setup'. A success is cached by inode and mtime under
the state root so Gatekeeper's first-launch scan is paid once.

The probe launches the binary with no arguments (usage, exit 0): the design
CLI has no --version command today.

* C2: name the runtime root every way a generated preflight may read it in the dist test

The generator lane's C1 resolves D from $GSTACK_ROOT instead of
$GSTACK_DESIGN, so the test now exports GSTACK_ROOT as well; it still fails
on main (no dist in the roots) and passes with and without the C1 change.

* A5: brain-sync stage fails loud when the artifacts source indexed zero pages

The stage reported "curated artifacts pushed" whenever gstack-brain-sync
exited 0, but a git push is not gbrain indexing. After a successful push
the stage now reads the maintained artifacts source's page count
(GSTACK_BRAIN_SOURCE_ID, else the source whose path is the artifacts
worktree or state root): zero pages is ERR with the fix command; a
missing source or an unavailable count says so instead of claiming
indexing.

Co-authored-by: Jimmy Zeisweiss <4261258+jzeisweiss@users.noreply.github.com>

* B1: state the effective read-only sandbox in /codex review prose

The codex parity invariant requires the skill to state sandbox_mode="read-only";
the command now takes it from _GSTACK_CODEX_SANDBOX, so the prose names the
effective default explicitly.

* A6: report deleted-worktree gbrain sources; prune only with proof, on request

A deleted Conductor worktree leaves its gstack-code-<hash> source behind
and gbrain's autopilot fails on it forever. Local absence is not proof a
source is dead (machines sharing a federated brain register their own
paths, and its pages may be the only copy of uncommitted work), so gstack
never removes one automatically.

- Each sync counts consecutive misses for gstack code sources whose path
  is missing, prints "path unavailable" once per source with the review
  command, and records which repository each synced source belongs to.
- New gstack-gbrain-sync --prune-gone-worktrees (with --dry-run, whose
  plan it always prints first) removes a source only when its path was
  missing on two consecutive syncs, its parent is readable, recomputing
  deriveCodeSourceId() for that path on this host yields its id, and the
  repository's git common dir still exists and no longer lists the
  worktree. It re-checks the path right before removing (through the
  #1734-guarded safeSourcesRemove) and logs each removal.

* D0: pass the Bun child flags after 'run' so wrappers that rewrite 'bun run' argv keep working

bun run --no-env-file --config=/dev/null <script> has the same effect
(verified on Bun 1.4.0) and keeps argv[1] === 'run', which the
publication-lock fixture's preload injection relies on. The sidebar-tabs
source guard now expects the flags in the terminal-agent spawn.

* E8: count the BROWSE_EXTENSIONS_DIR persistent launch in the signal-flags guard

The new launch site carries handleSIGINT/SIGTERM/SIGHUP false like the
other three, so the guard's expected count moves from 3 to 4.

* D2: give the memory-leak reproducer's fake request isNavigationRequest()

wirePageEvents now runs the navigation guard on every request event; the
fake request adapter supplies the method real Playwright requests have.

* A7: run only the resolve_symbol_edges dream phase, detected first

/sync-gbrain --dream and the --full auto-build ran gbrain's full dream
cycle (~35 minutes, LLM phases) when the call graph needs only the
resolve_symbol_edges phase. The stage now runs
gbrain dream [--source <id>] --phase resolve_symbol_edges when the
installed gbrain accepts that phase (detected by behavior: gbrain
validates --phase before printing --help). Otherwise it skips the forced
dream and says the full cycle costs about 35 minutes and how to run it,
instead of starting it.

Co-authored-by: liutiming <timingliu@outlook.com>

* D0: keep the dotenv isolation test runnable on the Windows lane

The compiled probe gets its .exe name on Windows, and the terminal-agent
case (owner identity comes from ps) is skipped there.

* B9: read cycle_freshness from a doctor run that includes it

/sync-gbrain Step 3.5 and the --full auto-dream decision read CYCLE from
gbrain doctor --json --fast, which skips the DB checks that carry
cycle_freshness, so the answer was always "unknown". Both now read
gbrain doctor --json --scope=brain (DB checks, no skill-file walk), parse
its JSON even when doctor exits 1 for a failing check, and match the
issue that names this source: "never completed" is never, "last cycled
Nh ago" is completed. When the installed gbrain does not report the
check, they say "unknown: installed gbrain does not expose
cycle_freshness" (lib readCycleStatus carries the reason to the --full
SKIP row).

The source-probe fixture that pinned the --fast call and a made-up
message format now answers --scope=brain with gbrain's real message.

* B4: compact the launch probe (cache keyed on inode, valid while newer than the binary)

Keeps design-consultation inside its parity size budget.

* C4: migrate the ship docs-dispatch test to the carved external ship

On Codex and Factory the documentation dispatch now lives in
sections/documentation.md behind a pointer relative to the installed skill,
like Claude; the test pinned the old inlined layout.

* B5: learnings callers report a failed search instead of reading as empty

Every generated gstack-learnings-search caller ended in 2>/dev/null || true,
so a missing bun or a failed search looked like 'nothing recorded'. Callers
now keep stdout, capture stderr and the exit status, and print
'LEARNINGS: unavailable (<first stderr line>)' on failure (resolver callers
in learnings.ts and review-army.ts, plus investigate, qa and ship's
adversarial section).

* F1: ship and review count unverified/unavailable outside reviews as missing coverage

INV-1 consumers in this lane's templates: /ship's readiness note, its
PR body Pre-Landing Review section and /review's completion rule now say
an outside review reporting unverified or unavailable is missing
coverage, never a pass. The PR body's Plan Completion section prints
F1's not-run line instead of the old 'No plan file detected.' The /ship
union-size cap moves to 1.415 for these lines (measured 1.4148).

* A8: rename the artifacts repo with OWNER/REPO, rewrite the remote only after it

The v1.27.0.0 migration passed a bare repo name to gh repo rename / gh
repo edit, which always fails (gh wants [HOST/]OWNER/REPO), then step 2
rewrote ~/.gstack-artifacts-remote.txt to the never-renamed repository.

- Qualify both names with the owner from the recorded remote URL (else
  the signed-in gh user) and surface gh's error.
- Step 2 moves the remote file but rewrites the URL only once the rename
  is confirmed; a later successful retry rewrites it then.
- A pending journal now counts as state to migrate, so the #1383 retry
  actually re-runs step 1 (it used to see no legacy file and declare the
  migration done).
- The v1.91.17.0 migration (placeholder name) repairs installs already
  hit: when the remote names gstack-artifacts-<user> that does not exist
  but gstack-brain-<user> does, it restores the old URL in the remote
  file and ~/.gstack's origin and says so. It only calls gh for such a
  remote and never fails the upgrade.

Co-authored-by: Brandon Pugsley <aibpaem@gmail.com>

* B8: read gbrain JSON with bun instead of jq in install and wireup

gstack-gbrain-install read package.json with jq but never checked for it,
so on a machine without jq a successful install failed with "cannot read
version". gstack-gbrain-source-wireup hard-failed without jq ("brew
install jq" is not actionable on git-bash), so the wireup was skipped on
every run. Both now read the JSON with bun, which they already require.

Co-authored-by: benjamin beres <benjamin.beres@bienpreter.com>

* B4: shorter probe messages (design-consultation parity budget)

* B5: shorter capture variable names (parity budget)

* E7: find the Aside CLI in ~/.local/bin when it is not on PATH (#2902)

The installer puts aside in ~/.local/bin, which is not on the default macOS
login PATH, so the probe said NEEDS_ASIDE ('download Aside') for a healthy
install. The probe now falls back to ~/.local/bin/aside, drives the resolved
binary, and prints READY: <path> when it is off PATH so later blocks run that
path. Ported from PR #2965 onto the current _gs_d probe.

Co-authored-by: Som Samantray <som.samantray@gmail.com>

* A2: key the dot-slug catalog entry on the message users actually saw

* E1: cmd.exe-safe investigate freeze hook

On Windows Claude Code runs hook commands through cmd.exe, which cannot parse
the single-quoted bash -c argument, so /investigate's scope-lock hook never
ran. Use one double-quoted bash -c script that names the hook path directly
(no S= variable: POSIX sh would expand $S to empty and skip the hook).

* F4: design resolver reads per-round variant paths

$D variants now writes $_DESIGN_DIR/round-<N>/variant-*.png and prints JSON
with roundDir, manifest and paths. The mockup flow and comparison-board loops
build the board from those printed paths instead of hardcoded
variant-{A,B,C}.png, and approved.json records approved_path and round so a
round-1 choice made after round 2 still resolves.

* E4: generators tolerate Bun's spurious EEXIST on Windows (#2329)

Bun on Windows throws EEXIST from a recursive mkdir of an existing directory,
so gen:skill-docs and the llms.txt writer crashed once a second file landed
in a directory. Both now use lib/fs-utils' mkdirpSync (ported from PR #2329
onto the existing helper instead of a new scripts/fs-utils.ts).

Co-authored-by: Vaughan <vaughan@cairnsitguy.com.au>

* C7: keep the required-mode hook's exit-2 deny

Copilot CLI treats a preToolUse exit 2 as deny (its hooks reference), and
#2413 established exit 2 plus hookSpecificOutput as the shape Claude Code
enforces, so the deny output stays as on main; only the matcher and the
in-place upgrade of older 'Skill' entries change.

* C3: guard Kiro's router copies on a present source; give the QA pointer adapter the copy and dist helpers

A copy fails where a symlink dangled, so the Kiro router and upgrade copies
are written only when their render exists (as every other runtime root
already does). test/qa-lazy-sections' extracted Kiro installer now supplies
_link_runtime_dists (C2) and the SKILL.md copy helpers (C3).

* Q3: office-hours asks open-ended questions in a Q<N> prose form (#2719)

In Conductor, and whenever AskUserQuestion fails, prose is the only channel,
and the prose fallback defined only the D<N> decision brief. Phase 2A/2B
diagnostic questions have no option set, so the model dropped the phase.
Office-hours' preamble now defines the Q<N> open-question form (question,
why I'm asking, what a strong answer sounds like) and routes free-text
replies to it; the diagnostic sections name it. Scoped to office-hours so
every other skill's preamble is unchanged. Ported from PR #2729.

Co-authored-by: Bharadwaj Mittapelli <bharadwajmittapelli@gmail.com>

* A2: render db-unreachable like timeout

gbrain's new db-unreachable status is a transient network failure; a setup
run while offline must not strip brain-aware blocks from every skill.

* A2: treat db-unreachable like timeout in gbrain-refresh

The memory lane's new db-unreachable status (network or DNS failure, config
intact) is transient, so gbrain-refresh keeps rendering brain-aware blocks
for it exactly as it does for timeout instead of suppressing them.

* C8: a render given disabled skills drops them from the router (#3018)

The root router named every skill and ends 'When in doubt, invoke the skill',
so it routed to skills the user disabled. gen-skill-docs takes
--disabled-skills (runGeneration disabledSkills): the router drops their
routing rules, keeps the other side of an 'A or B' rule, and names them as
disabled. The router and gstack-upgrade cannot be disabled. With no disabled
skills the render is byte-identical.

* G2/G3/INV-1: skill-start former-bucket notice and visible learnings failure; TMPDIR mktemp in gbrain bins; best-effort reasons

* C9: Step 2 falls back to the install registry's source checkout (DX-7)

Codex users who installed from the README's ~/gstack clone got 'ERROR:
gstack not found' from /gstack-upgrade: Step 2 only looked under host skill
directories. It now falls back to the source checkout the install registry
recorded for the current host.

* B4: design --version prints the version, and the probe uses it

The design CLI treated --version as an unknown command (exit 1), so the
probe launched the binary with no arguments instead. Add --version (prints
the gstack version, exit 0, before auth or network) and probe with it, so
messages read '<path> --version exited <code>' as planned.

* C4: no external render points at a section file that is not installed

design-html's detector install offer and plan-eng-review's self-check pointed
at $GSTACK_ROOT/<skill>/sections/<file> on every external host, where neither
skill is carved and no section tree exists. design-html now inlines the offer
off Claude, and a skill's pointer to its own {{SECTION}}-inlined section
names the inlined text.

* C1: skillify finds the browse SDK under its own host's install root

The template hardcoded ~/.claude/skills/gstack for the canonical
browse-client.ts, so external hosts read another host's install or nothing.
A new INSTALLED_ROOT resolver (sibling of INSTALLED_BIN_DIR) names the host's
global root, or the per-install root when setup renders for one.

* C4: rewrite section refs on hosts whose root stays a literal ~/<globalRoot>

Kiro, OpenCode, Cursor and Copilot render ~/.claude/skills/gstack as their own
literal root, not $GSTACK_ROOT, so plan-eng-review's self-check pointer
escaped the rewrite (found by the setup lane's installed-pointer check).

* Integrator: outside-voice fences use the shared runtime prelude; TMPDIR mktemp and per-round variant paths in design templates

* Integrator: C8 router omits disabled skills on every render (relink renders the served Claude router in place); Windows-safe E4/E6 entries; investigate hooks under the cmd.exe check; browse_extension_id reader; order-insensitive test typecheck identities

tsc printed one unchanged ceo-mode-option diagnostic's object members in a
different order (content?/isError?) once lib/fs-utils joined the generator's
import graph; identities now sort object-type members, and old baselines
normalize on read.

* Q1 (#2971 salvage): bound iOS proxy responses

One wall-clock deadline covers headers and body (stalled and dribbling bodies
time out), the whole body is capped at 64 MiB (upstream_response_too_large),
every terminal path clears its timer, removes listeners and destroys the
request, and the daemon threads the deadline through both listeners. Ambiguous
mutations are still never replayed after a timeout or abort. Ported from
#2971's proxy-transport work plus the size bound.

* I1: grade the AUQ matrix's (recommended) marker on option labels, the field the auto-decide hook reads

The decision-brief format puts a Pros / cons block with 'A) ... (recommended)'
in the question text and the same marker on the option label. The matrix
counted both and failed all six skills in periodic run 37151477069 with
'expected exactly one (recommended) option, found 2'. The question-preference
hook reads labels only, so the gate now checks the native question: a
Recommendation line in the question text and exactly one label ending in
(recommended). Fixtures move to the native question shape and add the real
spec capture plus a marker-only-in-text negative control.

* Q1: type the proxy-transport test doubles (typecheck:test ratchet)

* INV-1: gate outcome rows and troubleshooting sections for every lane's new messages

* Release v1.91.19.0 (next free patch in the queue: #3022 holds 1.91.17.0, #3023 holds 1.91.18.0); migration renamed; engines.bun >=1.4.0 to match the CI pin (#2815)

* v1.91.19.0 docs and metadata: CHANGELOG, TODOS, README troubleshooting/upgrade/type --selector, BROWSER.md type and profiles, AGENTS.md conventions, CONTRIBUTING #2949, touchfiles for the wave's new lib/bin files

* I1: give hermetic Codex runs a temp dir beside CODEX_HOME so Codex can create its PATH aliases

Codex 0.160 refuses to create its helper aliases (codex-linux-sandbox among
them) when CODEX_HOME sits under its temp dir, and the runner put the
hermetic home under the shard TMPDIR. Periodic run 37151477069 shows the
warning and 'Unable to spawn codex-linux-sandbox ... No viable candidates
found in PATH' for every exec_command in codex-sol-scope-termination.

* I1: Codex evals fail a review whose commands never ran and stop reading tool errors as skill-load problems

validateCodexReview accepted codex-review-findings in periodic run 37151477069
although its only command failed in bwrap ('I couldn't run the review'); it
now requires execution evidence from classifyOutsideReview (B1's INV-1
classifier). codex-sol-scope-termination failed on 'apply_patch verification
failed: invalid hunk' because the skill-load check matched any 'invalid';
it now matches Codex 0.160's skill-loader diagnostics only.

* I1: run paid eval containers with seccomp unconfined so Codex's sandbox can start

Docker's default seccomp profile blocks the user namespaces bubblewrap needs,
so every Codex command in a paid eval failed with 'bwrap: No permissions to
create a new namespace' (codex-review-findings, run 37151477069). Reproduced
in the CI image locally: default profile fails, seccomp=unconfined runs the
command. The runner stays non-root.

* I1: shared-libs plan actor reads one exclusion grammar for scope lists

Periodic run 37151477069 refused 'Existing-caller migration and helper
hardening stay out of scope' as a hardening proposal: the actor had two
partial exclusion rules (unanchored is/are/remains outside, and an anchored
list that only stays unchanged), the third refusal at this boundary after
36633323521 and 36776104571. One anchored predicate now covers a bare list
of scope nouns (hyphenated compounds included) with is/are/stays/remains and
unchanged/untouched/excluded/out of scope/outside/not part; any other verb
in the clause is still read as a proposal. Adds the real capture and two
adversarial controls.

* Parity caps: measured v1.91.19.0 union/skeleton sizes for ship, review, design-consultation, office-hours and design-shotgun, with the reason beside each

* Regenerate generated output for v1.91.19.0 (gen:skill-docs --host all, goldens, llms.txt, context-budget fixture)

* I1: plan-eng-review writes the Test Plan Artifact even while test choices are pending

Periodic run 37151477069 (plan-eng-review-artifact): a non-interactive review
recorded every Test review choice as pending and never wrote the QA test
plan, because Step 5 said to write the artifact 'when these test and eval
choices are resolved' while the artifact section says to list unresolved
choices as pending. Both now say to write it after the decision gate, with
unanswered choices under Pending Decisions. The pinned sentence becomes a
meaning-level check.

* I3: plan-eng-review states when findings first enter the report and that the scope menu's recommendation needs no repo probe

Judge rationales for 'plan-eng-review/SKILL.md sections' (actionability 3.67
in periodic run 37151477069, clarity 3 in 36385945043 and 35567915613) cite
two concrete tensions: 'do not add findings before Scope Challenge C' versus
the ledger saves C itself performs, and 'Recommendation: A when a branch diff
exists' versus 'do not probe for session state'. The write policy now says
findings first enter through Scope Challenge C's ledger saves; the selector
says its Recommendation line advises the user and needs no repository
inspection. The pinned sentence becomes the new meaning-level regex.

* I3: plan-ceo-review defines the closing sequence's cleanup step

Two of three judge samples for 'plan-ceo-review/SKILL.md modes' in periodic
run 37151477069 (actionability at the 4.0 gate) flag 'perform permitted
cleanup' as never defined. The only cleanup the skill has is archiving old
CEO plans, which needs approval; step 5 now says so.

* I3: /qa-only's value-card rule names only its own card location

qa-only rendered the /qa line 'Put it in the 8e.5 record (/qa) or under each
proposed test (/qa-only)'; judge samples for 'qa-only/SKILL.md workflow'
(actionability at the 4.0 gate in run 37151477069, 3.67 in 36907895813)
flag the /qa-only-irrelevant 8e.5 record. The qa-mode bar now renders the
invoking skill's location; both skills get shorter.

* test-of-test ratchet: a repo script or setup read by path counts as product code

codex-invocation-sites (runs scripts/gen-skill-docs.ts), gstack-config-gbrain-refresh
(copies bin/<name> through a variable) and setup-cso-windows-probe (reads setup and
scripts/build-cso-windows.ps1) exercise product code the detector could not see.

* I1: DX floor actor strips the editor hint from the footer line only (from #2971)

The custom-field reply normalization removed 'ctrl+g to edit in <editor>'
from every line, so question text that resembled the menu footer could be
rewritten and still authenticate. Only the last non-empty line is the
footer. Ported from Garry's #2971 with its negative control.

* I4: attribute every census testcase; deselections never count as SKIPPED

The paid report counted every skipped JUnit testcase as SKIPPED and every
testcase whose name was not a registry id as unattributed. On the 2026-10-03
census (run 37151477069) that was SKIPPED 82 / unattributed 72 (periodic) and
SKIPPED 282 / unattributed 24 (gate), and every case-shard sibling also wrote a
skipped history record that reset its pass-rate series.

- attributeJUnitCase: a name is its registry id or CASE_TEST_NAMES label, else
  the only id its file registers, else the keyless file itself. Skipped
  other-tier cases, case-shard siblings, isolated or unselected cases and Bun's
  '(unnamed)' hook placeholders are zero-credit deselections with a reason and
  write no history record. Anything else is listed by name as unattributed.
- One result and one history record per case per shard; a genuinely skipped
  selected case counts as SKIPPED and is listed with its reason, also in
  report-summary.md and collector-outcomes.json. SKIPPED panels show their
  reason and skipped trials are no longer called assertions.
- CASE_TEST_NAMES labels for the three /cso cases.
- tierSkipReason: a computed registration whose lane-tier ids are all
  registered to other test files is excluded at plan time, so the four gate
  and two periodic files that ran with every test skipped no longer take a
  runner.

Census rerun: periodic SKIPPED 0 / unattributed 0, gate SKIPPED 0 /
unattributed 0; verdicts unchanged.

* I5: split the deploy file per case; guard PR-lane shard walls at 600 s

test/skill-e2e-deploy.test.ts ran 668 s as one gate shard in the 2026-10-03
census (six sequential cases, 51-201 s each). It is now case-sharded, so each
case is its own shard with its own (smaller) supervision budget; no timeout
or budget was raised.

- CASE_SHARDED_FILES gains the deploy file (names already equal ids).
- Gate duration seeds for the six case keys from the census JUnit walls, so
  packing stays at 12 gate-census slices (CI cap 230m -> 200m).
- shardDurationViolations + PR_LANE_SHARD_LIMIT_MS: a free test fails when a
  planned gate shard's recorded wall (committed seed, and the last gate census
  in test/fixtures/paid-report-census/gate-census-durations.json) exceeds 600 s.
- Tests that pinned deploy as one shard follow the split (cookie planner keys,
  reuse fixture key, release/PR floors).

* TODOS: /review fingerprint under Codex's read-only .git sandbox (found by the live Codex check)

* Free-suite repairs from the first integrated Ubicloud run

Product:
- Outside-voice fences carry runtimeRootPrelude(ctx) explicitly again, so the
  resolver output runs on its own (the post-render pass skips fences that
  already assign GSTACK_ROOT); outsideVoiceRuntime stays deleted.
- D5: an explicit CHROMIUM_PROFILE (gbrowser's per-workspace gbd) is never
  seeded from the shared profile or registered for `browse profiles prune`;
  this was also the free suite's write to the real ~/.gstack/browse-profiles.json.
- gstack-ios-qa-daemon: BASH_COMPAT=50 for its >512-byte --help heredoc.

Tests migrated to the wave's deliberate contract changes:
- B1 heading admits `unverified`; sandbox flag is ${_GSTACK_CODEX_SANDBOX:?};
  verdict-form output (VERDICT/FINDINGS/REASON); fake codex follows the
  preflight + stdin + -o form; Q2 notice marked shown in a private state root.
- C1: an exported root needs bin/ and lib/; a lone GSTACK_BIN is not honored.
- C4: plan-ceo-review is carved on every host (carrier, discovery and
  methodology fixtures read sections/ when the render has it).
- E7: READY prints the resolved Aside path.
- INV-3: the cookie-approval reconstruction reverses the bun-install mktemp template.
- F1's `Plan:` binding read gets a reasoned tracker-guard exemption.
- Browse stubs stop their server instead of scheduling process.exit; the
  headless launch check only counts browsers its own launch started.

* Regenerate generated output after the outside-voice prelude repair

* I1: revert the seccomp-only eval container option; it does not let Codex's sandbox start

Run 37158847998 on this branch and a stock Ubicloud VM both show that
seccomp=unconfined only moves bubblewrap's failure from 'No permissions to
create a new namespace' to 'Failed to make / slave: Permission denied'.
Codex's sandbox started only with the host sysctl
kernel.apparmor_restrict_unprivileged_userns=0 plus seccomp and AppArmor
unconfined on the container, which a container job cannot set from its
steps. The relaxation bought nothing, so it goes; the fix is filed as a
TODO for the integrator.

This reverts commit ec97187.

* I1: a Codex review eval needs a completed command, not just review-shaped text

Run 37158847998 recorded codex-review-findings as passed again: the agent
wrote 'I couldn't run the review because the execution sandbox failed
(bwrap: Failed to make / slave)', the failure never reached stderr or a
command event, and no phrase rule matched. validateCodexReview now also
requires at least one command_execution event that completed with exit 0.
Fixtures gain that event; the CI output is a new negative control.

* Evals lane follow-ups: close the skillify gate TODO (I2 green), file its open reds, CHANGELOG lines; refresh measured parity annotations

* Regenerate generated output after merging wave/evals

* D2: bound the navigation guard's tab reset (commit, 5 s)

Ubicloud run 3: a click to a link-local link reported the block, but the
reset's page.goto('about:blank') waited the full 30 s default navigation
timeout, so the command hung past the test deadline and the stale block
leaked into the next command. The reset now waits for commit only, with a
5 s bound.

* I1: run Codex paid slices on a host-run job where bubblewrap can start

Docker's default seccomp/AppArmor profiles and the Ubuntu 24.04 host's
kernel.apparmor_restrict_unprivileged_userns=1 keep Codex's bubblewrap
sandbox from starting in a job container, so every Codex command failed.
The planner now packs Codex shards into their own slices (plan.codexSlices,
validated against the entries); evals-periodic runs those slices on a new
host-run job that lifts the host restriction and runs the same CI image
with seccomp and AppArmor unconfined. Artifact names match eval-slices,
so the report reconciles one manifest. Gate plans have no Codex slices.

* I1: the /codex review eval passes only when Codex executed

Run 37158847998 recorded codex-review as passed while the sandbox
preflight said unavailable and no review ran: the case only checked the
session exit and an optional output file. It now requires the shared
validator's executed verdict (findings, clean or unverified) in the
codex command's own output and a written, non-fail-closed GATE.

* TODOS: characterize the free-suite timing reds from Ubicloud run 4

* I1: QA evidence names the probes to rerun after an input change, before the one-shot materialize

ship-exploratory-late-input returned inconclusive in gate census 37158847998:
the fixture locale changed after captures 001 and 002, the agent reran only
input 9, and materialize (which publishes once) recorded capture 001 as open.
Its next line then said to rerun what is open, which the published verdict no
longer allowed. Every capture now lists the commands that observed an older
input snapshot and have no rerun on current inputs (revalidate, plus the next
line), and materialize's next line says the verdict is final.

* I1: the eng batching counter reads records under "Section N — <review> review" headings

plan-eng-multi-finding-batching failed in 6 of 12 runs with reviewCount 0-1
(or a timeout with every ask counted as Step 0). In run 37158847998 the agent
asked 11 separate questions, each with a saved ledger record, but filed
records R2-R10 under '## Section 1 — Architecture review'. The counter strips
'Section 1' but not the dash separator, so it rejected every record outside
the Decision ledger and the floor never filled. Quoted, historical and
unrelated headings still do not count.

* I1: plan-eng-review-artifact names its report file without making it the only output

Runs 37151477069 and 37158847998 wrote the whole review, including the
GSTACK REVIEW REPORT, into review-output.md and never ran the Test Plan
Artifact block. #3017 made the artifact a hard assertion and dropped the
prompt's explicit artifact instruction, leaving 'Write your review to
review-output.md' as the one named destination. The prompt now names that
file as the selected report file and says every other output keeps the
destination the skill gives it, without naming the artifact.

* browse: close returns when Chromium exits; Xvfb cleanup does not wait on its own zombie

BrowserManager.close() detached every 'disconnected' listener before
browser.close(). That also removed Playwright's internal listener that
resolves close(), so every launched-mode close waited the full 5 s race
timeout (measured 5,000 ms on origin/main and on this branch; raw Playwright
closes in ~50 ms), and Playwright 1.62 has no Browser.process() for the
SIGKILL fallback. The daemon's SIGINT shutdown therefore took over 5 s, which
reddened the CLI lifecycle test under load (1 of 8 on main) and left browsers
alive at shard cleanup. gstack now detaches only its own listeners: close
takes ~50 ms.

cleanupXvfb treated a terminated Xvfb child as alive until our own blocked
event loop reaped it, so every close waited a full second then sent SIGKILL
to a zombie. A zombie now counts as exited; the display test counts only
running Xvfb processes.

* I1: office-hours-brain-writeback reads the long skill with Read and skips the preamble

4 of 18 trials ended error_max_turns at the unchanged 12-turn budget with a
gbrain search but no gbrain put. The failing trial in 37151477069 read the
1260-line rendered SKILL.md with cat, got truncated output, spent four more
turns re-reading it with sed, then ran the preamble's skill-start block. The
passing trials skipped the preamble. The prompt now says to read the skill
with the Read tool and skip the preamble, onboarding, telemetry and
contributor sections, as the plan-eng fixtures already do. Budget unchanged.

* CI: no credential-shaped URLs in added lines (quality scan); release floor measured on the merged tree

The quality job's credential scan of the PR diff flagged 7 db.url_with_password
lines: three docs lines now describe the login instead of spelling a URL, and
three tests build the URL from parts. paid-retry-supervision's periodic release
floor is the merged tree's measured 35,711 (main's value; the evals lane's
33,821 predates main's new paid files).

* qa-evidence: hold the stdout-pipe cases on a FIFO, not on a paused child stream

Ubicloud run 4 finished the blocked-forever case in 71 ms (it should take the
5 s write deadline): the 2 MB write was not held, because saturation depended
on how the runtime buffers a paused child pipe. On POSIX the child's stdout is
now a FIFO the test reads only when it chooses to, so the kernel holds the
write; Windows keeps the paused pipe.

* TODOS/CHANGELOG: the three run-4 timing reds are fixed at their causes (daemon close latency, FIFO-held pipe tests)

* I1: a QA helper call bun could not load is checked at the installed path, not failed outright

review-exploratory-small-cli failed gate census 37162480720 with 'command
outside declared caller observation interface': the actor dropped one
character from the shard directory in a gstack-qa-evidence capture path, bun
refused to load it ('Module not found', exit 1), and the next call ran the
same capture correctly. A failed call whose only output is bun's refusal for
a gstack-qa-evidence/deadline path is now held to the interface as the same
command at the installed helper path, so a compound or undeclared command
still fails.

* I1: plan-eng-review's final self-check names its three outputs

The fixture-prompt repair did not hold: in 37162480720 plan-eng-review-artifact
again wrote one review-output.md, ran the skeleton's read-back gate on it and
stopped without the QA Test Plan artifact (no gstack-paths/gstack-slug block).
The transcript shows the agent's last check before finishing is the skeleton's
Section self-check, which said only 'and outputs'. It now names the saved
report, the QA Test Plan artifact and the Review Log, each at its own
destination or presented as not persisted (the existing write policy).

* I1: revert the plan-eng-review self-check wording; it did not change the artifact outcome

A local run of plan-eng-review-artifact in the CI image (Claude Code 2.1.284)
at e2f65ab22 still wrote one review-output.md in 8 turns and ran no Bash, so
the QA Test Plan artifact block never ran. Keep the skill text unchanged; the
case is deferred with its evidence.

* I1: seed the Codex files' periodic durations from their first executing run

Every Codex file recorded ~1 s because its commands could not start in the
container (codex-e2e*, outside-voice, workflow). Run 37162480720 executed them
on the host-run job; its slice 24 walls replace the placeholders so the
planner packs the Codex slice by real work (7 files, ~4.9 min estimated).

* Refresh scripts/free-test-durations.json (bun run test:ubicloud --record-durations; covers the wave's new test files)

* evals2 follow-ups: outside-voice-evidence helper in five case touchfiles; close the evals lane's Codex-CI, late-input, batching and brain-writeback TODOs (run 37162480720); CHANGELOG lines

* fix(browse): take the next free X display when a concurrent allocator wins the race

Pick-then-spawn let two allocators (parallel daemons or test shards) choose
the same free display; the losing Xvfb exited and the handoff failed (run 6,
handoff 'failure before launch'). spawnFreeXvfb now treats a display taken
during startup as a collision and moves on. Regression test forces the race
with a delayed Xvfb wrapper (pre-fix: fails; two-process repro 13/15 -> 0/15).

* test(ci): read the host-run Codex eval job's docker env, mounts and uploads

eval-codex-slices runs on the host and passes its executor env through docker
-e flags and HOME through a $RUNNER_TEMP/eval-home mount. The evidence and
coordination tests now map those back to the container view instead of
assuming job.container, and the job's native-capture upload carries the
qa-callers paths like the other executors.

* I1: plan-eng-review-artifact runs the real interactive review in a PTY, replacing the composing fixture

The claude -p case told the model to skip the preamble and questions and go
straight to the review. In 37151477069, 37158847998, 37162480720 and a local
CI-image run it wrote the whole review into one file without executing the
workflow, so it measured composing, not /plan-eng-review. The interactive
batching PTY run in 37158847998 did write the artifact during Test review, so
there is no product defect.

New test/skill-e2e-plan-eng-artifact.test.ts keeps the case id, tier
(periodic), kind (rule) and budget (CAPTURE_LONG_MS): it drives the real
/plan-eng-review over the same dashboard plan and sources, answers each
question with its first option, stops once the QA test plan exists or the
report completes, and asserts the same requirement (exactly one new
eng-review test plan in the run's own state root, about the dashboard change).
The composing case is retired from skill-e2e-plan.test.ts in this commit.
runPlanSkillCounting reports engTestPlans (read before fixture cleanup) and
passes them to isCollectionComplete. One paid run in the CI image (Claude Code
2.1.284): pass, collection_complete at 498 s with one test plan.

The new file takes its own periodic runner (seeded at 498 s), so the periodic
census plans 26 slices: the planner's --max-parallel and both executors' caps
move from 25 to 26 so every slice still starts at once; the paid-file census
pin moves from 106 to 107.

* evals2 final: plan-eng-review-artifact touchfiles point at its PTY file and helpers; plan file wall drops the moved case

Applies the lane's touchfiles-data patch (its outside-voice-evidence hunks
were already in 9a96fb3). skill-e2e-plan.test.ts lost one 600 s case to
skill-e2e-plan-eng-artifact.test.ts, so its periodic wall is two 900 s + two
600 s (3,000,000 + reserve) and its timeout pin lists the remaining cases.

* TODOS/CHANGELOG: close plan-eng-review-artifact (real PTY review, 498 s pass)

* test: plan-eng-review-artifact is a PTY runner consumer (selected by claude-pty-runner and pty-screen changes)

* test(windows): spawn cmd.exe hooks verbatim, use a .invalid UNC host, and match CRLF fixtures and native separators

windows-free-tests reds on 364f0f3, each verified on windows-latest
(46 pass, 1 POSIX-only skip, 0 fail):
- investigate-freeze-path cmd.exe cases: Bun MSVC-quoted the command line
  cmd.exe parses, so cmd saw \"bash and failed; Node's shell spawn (what
  Claude Code uses) passes it verbatim.
- investigate-freeze-path UNC cases: a single-label host made each
  filesystem probe wait on network name resolution (cd 11 s, -L 2.8 s);
  a reserved .invalid host fails at once (55-80 ms).
- paid-report-census: the junit fixture is CRLF in a Windows checkout.
- codex-invocation-sites: rendered paths use backslashes on Windows.

* eval(plan-design-with-ui): the Step 0D focus title may spell the seven dimensions as a word

Run 37166586458 slice 5 (dfaf154) asked 'D2 - Review all seven design
dimensions, or focus on specific areas?' and reached its review finding,
but the focus predicate only accepted the numeral, so the case failed with
no acknowledged Design focus. Both recognizers accept 7 or seven; the
byte-exact capture is a free regression, and engineering/other-count/
no-'design' titles stay rejected.

* eval(ship-docsync): git check-ignore is a declared read in the docs observation interface

Run 37166586458 slice 2 (dfaf154): ship-docsync-completion's parent ran
'git check-ignore -v .qa-state' to verify the prompt's claim that the
observer probe is Git-excluded. The read was outside the closed Git list,
so the run's DOC_PATH replacement was unprovable and failed. check-ignore
cannot write; it joins the list and the interface prose. Free regression
replays the captured command against the fixture and keeps composition and
redirects rejected.

* fix(browse): read Xvfb's own startup error to classify a lost display race

Run 10 (1ffd38e) failed handoff's concurrent promotion once: 'Xvfb on :100
exited during startup (code 1)' with the display free again by the exit
check, so a holder that won the lock and closed within the 100 ms poll
looked like a broken install. Startup stderr now goes to a private file;
'Server is already active for display' is a collision and moves on, and any
other startup exit carries Xvfb's fatal line. Regression: a stub that
reports the collision for one display and exits leaves spawnFreeXvfb on the
next display (pre-fix: the misleading install error).

* test: merged-main fixtures (E6 copies the Claude render library gstack-config now sources; the census fixture's plan-mode-no-op budget follows its v1.91.18.0 policy)

* qa: materialize runs once per PROBE_DIR, and qa-only says §1 reuses the REPORT_DIR it resolved

Run 37170610789 slice 3 (feaa28d): the qa-only workflow judge scored
clarity 3.33 and named two ambiguities: §4 said materialize runs once per
report root while mixed runs materialize each surface's PROBE_DIR, and
SKILL.md deferred exploratory §1 without saying §1 reuses the REPORT_DIR
resolved under Prepare Report Artifacts. A local run of the same judge on
the repaired text: clarity 3.67, completeness 4.33, actionability 4 (pass);
all three samples call the workflow internally consistent.

* Regenerate generated output for the qa materialize/REPORT_DIR wording

* eval: three detector defects from run 37170610789 (feaa28d)

- safety-codex-consult-embed: the fake codex recorded argv only, but every
  codex exec takes its prompt on stdin since E5; it records stdin for the
  '-' form and the check reads both.
- ship-docsync-missing-asset: 'ls ... 2>&1' on a read-only command is the
  same harmless stderr wrapper as '2>/dev/null'; stdout redirects, pipes
  and script calls stay rejected.
- qa-functional-webhook-fix: a pre-repair capture whose command reran on
  the repaired inputs may be classified superseded (exploratory §4); the
  evidence check accepts that and still rejects superseded on a post-repair
  capture or one never rerun.
Each free regression fails without its repair.

* fix(test:free): a browser record removed mid-read by a shutting-down daemon is absent

Run 13 (feaa28d) needed the flaky-retry for 'owned detached browser
settlement > cancel': 2 unattributed failures instead of 1. A 16-vCPU
stress run reproduced it once in 64 with the cause:
'browser ownership unavailable'. On SIGINT the cancelled daemon removes its
state file; record() saw it exist, then lstat/read hit ENOENT, which
failure() reports as an ownership error. ENOENT there now means absent.
New mode cancel-vanishing-record forces the removal between the check and
the read (pre-fix: 2 unattributed, deterministic; now 1).

* test(windows): the 2>&1 wrapper regression uses relative reads (no native paths in literal commands)

* test(plan-seed): wait for the fixture CLI's slash event instead of a fixed 50 ms

Run 14 (b2f729b) needed the flaky-retry for 'startup-placeholder': the
events log had paste/enter/end_turn but not yet the slash 50 ms after it
was sent. The check now polls (bounded 5 s); the deadline assertion
measures when submission settled, which is what it bounds. One test
typecheck diagnostic goes away with the typed reader.

* eval(plan-design-with-ui): recognize the Step 0D focus question by meaning, not one title spelling

The b2f729b gate run titled it "I've rated this plan 5/10 on design
completeness. Review all 7 dimensions, or focus on specific areas?", the
third native wording to miss a spelling-pinned regex. The native form is
now a class: a title that offers all seven (numeral or word) dimensions or
passes versus a focus and names design. Captured titles from 37166586458
and this run pass; other counts, engineering passes, titles without design
or without a focus stay rejected.

* eval(shared-libs-read-only): curl answers the provider rate-limit probe with the same 404 as gh

Gate census 37174266054 slice 9: the model probed API availability with
curl -o /dev/null https://api.github.com/rate_limit (GET, discarded). The
gh transport already answers rate_limit as an unknown endpoint (404); the
curl transport refused it, and the refusal counted as a read-only
violation. curl now gets the same 404. POST, file output, other repos and
raw hosts stay refused. Paid rerun (gate, 1 trial): PASS in 177 s.

* eval(shared-libs-plan-callers): an exclusion may combine states (unchanged/out of scope)

Periodic census 37174266054: every option ended 'Scheduler semantics,
existing callers and helper hardening stay unchanged/out of scope', which
the actor read as proposing hardening. Exclusion states may now be joined
by '/', 'or' or 'and'; the captured question is answered, and clauses that
pair a state with a real change ('stay unchanged/harden the helper') are
still refused. Paid rerun (periodic, 1 trial): PASS in 296 s.

* paid: --case plan-eng-review-artifact selects its PTY test by its registered name

A local --case run matched 0 tests (no spend): the PTY test's Bun name is
not its case id, and CASE_TEST_NAMES had no label for it.

* eval(plan-eng-review-artifact): periodic checkpoint resumes at Test review; the fresh full run moves to marathon

A fresh interactive /plan-eng-review cannot reach Test review inside the
600 s periodic budget: startup plus one-at-a-time Scope Challenge questions
took 595 s with six questions and no Test review in census 37174266054 and
in a local run (one earlier fresh run passed at 498 s). Per the eval-speed
policy, the long run moves to the non-blocking marathon lane and the
blocking case becomes a focused checkpoint.

- plan-eng-review-artifact (periodic, 600 s unchanged): resumes the real
  interactive skill from a saved review record: the plan, the Scope
  Challenge result, Sections 1-2 complete, and a Decision ledger rebuilt
  from that census run's six actual native questions and answers
  (test/fixtures/plan-eng-artifact-resume-37174266054.json). The live skill
  runs Section 3, Test review. Assertion unchanged: exactly one new
  eng-review test plan in the run's own state root, about the dashboard
  change. Paid trial (local, Claude Code 2.1.284): PASS, 364 s.
- plan-eng-review-artifact-full (marathon, PTY_LONG_MS): the fresh
  start-to-finish run with the same assertion; budget measured beside it.
- test/plan-eng-resume.test.ts proves the rebuilt record is a valid resume
  point and rejects missing, unanswered, edited or duplicated records.
- Registry: touchfiles, tiers, kinds, test-name labels, durations, paid
  file count 115, PTY consumer list; CHANGELOG and TODOS.

* setup-deploy: say which deploy fields count as confirmed, how to resolve the default branch, and group the detection tests

Periodic census 37176837432: the setup-deploy workflow judge's actionability
mean was 3.67 (4, 4, 3). The low sample named the Fly/Heroku/Railway/GitHub
Actions deploy trigger left undefined until Step 4's 'not already confirmed'
catch-all and an unstated way to find <default branch>, and misread
'A || B && C' detection lines. Step 4 now defines a confirmed field and names
the paths that leave the trigger unset, <default branch> resolves with
gh repo view, and the two OR'd detection tests are grouped explicitly (same
behavior). One judge run on the repaired text: 4/4/4, pass.

* Regenerate generated output for the setup-deploy wording

* TODOS: census data for the skipped CEO mode handoff (mode-routing, auto-decide-preserved); floor runner Edit-permission parse under collapsed TTY spaces

* fix(browse): a blocked navigation that fails at once leaves the tab blank and the next command clean

PR CI on 8d2d269 (free-suite 3): the redirect to [::ffff:169.254.170.2]
failed immediately on the runner, so its chrome-error page committed around
the guard's about:blank reset, and leaving the error page committed a
second about:blank after the reset resolved, interrupting the next three
tests' gotos. The guard now lets the blocked request settle (bounded at
1 s; a hanging target is still cut off by the reset), resets, and waits for
the main frame to stay quiet before returning. Regression: a redirect to a
blocked address on Chromium's unsafe port 1 fails at once (pre-fix: blank
check and the next goto fail, deterministically).

* qa: the checkpoint note is created for browser runs and composed for functional ones

Periodic census 37178143007: the qa-only workflow judge scored
actionability 3.67 again; one sample read step 2's 'Create
exploration-NNN.json' as applying to functional runs before reaching
'Functional: do not write this file'. The sentence now names both surfaces
up front; byte-neutral (the browser line drops its restated clause), qa
union 1.1098 under its 1.110 cap. One judge run on the repaired text:
3.67/4/4, pass.

* Regenerate generated output for the qa checkpoint wording

* eval(section-loading): byte-range prints of a section count as a complete read

Periodic census 37178143007: plan-ceo-section-loading printed
review-sections.md in five byte ranges (head -c 16000, then tail -c +N |
head -c 17000, one-byte overlaps) and the detector, which matched whole
printed lines, saw the lines split at each range edge as missing. Outputs
of the commands that name the section are also joined on their overlap.
Regression: the captured ranges count; dropping a middle range or the last
range does not (pre-fix: the complete ranges were refused).

* test(plan-design-review): count design terms the review wrote into the plan

* fix(aside): probe names the platform (GSTACK_PLATFORM override) so the macOS pitch never re-probes the OS

* fix(plan-ceo-review): Step 0E mode handoff runs gstack-ceo-mode-handoff so the selected mode is printed, not only composed

* chore: refresh context-budget fixture

* test(auto-decide): accept Step 0E's documented AUTO_DECIDE line with its decisions suffix; replay the captured trial

* docs: CHANGELOG and TODOS for the CEO mode handoff and platform line

* test(plan-design-review): plan-term credit needs a rated summary; adapter regression for census 37179171083

* test(auto-decide): the printed handoff may precede Step 0E's provenance log; replay census 37182865432; TODOS for two rotating census reds

* test(plan-ceo-section-loading): read the skill's ledger and currentDecision records for the stale-fill decision; replay three census reports

* test: $B counts as a browser call only in command position; qa functional prompt defines its classification values

* test(shared-libs-plan-callers): a verbless unchanged-scope list is an exclusion; replay census 37186854666; TODOS for two census reds

* fix(settings-hook): retry a lock released between mkdir and the existence check instead of dropping the mutation

* test(ship-docsync): the child's reply to a parent SendMessage counts as its spawned-marker report; replay PR run 37188668421

* test(browse): Xvfb stub-failure tests allocate from ranges no other allocator uses

* perf(ship): documentation gate snapshots and compares its candidate with gstack-docs-candidate

* fix(review): unverified Codex mode runs the passes in the mode summary too; stale-fill decision check accepts any ledger row ID

* test(shared-libs-plan-callers): judge scope by forbidden verb+object actions, not an exclusion grammar; replay six refusals and four passing censuses

* test(shared-libs-plan-callers): type the brief-line parameter

* test(ship-docsync): an inline-code status quote is not a false current claim; replay PR run 37196811165

* fix(review,ship): Codex challenge and structured-review bodies run in unverified mode, matching their headers

* docs(TODOS): overlay-harness control-arm error and format-approach provider refusals, with run links

* docs(TODOS): qa-only workflow judge data point

---------

Co-authored-by: Lewis <lewispeel@hotmail.com>
Co-authored-by: Yugz <yannduzelier@gmail.com>
Co-authored-by: Florian <florian.cleyetmerle@gmail.com>
Co-authored-by: cdecook23 <decook23@gmail.com>
Co-authored-by: mb1810 <michael.berger1810@gmail.com>
Co-authored-by: Koda <koda@edgeviewfinance.com.au>
Co-authored-by: Merit Blake <merit@newsprint.com>
Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>
Co-authored-by: pneumorea-ai <pneumorea@gmail.com>
Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com>
Co-authored-by: Matt Van Horn <mvanhorn@gmail.com>
Co-authored-by: Infiniteyieldai <infiniteyield.ai@gmail.com>
Co-authored-by: Gabriel Rondon <grondon@gmail.com>
Co-authored-by: Davon Johnson <d.johnson@dayofshow.net>
Co-authored-by: Kevin Grasso <kevin@biblingo.com>
Co-authored-by: Xuan-Ce Wang <261591073+xwang4-svg@users.noreply.github.com>
Co-authored-by: loulanyue <260355617@qq.com>
Co-authored-by: Thomas Reuser <thomasreuser@gmail.com>
Co-authored-by: Bit0ps <alex.dreamscaper@gmail.com>
Co-authored-by: oli548 <312288720+oli548@users.noreply.github.com>
Co-authored-by: Slava <slava.istomin@tuta.io>
Co-authored-by: ryanlaiwy <129729476+ryanlaiwy@users.noreply.github.com>
Co-authored-by: 李翊楠 <yinanli1917@gmail.com>
Co-authored-by: Mammad M. <mammad@digit.az>
Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com>
Co-authored-by: Spooks444 <193448029+Spooks444@users.noreply.github.com>
Co-authored-by: punksterlabs <77217235+punksterlabs@users.noreply.github.com>
Co-authored-by: Jonathan Zhang <topcoder1@Mac-Studio.local>
Co-authored-by: Marek Cermak <marek.cermak@strv.com>
Co-authored-by: y$un_ <forrest.sun527@gmail.com>
Co-authored-by: neallee <neallee@gmail.com>
Co-authored-by: hardy <932821449@qq.com>
Co-authored-by: afshaker <andrew@phone2.io>
Co-authored-by: cube <frosimanuel@gmail.com>
Co-authored-by: jacob-wang <jiayuw794@gmail.com>
Co-authored-by: miao <miao@macmini-2.local>
Co-authored-by: Jonah Berg <jonah.berg.g@gmail.com>
Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>
Co-authored-by: Ryan Alberts <ryan.a.alberts@gmail.com>
Co-authored-by: AlinValentin7 <AlinValentin7@users.noreply.github.com>
Co-authored-by: Evgenii Lopatin <e75533@gmail.com>
Co-authored-by: Jino <dgfrty00@gmail.com>
Co-authored-by: snig-17 <santosh.m.tiwari@gmail.com>
Co-authored-by: Sholto McNeilage <sholto@nodenarrative.com.au>
Co-authored-by: Phil Hie <philhie@outlook.com>
Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
Co-authored-by: Jimmy Zeisweiss <4261258+jzeisweiss@users.noreply.github.com>
Co-authored-by: liutiming <timingliu@outlook.com>
Co-authored-by: Brandon Pugsley <aibpaem@gmail.com>
Co-authored-by: benjamin beres <benjamin.beres@bienpreter.com>
Co-authored-by: Som Samantray <som.samantray@gmail.com>
Co-authored-by: Vaughan <vaughan@cairnsitguy.com.au>
Co-authored-by: Bharadwaj Mittapelli <bharadwajmittapelli@gmail.com>
2026-10-04 07:58:41 -07:00
ac20ef1e13 v1.91.16.0 fix: safe installs and upgrades, skills start everywhere, truthful completion (parallel wave) (#3014)
* fix(make-pdf,design): real TOC page numbers and anchors; design models move to gpt-5.5 + gpt-image-2 (#2903, #2807)

* ci: pin every action to a SHA, Scorecard, checksummed Bun, scoped Codex CLI in the CI image, macOS named-regressions job (#1948, #1706, #1997)

* fix(cso,browse): Windows/macOS lease identity, retention skips bad runs, truthful 'not assessed' reports, env.kv false positive, Windows browse spawn fixes (#2894, #2912, #2048, #2637, #2805)

* fix(outside-voice): one Codex model precedence for probe and dispatch, bounded timeouts with KILL escalation, probe cache identity, guarded mktemp, skills hidden in nested Codex (#2776, #2914, #2787, #2881, #2847)

* feat(setup): install registry, ./setup --status, staged activation, per-scope migrations, host tiers and conformance kit, OpenCode/Hermes fixes, Copilot CLI (experimental), disabled_skills, slimmer CLAUDE.md (#1925, #2879, #2347, #2338, #2626, #2629, #2825, #2015, #393, #1206, #2096)

Co-authored-by: Andrey Esipov <61357035+andrey-esipov@users.noreply.github.com>
Co-authored-by: Matthew Williams <5863886+ridermw@users.noreply.github.com>
Co-authored-by: Rocky Wu <92764750+lolisaigao1234@users.noreply.github.com>

* fix(preamble): skills start in worktree-isolated Claude Code sessions — one literal skill-start line, context-recovery helper, no eval in skill blocks, $N token tripwire, honest plan-mode precedence (#2763, #2896, #2851)

* fix(setup): per-install renders for renamed, vendored and custom-config installs, space-safe alias roots, per-host /cso launcher path (#1882, #2906)

* test: free tests never write the real HOME (home-write tripwire), no orphaned browse daemons, symlinked-TMPDIR (macOS) fixes, gbrain-on-PATH isolation (#2895, #2816, #2829, #2841, #2982, #2911, #2909, #2908, #2910)

* chore: integrate wave follow-ups — regenerate all hosts, README model docs, TODOS, touchfiles, duration seeds, Bun 1.3 telemetry test fix, replay fixtures pinned to their recorded bytes

* v1.91.14.0: release metadata

* test(browse): group-kill cleanup falls back to the leader when macOS returns EPERM for a live group

* ci: Codex login steps run under bash (the CI container's default sh rejects pipefail)

* fix: full-suite repairs — private state roots for 7 real-HOME writers, truthful home-guard attribution with --attribute-home, gstack-paths shim in ship-hook fixtures, Copilot in team-init probes, test-of-test detector, cookie review fixture pinned to the approved excerpt

* fix(windows): LF for Dockerfiles, normalized home-guard exclusions, path.relative fixture containment, ship push test spawn timeouts

* test(design-detect): widen the whole-scan budget test's timing margins so a loaded macOS runner can't time out the single-batch call

* fix(evals): qa protocol names defined for the judge, async PTY judge so overlapping sessions keep their cleanup deadline, bws setup cases use the standard JUDGE_MS tier

* test(pty): the non-blocking judge case is POSIX-only (its fake judge is a shebang script, like the PTY harness it covers)

* test(evals): ship-coverage-audit names absolute skill paths, uses Read, and skips the Step 7 artifact nothing consumes (removes the mechanism behind 3 of 4 historical timeouts)

* test: deflake terminal-agent concurrent startup (write the placeholder record before spawning, like the product) and qa-evidence resumed pipe (barrier on a full high-water mark instead of a sleep)

* fix(design): daemon log stream handles open errors instead of crashing the daemon; shutdown test awaits the stubbed exit

* test(evals): skillify checks the product's resolved state root (no GSTACK_HOME bypass), negative cases check every root; tpa shims live outside the agent's cwd and answer consistently

* test(evals): coverage-audit detector splits parallel columns at │ rails and [+] groups; coverage prompts name absolute skill paths; TODOs from the gate eval census

* fix(plan-eng-review): scope gate always sends the menu (no headless 'Scope pending' exit); PTY harness ends idle turn-ends and API-error panels promptly with a precise diagnostic

* test(evals): one position-bound coverage-read rule credits read-only && chains (labels, context ranges, git displays in either order) and still rejects forged output

* fix(ship): documentation section has one saved source inserted by cat (never retyped); spawned-marker evidence is the child output; refreshed codex/factory ship goldens

* test(docsync): the byte-exact insert positive is POSIX-only (the bash-command observer refuses backslash paths by design)

* test(evals): plan-design-with-ui focus-title rule accepts the comma-less 'dimensions or focus' wording (replayed on the captured CI run, negative controls kept)

* fix(qa-callers): exploratory preflight says the scope/method Reads come after exploratory.md; design-review scan check credits an executed scan proven by one engine run over the changed files

---------

Co-authored-by: Andrey Esipov <61357035+andrey-esipov@users.noreply.github.com>
Co-authored-by: Matthew Williams <5863886+ridermw@users.noreply.github.com>
Co-authored-by: Rocky Wu <92764750+lolisaigao1234@users.noreply.github.com>
2026-10-03 07:54:09 -07:00
Garry Tan df89475b17 v1.91.11.0 refactor: one state-root rule, browse route table, shared shard engine, PTY harness split, MECE review resolvers (#3002)
* refactor(resolvers): split review.ts into MECE resolver modules (pure move)

Move every function from scripts/resolvers/review.ts, unchanged, into:
- review-dashboard.ts: review dashboard, plan-file review report
- plan-gates.ts: approval check, exit-plan-mode gate, plan-file discovery,
  plan-completion audit/gate (ship + review), plan verification exec
- spec-review.ts: both spec review loops, benefits-from, anti-shortcut clause
- outside-voice-steps.ts: Codex second opinion, adversarial step, Codex plan
  review, Codex doc review, disabled-outside record
- review-scope.ts: scope drift, cross-review dedup, shared-code reuse

review.ts is deleted; index.ts imports the new modules. gen-skill-docs
output is byte-identical for every host (--host all). Test imports and
source-path references are re-pointed; the two source-text report/gate
tests in gen-skill-docs.test.ts become behavioral renders across every
consuming skill and host. All 46 touchfile entries that named review.ts
now name all five modules, guarded by a recorded selection golden.

* test(browse): black-box auth matrix for every server route and both surfaces

Drives buildFetchHandler fetchLocal/fetchTunnel with no token, wrong token,
root token, scoped token and the SSE cookie for all 33 routes, plus unmatched
paths and wrong methods. Denials assert today's exact status, body and content
type; allowed credentials assert the handler was reached. Written against the
unchanged if-chain server so the W3 route-table refactor must keep it green.

* refactor(shard-engine): move scripts/test-strict-output.ts to scripts/lib/shard-engine.ts

The shared shard engine grows from the existing strict-output module
(runShardChild, killProcessGroup, signal forwarding, strict classifier).
scripts/test-strict-output.ts stays as a re-export so existing importers,
mock.module paths and the strict-output/run-shard-child tests are unchanged.
The engine inherits the global touchfile entry; the free runner's CLI-routing
fixture copies the new module.

* refactor(resolvers): decompose the three >150-line review resolvers (output-neutral)

Split generateAdversarialStep, generateCodexPlanReview and
generatePlanCompletionAuditInner into per-section helpers whose template
literals are copied verbatim, so every function in the new modules is at
or under 150 lines. gen-skill-docs output is byte-identical for every host
(--host all, compared against 96764e80 with a fixed --link-root).

* refactor(resolvers): one outside-voice failure policy (deliberate prose unification)

outsideVoiceFailurePolicy(ctx, opts) in outside-voice.ts now renders the
auth / timeout / empty-response bullets for all four call sites that
hand-typed them (Codex second opinion, adversarial step, Codex plan
review, design outside voices). Options are explicit per site
(timeoutMinutes, onTimeout, stderrOnEmpty, fallback, escape) with no
defaults.

Deliberate generated-prose changes (every host):
- office-hours: 'Fall back to <native> subagent.' becomes
  'Fall back to the <native> subagent below.'
- plan-devex-review: the plain 'Auth failure (stderr contains ...)'
  bullets become the canonical bold bullets; auth also triggers on
  'API key'; 'auth failed' becomes 'authentication failed'.
- review/ship adversarial: 'exceeded 9 minutes and was terminated'
  becomes 'timed out after 9 minutes and was terminated'; the timeout
  is still MISSING COVERAGE.
- design outside voices: unchanged.

Adds ratchet (d) (test/outside-voice-failure-policy.test.ts) with a
reasoned allowlist for /codex's own CLI errors, the MISSING COVERAGE
retention test, refreshed codex/factory ship goldens, and outside-voice.ts
in every touchfile entry of review.ts and design.ts (selection golden
extended).

* test(pty): fake PTY session driver with an injectable clock through the runner launch seam

The three plan-skill runners take an optional PtyDriver (launch, now,
monotonic, sleep); omitted, they use the real launcher and clocks exactly as
before. test/helpers/pty/fake-session.ts feeds scripted frames through that
seam, and claude-pty-runner.runners.unit.test.ts runs observation, counting
and floor for success, deadline timeout, permission prompt and plan-ready
outcomes with no CLI or real timers. These cases must stay green unchanged
through the W4 split and the runPtySession extraction.

Touchfiles: every entry that lists claude-pty-runner.ts or pty-screen.ts now
also lists test/helpers/pty/**.

* refactor(shard-engine): run both lanes on the shared engine; lane policy injected

Engine (scripts/lib/shard-engine.ts) gains the W2 primitives: per-shard
tmp/Chromium sandbox + async cleanup backstop, log-path allocation and
full-stream log capture, one duration-seed reader/writer with a lane
predicate, LanePolicy (seed predicate + zero-execution verdict),
strictShardStatus, and the shared CLI flag loop. runShardChild takes an
optional companion (signal/settle) and waits a bounded 250ms to reap a
wall-killed child.

Free lane stops spawning shards itself: runFreeShard uses runShardChild
with trackShardBrowser as the companion (win32 path unchanged: no process
group, no negative-pid kill). Its sync state-dir removal stays lane policy.
Paid lane uses the sandbox, log, seed, verdict and flag primitives; the
hollow-shard guard applies PAID_LANE_POLICY. Lane outcomes are unchanged
(free keeps >= 0 seeds and file-count zero-exec rule; paid keeps > 0 seeds,
warning under selection and passed-empty under EVALS_ALL).

paid-free-boundary's closure assertion now names the engine module, where
the strict classifier lives.

* test(shard-engine): engine unit tests, fixture-corpus equivalence, per-lane CLI parity

- test/shard-engine.test.ts: failing/unhandled/module-load output fails both
  lanes, per-lane zero-execution and seed rules, whole-group kill on a wall
  timeout (both lanes), mocked-win32 path with no negative-pid kill,
  companion settle order, log capture, sandbox isolation, flag loop.
- test/shard-engine-equivalence.test.ts + test/fixtures/shard-equivalence:
  seven outcome fixtures plus one real shard, run through both lanes and
  compared with classifications recorded from the base runners (96764e80).
- test/shard-cli-parity.test.ts + test/fixtures/shard-cli-parity: flag set,
  defaults, validation errors and the Unknown argument error per lane match
  the base runners.

* refactor(shard-engine): decompose runFreeShard and runPaidShard to <= 150 lines

Output-neutral extraction under the fixture-corpus equivalence and runner
tests: captureFreeStream, explainFreeVerdict and logFreeRecovery (free);
paidShardCommand, settleShardSpool, settleBootstrapRetention and
printLogTail (paid). The bootstrap scope-creation block that
bootstrap-retention.test.ts evaluates stays verbatim.

* refactor(pty): split claude-pty-runner.ts into test/helpers/pty/* behind a barrel

Pure move: every line of the former 5,047-line runner lands verbatim in one
module (four private helpers gain `export` for cross-module use):
binary, screen (absorbs test/helpers/pty-screen.ts, which now re-exports it),
launch, session (PtyDriver), judge, classify, auq, plan-native, boundaries,
runners/{observation,counting,floor}. claude-pty-runner.ts re-exports the
original public surface by name; pty/ modules import siblings directly.

Tests that read the runner's source text:
- rewritten as behavioral: the unit test's model-pin tripwire (fake CLI argv:
  fallback chain, --model before extraArgs, hermetic --strict-mcp-config),
  pty-skill-seeding-wiring (runners through the fake driver; launcher through
  a fake CLI reporting CLAUDE_CONFIG_DIR). The "three wrappers forward model"
  grep is replaced by the runners' fake-driver launch assertions.
- pty-screen-session / pty-screen-supervision: stop copying runner source;
  they mock.module the real pty/screen.ts (and the fixture cleanup) instead.
- re-pointed to the owning module (they execute a sliced runner body with
  injected boundaries; no seam exists for those boundaries yet):
  eng-seeded-completion-ai, plan-floor-permission, plan-create-prepublication,
  plan-count-completion; hermetic-wiring's source guard now reads pty/launch.ts
  and scans every pty/ module for raw process.env spreads.
- plan-count-timeout and pty-output-wake mock the viewport at pty/screen.ts.

* test(ratchet-c): enforcing module/function size ratchet and moved-code touchfile coverage

Ratchet (c) ships enforcing: test/helpers/module-size.ts counts file and
top-level function lengths by brace matching over masked source (strings,
comments, regex literals and template text masked; ${} expressions kept),
covering function declarations, arrow functions assigned to consts and
route-table handler properties, with no parser dependency. Its self-test
uses template literals and code-fence braces copied from
scripts/resolvers/review.ts and design.ts. test/fixtures/module-size-ratchet.json
binds scripts/lib/shard-engine.ts (<= 800 lines, <= 150 per function) and
records the residual runner sizes (free 2352, paid 1921) as non-growth caps;
allowlist entries are keyed on file plus matched text and need a reason.
Failure output lists file:line, the rule, Fix: and the allowlist path.

touchfiles.test.ts gains the moved-code superset check over
test/fixtures/touchfile-move-goldens/ (W2 golden recorded at 96764e80:
test-strict-output.ts and test-paid-shards.ts global, test-free-shards.ts none).

* refactor(browse): declared route table replaces the buildFetchHandler if-chain

The ~1,300-line if-chain in buildFetchHandler becomes a route table:
each entry declares method, path, auth kind and surfaces, and one auth
gate in browse/src/routes/table.ts returns the per-kind denial (root-bearer,
scoped, root-or-sse-cookie: 401 Unauthorized; root-token: 403 Root token
required; extension-origin: 403 Forbidden). Unmatched requests take the
declared fallthrough (root-bearer check, then plain-text 404). Handlers move
to browse/src/routes/{core,pairing,pty,tokens,tunnel,activity,commands,files,
inspector}.ts and receive a RouteContext with auth checks as functions
instead of closing over factory locals. Dispatch order is unchanged:
tunnel filter, beforeRoute overlay, gate, handler. TUNNEL_PATHS stays a
literal in server.ts.

Behavior-preserving: the black-box auth matrix from the previous commit
passes unchanged. /memory and /inspector/events are declared root-bearer
because the blanket check always ran before their SSE-cookie branch.

Source-text route tests are rewritten as behavioral tests through
buildFetchHandler or a route's real handler with a stub RouteContext
(browse/test/route-test-harness.ts). Checks with no runtime seam are
re-pointed to the route modules: Surface type, /inspector/events SSE
helper, sanitizeReplacer imports, /pty-inject-scan sidecar-client import,
and the ngrok config lookup and startTunnel wiring that stay in server.ts.

* test(browse): stubbed-handler auth matrix and route inventory for the route table

Every ROUTES entry runs through the real dispatcher and gate with stub
handlers on each declared surface and six credentials; denials assert the
exact status and body each auth kind returned at 96764e8, admitted
credentials assert the handler ran (with the gate's TokenInfo for scoped
routes). Also pins the reviewed route inventory (method, path, auth kind,
surfaces), that every entry declares auth and surfaces, that the table's
tunnel paths equal the TUNNEL_PATHS literal with GET /connect admitted, the
unmatched fallthrough, and that the root token is rejected on every tunnel
route through buildFetchHandler.

* test(browse): ratchet (b) keeps route dispatch inside the route table

Scans browse/src/server.ts and browse/src/routes/*.ts for pathname
comparisons; only the table matcher and the tunnel-surface filter are
allowed, listed with reasons in browse/test/fixtures/route-dispatch-allowlist.json
(keyed on file plus line text). Also checks every entry declares auth and
surfaces and that gstack registers no beforeRoute overlay itself. Self-tests
plant a violation and assert the file:line, Fix: and allowlist path in the
message, that a shifted line stays allowlisted, and that a reasonless entry
is rejected.

* test: touchfile superset check for modules moved out of browse/src/server.ts

Records the paid evals selected by touching browse/src/server.ts at 96764e80
(17 E2E, 1 LLM judge) and asserts every browse/src/routes/*.ts module selects
a superset. The test reads every golden in test/fixtures/moved-module-selection/
so other moved-code goldens can sit beside it.

* test(shard-engine): give non-timeout corpus fixtures CI headroom; keep the POSIX golden off the Windows lane

Only the wall-timeout fixture keeps a 3s wall; the rest get 60s so a loaded
host cannot turn a pass into a timeout. Base and branch runners still agree
on every classification under the new walls. The Windows exclusion entry
moves the free runner's ratchet (c) residual cap to 2356 lines.

* refactor(pty): one runPtySession loop drives observation, counting and floor

test/helpers/pty/session.ts owns launch -> start -> (poll -> tick)* ->
timeout and the failure contract the three runners each hand-rolled: the
run's own error wins over capture and close errors, close always runs, owned
fixture cleanup runs last (also when launch fails). Each runner now supplies a
PtySessionPlan: its boot/command step, poll cadence (2s observation/floor
sleep; counting's output wake + 250ms coalesce), tick policy (permission
handling, native identity, terminal rules stay per runner because they differ)
and capture hooks. The runner bodies are decomposed into top-level steps so no
function exceeds 150 lines; behavior is unchanged and the fake-driver cases
from the first W4 commit pass unmodified.

The counting capture step and the native completion-summary predicate are now
named functions (countingCapture, isNativeCompletionSummary), so
plan-create-prepublication and plan-count-completion call them directly
instead of executing sliced source. The two harnesses that still execute a
sliced runner body with injected boundaries (eng-seeded-completion-ai,
plan-floor-permission) pass the PtyDriver seam instead of overriding
Date/Bun.sleep.

* test(ratchet-c): register route modules, review resolver modules and server.ts residual cap

* refactor(pty): decompose launchClaudePty and engNumberedFindingAUQ under 150 lines

launchClaudePty (349 lines) becomes launch preparation (args, hermetic
child env, owned state roots), recorder creation, spawn, the trust-dialog
watcher, close, and the session handle over one PtyProcess state object. The
failure order is unchanged: abort the viewport, dispose any recorders created
so far, dispose the viewport, rethrow. The --model / --strict-mcp-config
ordering and seedSkills wiring stay pinned by the behavioral fake-CLI tests.

engNumberedFindingAUQ (345 lines) keeps its guards and dispatch; each
self-contained issue family (declared cache, library retry hooks, cache
owner, injected singleton, shared writers, injected export) moves verbatim
into its own function. Every pty/ module is now <= 800 lines and every
top-level function <= 150 lines.

* test(pty): split claude-pty-runner.unit.test.ts along the pty/ module seams

The 188 unit tests move verbatim into claude-pty-runner.{screen,classify,
auq,launch,plan-native,boundaries}.unit.test.ts (test names unchanged; each
file imports only what it uses from the barrel). The five files that no longer
read a SKILL.md template join the test-of-test ratchet baseline with a reason.

* test(touchfiles): moved PTY modules keep their paid-eval selection

test/fixtures/touchfile-selection/w4-pty.json records, at 96764e8, the paid
evals selected by touching test/helpers/claude-pty-runner.ts (20) and
test/helpers/pty-screen.ts (20). touchfiles.test.ts now asserts every .ts file
under test/helpers/pty/ (and pty/screen.ts for both sources) selects a
superset, reading every golden in that directory so later moves can add one;
a planted-violation case pins the report and its Fix line.

* fix(browse): unexchanged pair setup keys no longer authenticate bearer requests

validateToken accepted a gsk_setup_ key as a bearer on /command, /batch and
/file (found while building the W3 auth matrix). A setup key now only
authenticates the /connect exchange.

* W1: one state-root owner (lib/state-root.ts + bin/gstack-state-root.sh), gstack-paths --explain and fail-stop, parity tests

* W1: guarded migration of every executable state-root site; uninstall deletes only ~/.gstack

Bins, careful/freeze hooks, setup, upgrade migrations, browse/src, design,
ios-qa daemon, lib and scripts resolve the state root through
bin/gstack-state-root.sh (bash) or lib/state-root.ts (TS). Bins source the
twin and stop with a reinstall message when it is missing; hooks source it
and never spawn gstack-paths. browse/src/config.ts and lib/cso/state.ts
delegate to resolveStateRoot. Analytics writers and readers move together
so the usage log stays one file. gstack-uninstall deletes state only at
~/.gstack, refuses (exit 2) when it resolves to /, $HOME or an ancestor,
the checkout or the git root, and leaves any other resolved root in place
with the removal command. Fixtures that copy single bins now copy the twin.

* W1: privacy keys and trust-policy deny tiers merge across state roots; gstack-config reporting; test hermeticity

readConfigKey / gstack_read_config_key return the most restrictive
telemetry, memorable_recall, codex_reviews and update_check across the
resolved root and ~/.gstack; other keys read the resolved root only.
gstack-config set reports an overriding root with the exact override
command, list shows the winning root and a root-variable disagreement line.
gstack-gbrain-repo-policy get merges deny/read-only tiers. gstack-egress
reads through readConfigKey. test-setup.ts strips inherited
GSTACK_STATE_ROOT/GSTACK_STATE_DIR and redirects the legacy root.

* W1: shared hook logging helper (hosts/claude/hooks/hook-log.ts)

One hook-errors.log writer: root from resolveStateRoot, 0600 on every
append, opt-in rate limit used only by memorable-user-prompt. The five
hooks route through it.

* W1: docs/state-root.md and README troubleshooting pointer

Precedence table, a real --explain example, the move-your-state recipe,
merged privacy keys, the uninstall rule, the resolver-failure fix, and the
plugin-mode note (evidence gate: no official plugin distribution).

* W1b: template and resolver prose resolve state through guarded gstack-paths; ratchet (a)

Every gstack-paths eval in templates and resolvers carries the fail-stop
guard; executable ~/.gstack paths in bash blocks (context recovery preamble,
eureka log, analytics, project artifacts, upgrade snooze, setup-gbrain lock,
retro snapshots, ship consent marker) use $GSTACK_STATE_ROOT, and the writer
prose that pairs with them points at the printed PROJECT_DIR / RETRO_FILE.
ship drops export GSTACK_STATE_ROOT. SKILL.md regenerated (claude + codex),
ship goldens re-pinned, parity and context-budget caps raised to the measured
sizes with notes. test/state-root-ratchet.test.ts enforces the rule with a
reasoned allowlist; W1 touchfile entries plus a superset golden.

* refactor: apply W1 state-root edits in W2/W3/W5-owned files; one moved-code touchfile golden for all workstreams

* test: fold the moved-code touchfile golden into touchfiles.test.ts; fix integration fixture closure and caps

* v1.91.11.0: CHANGELOG, TODOS, docs and conventions for the refactor wave

* test: re-measure plan-ceo/design-consultation caps and ship goldens after the guarded plan-discovery and spec-review blocks; add the state-root twin to the workflow-boundaries fixture

* fix(windows): migrations resolve their directory with either path separator; state-root parity compares under the HOME Git Bash actually sees

* fix(review,ship): state plan-check timing after smoke expiry and test_stub Skip semantics (review workflow judge clarity)

* test(qa-eval): webhook fix eval asks for the fix loop's post-repair probes; eight-scenario coverage stays in the report-only case and the harness recheck

* test(qa-eval): re-pin the webhook prompt contract to the fix-loop stage; R29 coverage omissions stay bound by the report-only case

* fix(review,ship): plan checks publish a checkpoint before each probe; only the smoke expiry stop is skipped

* fix(qa): carry #2999's checkpoint receipt link, report-template line and full-revision placeholder (identical hunks)

* test(qa-callers): disable git auto maintenance in the caller fixture

Git 2.47+ runs auto maintenance detached after commit; on the CI runner's git
2.55 it rewrote .git/objects fan-out directories while the write observer was
running, which surfaced as unauthorized mutations. Same gc.auto=0 /
maintenance.auto=false guard the shared-libs fixture already uses.

* test(plan-mode-no-op): require prose evidence for the prose-fallback members so a spinner-frame judge verdict cannot end the run as asked

* test(ship-docsync): carry #2999's seeded-attempt docsync harness (identical files)

The doc-sync fault cases replayed attempt 1 before reaching their gate and ran
out of their 285s budget. The fixture now seeds attempt 1 and the parent starts
at the gate under test. Taken byte-identical from origin/capy/audit-fix-wave
(fb526898, e6ac813d, 6ce10ff7, d0c53577, 77cce3be). Local: stale-before,
recovery and late-result 6/6 PASS (97-164s); the whole file 12/12 PASS.
2026-10-01 11:57:48 -07:00
28d59ad56c v1.68.0.0 fix: next tracker wave — 16 verified fixes in, 90 stale PRs and 21 issues closed with receipts (#2632)
* fix(plan-tune): reject never-ask on one-way ids at --write

--check already ignored those prefs; --write still stored them and
--stats counted them as a working NEVER_ASK. Refuse the write and
count leftover on-disk prefs as INERT_ONE_WAY.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix: gstack-config get returns "" with exit 0 for keys that have no default

Skill preambles read configuration with

    VAR=$(gstack-config get <key> 2>/dev/null || echo "<default>")

and that fallback only fires on a non-zero exit. lookup_default ended in a
catch-all that echoed "" and returned 0, so for any key missing from the table
VAR came back empty and the default written right there in the preamble was
unreachable. The skill then branched on a value it never specified: "skip
entirely if QUESTION_TUNING is false", reached with QUESTION_TUNING="".

Four keys that skills actually read had no entry and took that path:

    question_tuning         -> callers assume "false"
    repo_mode               -> callers assume "unknown"
    team_mode               -> callers assume "false"
    transcript_ingest_mode  -> callers assume "off"

Each default above is the value the call sites already substitute in their own
`|| echo` fallback, so this only makes reachable what was already intended.

The catch-all now returns non-zero. That is deliberately scoped to the
unknown-key arm alone: keys whose default is intentionally empty still exit 0,
because "" is their real answer and their callers depend on it --
cross_project_learnings ("unset triggers the first-time prompt"),
redact_repo_visibility ("empty falls through to gh/glab detection"),
salience_allowlist, user_slug_at_*. Making every empty answer an error would
have broken those.

test/gstack-config-defaults.test.ts pins the class rather than the four
instances: it parses the case arms and asserts every `gstack-config get <key>`
site in the tree is covered, so adding a read without a default fails CI. It
also pins the exit-code contract in both directions. Verified failing against
the pre-fix script, where it names exactly those four keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(redact): a typo'd subcommand no longer exits 0 having done nothing

main() recognised exactly two subcommands and let everything else fall through
to the stdin scan. On empty stdin that prints "(no findings)" and exits 0, so:

    $ gstack-redact install-prepush-hooks    # plural typo
    gstack-redact scan — repo UNKNOWN
      (no findings)
    $ echo $?
    0

No hook was installed, and the operator has every reason to believe the
credential guard is armed. A guard that silently no-ops must never exit 0.

Two smaller faults in the same dispatch, both of which lead people here:

- There was no --help handler, so `gstack-redact --help` fell through to the
  scanner. Piping a credential to it scanned the secret and exited 3.
- With no piped input and no --from-file, readInput() blocks on readSync(fd 0)
  until an EOF that an interactive terminal never sends. That prints nothing
  at all, so it reads as a hang rather than as "this is a filter, feed it".

Now: --help/-h/help prints usage and exits 0; an unrecognised positional
prints the offender and exits 1; a TTY with nothing piped in prints usage
instead of blocking. "scan" stays accepted, because the human output header
reads "gstack-redact scan — repo …" and that is what people type.

Usage errors exit 1, deliberately not 2 or 3. Those mean MEDIUM and HIGH
findings and callers gate dispatch on them, so a usage error exiting 2 would
be read as "medium findings — prompt the user". A test pins that.

Tests: 4 written failing first, then fixed. Full suite 7,722 pass / 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(browse): one ambiguous ref no longer kills the whole annotated screenshot

`snapshot -a` exits 1 with "Selector matched multiple elements" on most real
pages, so /qa, /canary and /land-and-deploy silently produce reports whose
screenshots do not exist. Plain `screenshot <path>` is unaffected.

Refs are built as getByRole(role, {name}) and disambiguated with .nth() when
role+name repeats. That disambiguation cannot fire for a node with NO accessible
name: the locator degrades to getByRole(role) with no name filter, and the count
driving .nth() is taken from the FILTERED aria snapshot while getByRole matches
the unfiltered DOM. Measured on a live page: the tree surfaced 2 unnamed
paragraphs, the DOM had 9. Landmarks (banner/main/contentinfo) and paragraphs are
correctly unnamed per ARIA, so this is the common case rather than an edge case.

boundingBox() then hits Playwright strict mode, and the catch allowlisted only
timeout/closed/Target/Execution-context messages — so the strict-mode error was
re-thrown and aborted every remaining annotation.

Two changes:

- `.first()` before boundingBox(), so an ambiguous ref draws a box on its first
  match instead of aborting. The heatmap path below has always tolerated this via
  a bare `catch {}`; annotate was the only path that could be killed outright.
- the catch no longer re-throws on unrecognised messages. A box we cannot measure
  is a box we do not draw, never a reason to lose the rest of the page. Set
  BROWSE_DEBUG to see what was skipped.

Also: `-o` passed without `-a`/`-H` was silently ignored (exit 0, no file), which
reads as "screenshots are broken" rather than "you forgot a flag". It now warns
and points at `browse screenshot <path>`.

Verified by rebuilding both ways against the same page with 51 refs present:
  before — "Selector matched multiple elements", no file written
  after  — exit 0, 229KB PNG

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(version-bump): missing or empty VERSION no longer repairs a fabricated 0.0.0.0 into package.json

repair now fails with exit 2 when the VERSION file is absent or empty
instead of folding to DEFAULT ("0.0.0.0") — which passed VERSION_RE and
regressed package.json below where it started. classify gains an additive
versionFileExists field so /ship can tell a real 0.0.0.0 from a fabricated
one. Re-derived from PR #2612 under the generated-file screening rule.

Fixes #2600 (repair half; the path-configurability half landed in v1.67 via #2531).
Contributed by @Lockyer228

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(memory-ingest): --probe counts post-attribution, through the same gate --bulk uses

probeMode previously stat'd every walked file, so setup-gbrain gated its
silent bulk ingest on pre-filter counts that the write path would never
ingest (#2394). The attribution decision now lives in ONE shared gate
(sessionIsAttributable — cheap-parse: cwd extraction + memoized
resolveGitRemote, never a full page build) used by BOTH probeMode and
preparePages, so the two stages' post-attribution counts are structurally
identical. ProbeReport gains skipped_unattributed; the probe prints what it
excluded and --include-unattributed restores raw counts. The parity is
pinned at the prepare stage (probe post-attribution == transcripts reaching
import), deliberately NOT == final written.

Re-derived from PR #2612 under the generated-file screening rule; the
shared-gate design and the remote memo are additions from the plan review.

Fixes #2394.
Contributed by @Lockyer228

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(browse): allow CPU and network throttling for performance measurement

Adds Emulation.setCPUThrottlingRate and Network.emulateNetworkConditions to
CDP_ALLOWLIST.

Motivation: diagnosing a real "uploads take 1-2 minutes" report, the only
machine available was a fast developer workstation. Client-side processing
measured 1.4s where the user experienced minutes, so the conclusion had to be
reached arithmetically rather than observed. Throttling would have let the
measurement reproduce the reporter's conditions directly.

Both fit the existing posture rather than widening it:
  - Emulation already allows setDeviceMetricsOverride, clearDeviceMetricsOverride
    and setUserAgentOverride, which are equally mutating and scoped to the tab.
  - Neither method reads page content. setCPUThrottlingRate affects only timing;
    emulateNetworkConditions constrains traffic rather than inspecting it, so no
    request bodies, headers or cookies are exposed. Both are output: 'trusted'
    because they return no page-derived data.

scope 'tab' for both, matching the surrounding Emulation entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(session-update): lock pidfile records the live holder; hard TTL bounds every wedge (#2613)

echo $$ inside the backgrounded subshell recorded the PARENT hook's PID —
which exits immediately — so every subsequent session judged the lock stale
and rm -rf'd a LIVE holder's lock, letting concurrent updaters run over each
other. The pidfile now records ${BASHPID:-$(sh -c 'echo $PPID')} (macOS
bash 3.2 has no BASHPID; the sh child's PPID is exactly this subshell).

Staleness is now two independent detectors: PID liveness (as before, but
against the real holder), and a 30-minute hard TTL on the heartbeat mtime —
reclaimed regardless of kill -0, so a recycled PID or hung holder can't wedge
the lock forever. The holder touches the pidfile after the pull and after
setup, so a legitimately-slow run keeps itself alive. Empty and missing
pidfiles are respected inside the TTL window (the mkdir→echo race) and
reclaimed past it.

Fixes #2613.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(browse): explicit windowsHide on every Bun.spawn site + census tripwire (#2575 residual)

Bun.spawn sites were structurally outside the windowsHide census (it swept
child_process bindings only). The runtime was already safe — native Bun hides
consoles by default and bun-polyfill.cjs defaults windowsHide !== false since
#2523/#2539 — but implicit defaults are exactly what regress silently. Every
Bun.spawn/spawnSync in browse/src now carries the explicit flag (harmless on
unix-only sites like Xvfb/xattr/open), and a second SWEEP in
windows-spawn-hide.test.ts fails CI on any new flagless Bun.spawn site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gbrain): brain worktree advances on the daily sync — no more silently stale brains (#2516)

The daily pull refreshed only ~/.gstack itself, never the detached worktree
at ~/.gstack-brain-worktree that gbrain actually indexes — so after setup the
brain served stale pages forever unless setup-gbrain/sync-gbrain happened to
run. brain-sync --once now advances the worktree once per 24h behind an
ATTEMPT stamp (.brain-worktree-last-advance — a persistently-failing advance
warns once a day, not at every skill boundary), inside the existing run lock
and before any ingest step touches the worktree.

The new gstack-gbrain-source-wireup --advance-only is built for the
unattended cadence: git-only (no gbrain prereqs), pins every operation to the
managed worktree (refuses paths that are not worktrees of the artifacts
repo), refuses dirty worktrees, and never runs the force-remove recovery — a
cron path must not be able to delete local changes. A static pin keeps the
force-remove out. docs/gbrain-sync.md stops overclaiming the old cadence.

Fixes #2516.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(memory-ingest): honor the per-remote deny/read-only trust policy (#2392)

Transcript ingest now respects the same trust store as code import — the gate
existed only in gstack-gbrain-sync's runCodeImport, so memory-ingest happily
ingested transcripts from deny-listed repos. preparePages filters prepared
transcript pages through ONE batch policy lookup (new 'get --batch' verb on
bin/gstack-gbrain-repo-policy — the script owns URL normalization; the client
adds repoPolicyTierBatch, one spawn for all distinct remotes, so large corpora
never pay a 10s-timeout subprocess per remote).

Outcomes match code-import semantics: read-only → clean skip
(skipped_policy_readonly), deny → counted refusal (skipped_policy_deny),
corrupted/unreadable store → HARD ERROR before any write (state, staging,
egress receipt, and import all untouched) with the recovery command named —
policy corruption must never read as successful ingestion. Artifacts are
never policy-filtered (their git_remote is a project slug, not a remote).

Fixes #2392.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): repo_mode keeps its empty no-default semantics (#2611 follow-up)

The ported defaults table synthesized repo_mode → "unknown", but EMPTY is
load-bearing for that key: gstack-repo-mode treats any non-empty answer as a
user override and skips its own repo classification — the synthesized default
turned the classifier into dead code (REPO_MODE=unknown everywhere; caught by
test/gstack-repo-mode.test.ts via the wave's cross-agent blame protocol).
repo_mode joins the empty-is-real carve-outs (empty output, exit 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pair-agent): consent before killing a healthy headless daemon

The pair-agent headed switch spawned 'connect --force-restart'
unconditionally — auto-killing a live headless daemon (open tabs, cookies,
logins) in direct contradiction of the iron rule it sits beside ('only an
explicit --force-restart may kill a live daemon'). The CLI now captures
daemon liveness BEFORE ensureServer (which can itself boot a fresh daemon)
and relaunches only when the user passed --force-restart to pair-agent;
otherwise it prints the tab count and continues against the existing daemon.
The /pair-agent skill gains a matching one-way-door consent question
(template half rides the wave's template block).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gbrain-status): MCP scoping is per-project, and project-local beats user scope

hasRemoteOnlyGbrainMcp scanned EVERY project's mcpServers in ~/.claude.json,
so one project's remote gbrain registration reclassified broken local engines
as thin-client machine-wide. It now reads user scope plus only the cwd's
nearest-ancestor project key.

The precedence itself was verified empirically and hermetically (fake HOME +
CLAUDE_CONFIG_DIR fixtures, claude 2.1.233): with both scopes defining
gbrain, 'claude mcp get gbrain' reports Scope: Local config — PROJECT-LOCAL
WINS. Both in-repo consumers assumed the opposite; brain-cache's endpoint
resolution flips to nearest-ancestor-project-first, and the stale user-first
pin in brain-cache-roundtrip now pins the verified precedence. (The user-first
jq in the brain-sync preamble resolver gets the same swap in the template
block.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(slug): gstack-slug matches remote-slug's owner-repo canonical form (live misfile bug)

Found live during this wave's CEO review: bin/gstack-slug emitted
SLUG=garrytan for this garrytan/gstack worktree while remote-slug correctly
gave garrytan-gstack — decisions, timeline, ceo-plans, and learnings were
filing into the wrong project store (observed polluting Context Recovery with
another repo's decisions). Root cause: a stray empty ~/.git directory made
the walk-up crown $HOME as the outermost project root; the remote lookup ran
only against that root, failed silently, and the basename fallback cached
'garrytan' sticky. NOT worktree-specific — any strong marker on a non-repo
ancestor triggered it.

Fix: the walk now finds the outermost ancestor whose .git actually resolves
an origin remote and derives owner-repo with remote-slug's byte-identical
parse; marker-only ancestors keep anchoring the basename fallback but can no
longer shadow a real remote. A new cache self-heal recomputes the poisoned
shape (cached == basename of a marker root while a remote-bearing repo exists
below), preserving legit #2212 stickiness. Nested-repo walk-up, no-remote and
non-git fallbacks, and the SLUG=/BRANCH= eval contract are unchanged, pinned
by a 10-case parity suite. Store migration for pre-fix data is tracked in
TODOS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(brain-sync): per-record spool dir — the enqueue/drain race dies structurally

Producers appended lines to .brain-queue.jsonl while the drain re-read and
os.replace'd it; the in-code comment admitted a lockless append between the
re-read and the replace was lost. Locks and rename-rotation designs were both
reviewed and rejected (each retained a tail race); the shipped design is a
maildir-style spool: one FILE per record in .brain-queue.d/ (tmp + atomic
rename), the drain snapshots filenames, processes, and deletes exactly what
it snapshotted. Writer and drainer never share an inode — nothing to race.

Semantics: at-least-once (a crash between process and unlink re-drains;
downstream content-hash dedup absorbs duplicates); retained (privacy-held)
records keep their files; unparseable records are kept + warned, never
destroyed. Legacy .brain-queue.jsonl migrates atomically on the next drain
(crash-leftover .migrating files recovered too); status/drop-queue count both
surfaces; discover-new writes spool records and advances its cursor
per-record-written. The preamble's queue-depth line switches to spool count
in this wave's template block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bin-context): native slug fallback walks up like bash gstack-slug

slugFromEnvironment derived the slug from the INNERMOST repo's origin while
bash gstack-slug walks to the outermost project root — nested/vendored repos
split their stores across the bash/native boundary (win32 hits the native
path constantly). The native fallback now ports _outermost_project_root
faithfully (strong/weak markers, outermost-strong-wins, 64-depth cap,
fixed-point termination) plus the full resolution order: env override →
walk-up → sticky cache with the #1125 self-heal → remote get-url → basename.
Twelve mirrored scenarios drive BOTH implementations against the same
fixtures and pin identical slugs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(next-version): git fallback queries the live remote, never mutates, and keeps 3-digit width

The degraded path counted every remote-tracking ref on every remote — stale
experiment branches and second remotes inflated version allocation, and a
failed base read flipped 3-digit repos to 4-digit slots. Now: ls-remote
--heads origin first (GIT_TERMINAL_PROMPT=0, 5s timeout, zero local ref
mutation); on failure, local refs/remotes/origin ONLY with an explicit
stale-refs warning; a failed base read zeroes at the LOCAL version file's
width so a 3-digit repo allocates 0.0.1, not 0.0.1.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): hooks register the global-install path and re-point stale ones

Registering hooks from a dev worktree baked that worktree's absolute path
into settings.json — deleting the worktree left a dead hook erroring on
every session stop, and the presence-only dedup (list-sources | grep) could
never re-point it. setup's hook paths now route through _hook_install_path
(global install preferred, source dir fallback), and the new ensure-event
verb on gstack-settings-hook compares the registered command payload against
canonical: identical → no write, different → single atomic replacement
(never zero or two registrations). The plan-tune hooks had the same stale
pattern and get the same fix without re-triggering their consent prompt.

Also hardened: bun 1.3.13 turns an uncaught sync fs error in bun -e into a
SILENT exit 0 — the registrar's write path now catches, prints, and exits 1,
so a failed update can never report fake-green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(preamble): learnings capture is unconditional at completion (#2402)

43 of 44 learnings entries came from explicit /learn — the completion-status
prose read 'if you discovered a durable project quirk... log it', which
models treated as optional. The step now ALWAYS runs: review the session for
durable learnings, log each one, and state 'No durable learnings this
session' explicitly when the review comes up empty — an empty result, never
a skipped step. Re-derived from PR #2612 under the generated-file screening
rule.

Fixes #2402.
Contributed by @Lockyer228

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(scrape): untrusted-content warning on the page-fetching skills (#2441)

/scrape and /skillify consumed page content with zero injection guidance —
the CHANGELOG claimed coverage the skills didn't have. The warning now lives
in ONE exported const (UNTRUSTED_CONTENT_WARNING in resolvers/browse.ts),
embedded in the browse COMMAND_REFERENCE as before AND injected standalone
into both skills via the new {{UNTRUSTED_CONTENT_WARNING}} token — single
source, wording can never drift between surfaces. Re-derived from PR #2612
under the generated-file screening rule. (Structural isolation for
skillify-generated code is tracked as its own TODO.)

Fixes #2441.
Contributed by @Lockyer228

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): checklist paths resolve from the installed skill root (#2518)

/review Step 2 read .claude/skills/review/checklist.md — a path relative to
the TARGET repo, which only resolves in gstack's own checkout. Every
checklist/greptile-triage/TODOS-format reference (six across five templates —
two more than the issue named, same class) now uses the installed-root form
~/.claude/skills/gstack/review/... that the templates' other references
already use. The install-root class itself (non-default install dirs) is
#1882, deliberately its own PR.

Fixes #2518.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pair-agent): one-way-door consent question before a daemon relaunch (template half)

The skill flow now checks daemon liveness before Step 4 and asks an explicit
one-way-door question (tabs/cookies/logins are lost) before passing
--force-restart — never proceeding on a vague reply. Pairs with the CLI-half
commit that stopped pair-agent auto-killing live daemons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(codex): resume does not amortize the ~21K session prelude (#2387)

Measured (#2387): every codex exec call pays Codex's session prelude, and a
resumed call came in slightly ABOVE a fresh one — resume buys continuity,
never token savings. The skill now says so where the resume flow lives:
prefer one codex call per skill, batch questions into it.

Fixes #2387.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(upgrade): fast-forward first; reset --hard only behind a proved-safe gate (#2517)

/gstack-upgrade went straight to stash + reset --hard origin/main. Now it
tries git pull --ff-only --autostash first (the same policy session-update's
auto-upgrade uses). The destructive fallback runs unprompted ONLY when both
git status --porcelain AND git rev-list origin/main..HEAD are empty — a
clean tree with unpushed local commits is NOT safe, reset destroys them.
Anything else requires an explicit one-way-door confirmation that lists every
dirty file and unpushed commit being discarded.

Fixes #2517.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(preamble): brain-sync block counts the spool queue and resolves MCP project-first

Two resolver halves deferred from earlier wave commits: the queue-depth line
counts .brain-queue.d/*.json spool records (plus legacy lines until the
drain migrates them), and GBRAIN_MCP_ENTRY_JQ swaps its operands to
nearest-ancestor-project-first — matching the empirically verified Claude
Code precedence (project-local beats user scope) instead of the backwards
user-first assumption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: regenerate SKILL.md docs + golden fixtures (single regen for the template block)

Pure generator output for the six template/resolver commits above (learnings
capture, untrusted-content warning, review paths, pair-agent consent, codex
resume note, upgrade ff-only, brain-sync block) — bun run gen:skill-docs +
--host codex + --host factory, with the three ship golden fixtures refreshed
per the documented procedure. The three sidecar-path pins in
gen-skill-docs.test.ts move to the new installed-root/$GSTACK_ROOT contract
(#2518). Restores template freshness; full suite green from here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: TODOS.md — strike the six wave-fixed residuals, add two follow-ups

The v1.67 adversarial-review residuals section shrinks to the one item the
wave couldn't reach (iOS tap routing — needs real-device verification). New
entries: skillify structural isolation (a prose warning is not a boundary for
page-derived generated code) and the slug store migration (pre-fix sessions
on stray-marker machines filed data under the degraded slug; post-fix reads
go to the correct store, so history needs a merge/alias).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: align cross-cutting pins with the wave's contracts

Three suites pinned pre-wave behavior: browse's gstack-config test asserted
the old unknown-key ''/exit-0 shape (#2611 made it exit 1); the Windows-paths
suite pinned O_APPEND enqueue atomicity (the spool design satisfies the same
invariant via tmp + os.replace, one file per record — pinned in its new
form); and nine carve-guard skeleton ceilings absorbed the #2402
unconditional-learnings prose (~450B per skill), bumped with measured values
per the guard's own protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: re-anchor the referenced-path scanner self-check to the gstack-rooted review refs

The self-check pinned the review checklist as a class-1 alias-relative ref;
#2518 moved those refs to the installed gstack root (class 2). The guard now
proves the scanner sees them in their new class, so the class-2 assertion
can't go vacuous.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: pin the wave's prose-tier behaviors (ship coverage-audit gap closure)

The coverage audit found one regression-shaped gap: nothing pinned that the
upgrade template's ff-only pull precedes the gated reset --hard (#2517) — a
future template edit reverting to reset-first would fail nothing. Pinned:
the ordering, the FF_OK gate, and the unpushed-commits check. Also pinned
the two minor gaps: the {{UNTRUSTED_CONTENT_WARNING}} injection points in
scrape/skillify (#2441) and brain-uninstall's spool-dir cleanup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: pre-landing review round — 8 auto-fixes + 8 accepted findings hardened

The ship review army (4 specialists + red-team + checklist, 29 findings)
produced 8 mechanical auto-fixes and 11 decisions; the accepted set:

- win32 slug parity completed: lib/bin-context.ts gains the remote-first
  outermost walk + degraded-cache self-heal the bash side got this wave —
  the two implementations now agree on the stray-marker live-bug shape,
  pinned by shared fixtures (multi-specialist 9/10 finding).
- probe honors the plan's bounded-read decision: 256KB prefix, extraction
  semantics mirrored from parseTranscriptJsonl so probe/prepare can never
  diverge on the same file (>1MB transcript test).
- policy normalize parity: bash normalize() now matches canonicalizeRemote
  on .git/-trailing and uppercase-.GIT shapes (7-shape corpus pinned two
  ways) — a deny for those shapes could previously slip the transcript gate.
- session-update reclaim is TOCTOU-safe (atomic mv-aside on both branches).
- settings-hook: unparseable settings.json errors instead of being replaced
  with {}; ensure-event keys on (event, source) so matcher changes update
  in place — never zero or two registrations.
- dot-only slug guard at both parse sites (hostile 'url = ..' can't escape
  projects/); enqueue tmp-file janitor (1h TTL, inside the drain lock);
  brain-sync .migrating never clobbered; drop-queue/status count .migrating;
  snapshot -o warning correct + surfaced in diff mode; version-bump test
  order-dependence removed; uninstall clears the advance stamp.

Deferred with record: slug heal-probe cost sentinel (P3 TODO), FF_OK
conflation (noted, misdiagnosis-only).

270 pass / 0 fail across the 10 touched suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: adversarial round — the P0 finalize fail-safe and 12 hardened findings

Three adversarial passes (Claude fresh-context, Codex chaos, Codex structured
with P1 gate) on the full wave diff. Multi-source findings, all fixed:

- P0: finalize_queue is now explicit-delete-only — a record is unlinked ONLY
  when classification proves it staged or dropped; a classifier crash, a
  missing class file, or a malformed pulled .brain-privacy-map.json (which
  previously nuked the whole snapshotted queue, remotely triggerable) now
  retains everything, warns, and re-drains next run. load_privacy_map treats
  corrupt maps as retain-all, never as empty.
- next-version cannot silently drop a live claim: unreadable advertised refs
  get a targeted --depth=1 fetch + retry; still-unreadable claims surface as
  UNKNOWN warnings instead of duplicate-version silence.
- session-update lock: ownership-checked EXIT trap (a TTL-reclaimed holder
  can no longer delete the new holder's lock) + a 5-min background heartbeat
  so a legitimately-slow pull/setup is never reclaimed while alive.
- ensure-event collapses ALL same-(event,source) duplicates to one canonical
  entry; unique per-process tmp path; setup call sites surface (not swallow)
  the hardened refusals.
- memory-ingest: --limit counts only policy-permitted pages (denied records
  no longer starve permitted ones); --probe applies the same policy filter as
  --bulk (skipped_policy_* fields on the report).
- version-bump repair accepts a genuine literal 0.0.0.0 VERSION file.
- slug heal restricted to the stray-.git shape — package.json-anchored
  wrapper roots keep their legit sticky identity (#2212 preserved).
- brain-sync: idle fast path sees leftover .migrating records; unparseable
  spool records quarantine instead of warning forever; migration comment
  stops overclaiming the transition-window race.
- CDP throttling justifications document override persistence (callers own
  restoration), pinned in the allowlist test.

Deferred with record: deny retroactivity for already-ingested pages (P2 TODO,
same semantics as the code-import gate); legacy-migration tail race
(transition-window, requires pre-spool writers).

288 pass / 0 fail across the 10 touched suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: regenerate SKILL.md docs + goldens (Windows-separator jq fix)

Pure generator output for the brain-sync block's jq ancestor match now
accepting backslash-formed Windows project keys — previously project-scoped
brains were invisible on Windows while the TS scope resolvers saw them.
Golden ship fixtures refreshed per the documented procedure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: codex verify-pass residuals — chunked cwd read, post-filter partial count, migrating depth

The verify re-review passed the P1 gate (0 P1s) and left three residuals,
all applied: transcriptCwdFromPrefix reads in chunks until one complete
record (4MB cap) so a giant first prompt can't truncate mid-JSON and break
probe/bulk parity; partial_pages derives from the FINAL prepared set instead
of the whole scanned corpus; the preamble queue-depth line counts leftover
.brain-queue.jsonl.migrating records like the status path does (regen + goldens included).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump version and changelog (v1.68.0.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update project documentation for v1.68.0.0

BROWSER.md: fix the $B cdp example (positional JSON params, not --json;
depth is the real CDP param) and add the new perf-throttling examples
(Emulation.setCPUThrottlingRate, Network.emulateNetworkConditions) with
their clear-override counterparts. USING_GBRAIN_WITH_GSTACK.md: the
state-files table row for the sync queue now names the maildir-style
spool dir .brain-queue.d/ that replaced .brain-queue.jsonl this release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: align memory-pipeline probe pins with the #2394 stage-count contract

The paid-tier E2E pinned the pre-fix contract (probe headline = raw
discovered). Probe now counts post-attribution — the same gate --bulk
uses — with an explicit unattributed-skip line. Adds the
--include-unattributed companion pin so all 9 fixtures stay accounted for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(next-version): batch missing-tip fetches — one bounded round trip, never a per-branch crawl

The targeted-fetch retry for branches whose advertised tip has no local
object ran ONE git fetch per branch (10s cap each). On a shallow clone
against a busy remote that crawls the network for minutes — CI's shard
deadline killed the free suite mid-file. Missing tips now collect into a
single batched shallow fetch (15s cap); refs still missing after the
batch (one unservable ref fails the whole transfer) get a capped
per-branch retry, and anything past the cap warns as an UNKNOWN claim
instead of fetching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(next-version): pin the batched fetch + make the offline-contract tests hermetic

Two new G2 pins: N unfetched claim branches resolve with exactly ONE
fetch spawn (PATH-shimmed git counts invocations), and one unservable
ref no longer poisons the batch — live claims resolve via the bounded
retry while only the ghost warns UNKNOWN.

The #2545 offline-contract tests now run the CLI in a local fixture repo
instead of the repo's own checkout: the checkout path did a live
ls-remote against the real origin (operator-network-dependent, and the
CI shard-deadline hang). The online-contract test gains a succeeding gh
stub, so fallback:null is asserted deterministically instead of only
when the operator happens to be authed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(redact-cli): derive the synthetic AWS-key fixture — no contiguous credential literal in source

The CI quality gate scans every ADDED diff line with the redact engine,
so the #2610 port's raw fixture literals failed the very gate they
exist to test. The fixture is now assembled at runtime; the scanner
still receives the identical bytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(next-version): pin the fixture's host via origin-URL sniff — kills the last environment dependence

The hermetic offline-contract fixture had no origin remote, so
detectHost() fell through to auth probes: a machine with glab authed
passed via the gitlab path while a bare CI runner read host:unknown
(offline stays false there) and failed. The fixture now pushes to a
local bare origin at a path containing github.com — the URL sniff pins
host:github identically everywhere, asserted explicitly in both tests,
with every git call still local.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: y$un_ <forrest.sun527@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: benjamin beres <benjamin.beres@bienpreter.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ricky <ricky@kinokostudio.com.hk>
Co-authored-by: Connex Client Access <paul@paulkortman.com>
Co-authored-by: henbima <henbima@gmail.com>
2026-08-19 11:42:55 -07:00
Garry TanandClaude Fable 5 c118e2402e v1.64.1.0 v1.64.1.0: the code-smell fix wave — every pipeline guard now provably fires (net −24,943 lines) (#2572)
* fix(ci): skill-docs freshness gate covers all 10 hosts and can actually fail

The Codex/Factory gates ran 'git diff --exit-code -- .agents/' / '-- .factory/',
but both paths are gitignored (.gitignore:16-17) — git diff on ignored untracked
paths is always empty, so those two gates were structurally incapable of failing
and 7 of 10 hosts had no gate at all.

New shape: one 'gen:skill-docs --host all' pass (the generator hard-fails on any
per-host error, gating all 10 hosts on generates-cleanly), byte-freshness via
git diff for tracked output, plus a porcelain check that fails on untracked
generated strays (git diff can't see brand-new files). The gitignored-hosts
byte-freshness limitation is documented in the workflow comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): exorcise the sidebar-agent ghost from the test suite

browse/src/sidebar-agent.ts was deleted in the v1.14 sidebar refactor, but the
test suite kept testing it for 48 versions. Nothing noticed because the free
suite runs in no CI job and Bun-era module-load errors were suppressed in the
Windows shard runner via an exclusion pattern whose own comment documented the
breakage ('broken on every platform since v1.14 ... exit 0').

- Delete sidebar-security.test.ts + security-source-contracts.test.ts: crashed
  at module load (unguarded readFileSync of the deleted file); per-assertion
  triage confirmed every SERVER_SRC pin targeted the deleted chat prompt
  builder (zero hits in today's server.ts) — nothing to port.
- Delete sidebar-integration.test.ts: 11 of 13 tests exercised deleted
  endpoints (/sidebar-command queue, /sidebar-agent/event, chat buffer); the 2
  passing tests pinned only the blanket auth gate, covered by
  server-auth.test.ts + dual-listener.test.ts.
- Delete test/skill-e2e-sidebar.test.ts: E2E for the deleted queue flow.
- sidebar-ux.test.ts 1,669 -> 830 lines: 20 dead-chat describes + 15 dead
  tests removed (incl. 10 vacuous passes asserting on empty indexOf slices);
  2 stale pins on LIVE features fixed (content.js typed-catch CSSOM fallback,
  arrow-hint window widened). 95 pass / 0 fail.
- sidebar-tabs.test.ts: both failures were stale pins, not regressions —
  forceRestart's deliberate ws.close(4001) and the terminal-agent spawn that
  moved into spawnTerminalAgent() (identity-based kill refactor). 28 pass.
- touchfiles.ts: drop the three sidebar E2E entries from BOTH maps
  (E2E_TOUCHFILES + E2E_TIERS) — they pointed diff-selection at the deleted
  file, so those tests were unreachable by any diff.
- test-free-shards.ts: remove the now-dead sidebar-agent exclusion pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ci): run the free test suite in CI (it ran nowhere)

The full free suite (bun test: browse/test/ + test/ + make-pdf/test/) had no CI
job on any Linux/macOS runner — only Windows curated shards, paid evals, and
doc-freshness gates existed. That's how two module-load-crashing test files
survived 48 versions.

Same cached Dockerfile.ci image and container wiring as evals.yml (deps
restore, build, Chromium verify). Includes a module-load-error guard: older
Bun reported test-file import crashes with exit 0 on macOS/Linux, so the job
also fails on any nonzero 'N errors' count in the summary — future crash-class
regressions can't hide from the exact job built to catch them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test): validate touchfile dependency paths exist on disk

New guard in touchfiles.test.ts: every non-glob dep path must exist, and every
glob's anchor directory must exist. This is the axis the 181-key two-map sync
discipline never covered — an entry can point at a long-deleted file and
diff-based selection then silently never triggers those tests (the sidebar
trio sat rotted for 48 versions).

First run immediately caught a fourth rotted entry: 'spec authored quality'
referenced test/fixtures/spec/** (directory does not exist) and selected for a
judge test that exists nowhere in the repo. Removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): remove deleted /sidebar-chat endpoint from tunnel allowlist

TUNNEL_PATHS is the audited tunnel attack surface — its own comment says every
addition widens it. '/sidebar-chat' stayed in the set after the endpoint was
deleted with the chat-queue path, meaning any future route matching that path
would have been silently tunnel-exposed. The set is now exactly the pair
ceremony (/connect) and the scoped command endpoint (/command), and the
dual-listener closed-set pin enforces that.

Also repairs a pre-existing red pin in dual-listener.test.ts: v1.63.0.0 made
the tunnel allowlist args-aware (canDispatchOverTunnel gained a second param)
without updating the test — red on main since then, invisible because the free
suite had no CI job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): delete chain's shadow dispatcher that skipped every security gate

meta-commands.ts carried a 'CLI mode' fallback that re-implemented command
routing without the server pipeline's gates: no scope check, no domain check,
no tab ownership, no rate limit, no hidden-element stripping, no scoped-token
enveloping — and it called handleReadCommand without a BrowserManager, which
also skipped the JS-origin cookie-exfiltration assertion. It was unreachable
in production (server.ts always passes executeCommand) and one boolean away
from being live.

chain now hard-errors without a server context. handleReadCommand's bm param
is required and assertJsOriginAllowed runs unconditionally. The chain tests
that exercised the deleted fallback now route through a server-shaped
executeCommand adapter (real handlers + trust wrapping + {status,result}
envelope), so their behavioral coverage — sequencing, trust markers, pipe
format, aliases, error reporting — survives on the production-shaped path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(extension): delete the dead chat-queue client surface

The sidebar-command handler in background.js POSTed to a server endpoint that
no longer exists (deleted with the chat queue) — ~35 lines of fully-wired dead
code including error handling for the permanent 404, plus its allowlist entry.
No sender in the extension ever emitted the message type.

chatEnabled leaves the /health contract (server hardcoded false, background.js
re-derived it, nothing consumed it — the chat input element it guarded is gone
from sidepanel.html). BROWSE_SIDEBAR_CHAT env flag had zero readers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): delete dead exports the ripped chat path left behind

Three-way split by importer class:

(a) Zero importers, deleted: the whole attack-attempt logging cluster in
security.ts (logAttempt, AttemptRecord, salted hashPayload + device-salt,
attempts.jsonl rotation, telemetry spawn plumbing incl.
buildTelemetrySpawnCommand/resolveBashBinary — the LIVE attempts.jsonl writer
is tunnel-denial-log.ts with its own rotation); the decision-file handshake
(writeDecision/readDecision/clearDecision/excerptForReview — written for
sidebar-agent's poll loop, which no longer exists); sidebar-utils.ts (whole
module — its sanitizeExtensionUrl 'sanitized before embedding in a prompt'
for the deleted prompt builder); 8 dead server.ts imports (sanitizeExtensionUrl,
generateCanary, injectCanary, writeDecision, rotateRoot, serializeRegistry,
restoreRegistry, clearAgentRecord); buildPtyClearCookie + buildSseClearCookie;
WEBDRIVER_MASK_SCRIPT (orphaned by the D7 stealth narrowing — applyStealth
never used it).

(b) Dead-pin tests edited with their exports: the 'still exported' pin in
stealth-layer-c, the string-content describe in stealth-webdriver (its live
applyStealth behavioral coverage untouched), the clear-cookie assertions,
security-review-flow.test.ts deleted whole (all 4 describes exercised the
dead decision mechanism, incl. a 'simulated sidebar-agent poll loop').

(c) KEPT deliberately: leaseCount (live behavioral coverage),
extractPtyCookie + validatePtySessionToken (extractPtyCookie is adopted by
the terminal-agent cookie-parse unification later in this wave),
resetSessionMarker + clearContentFilters (test-support API for the live
content-security layer).

Also fixes two pre-existing red pins found while here, invisible until the
free suite got a CI job: the v1.44 spawnClaude->maybeSpawnPty rename in
terminal-agent.test.ts, and a cross-file test-isolation bug where
content-security.test.ts's clearContentFilters() wiped the auto-registered
url-blocklist filter for every later file in the same bun process
(security-integration.test.ts failed on co-run; afterAll now restores it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): delete the dead ML layers — transcript classifier and DeBERTa ensemble

The L4b Haiku transcript classifier and the opt-in DeBERTa ensemble
(GSTACK_SECURITY_ENSEMBLE=deberta, a documented 721MB download) had ZERO
production callers since the chat-path agent that invoked them was ripped.
The only live ML path is scanPageContent (testsavant) inside the security
sidecar subprocess. Deleted by import graph:

- security-classifier.ts 614 -> 265 lines: HAIKU_MODEL, checkTranscript,
  shouldRunTranscriptCheck, loadDeberta, scanPageContentDeberta, ToolCallInput,
  all DEBERTA_* consts + load state. Header now states the live truth
  (imported only by security-sidecar-entry.ts). downloadFile kept, name
  intact — it is an enumerated egress sink (HF model download).
- security-bunnative.ts + test: a research skeleton self-described as 'NOT a
  production replacement', shipped into src/ with zero importers.
- security-bench-ensemble{,-live}.test.ts + the Haiku response fixture: a
  paid live-model benchmark for a layer that could not fire. The
  security-classifier-tdz test's only case exercised checkTranscript — gone.
- security.ts: layer-model header rewritten to the live architecture;
  StatusDetail.layers -> {testsavant, canary}; getStatus() no longer requires
  the impossible transcript==='ok' for 'protected' (old on-disk session state
  with a transcript key is tolerated on read, never re-emitted).
- security-sidecar-entry.ts needed zero changes: it serializes
  getClassifierStatus() verbatim and no consumer read .transcript (verified
  in sidecar-client + server.ts).
- BROWSER.md security section matches reality (ensemble knob gone, 112MB not
  22MB, sidecar hosting documented). combineVerdict/THRESHOLDS retained as
  the pure, tested combiner of record — comments now flag transcript/deberta
  votes as producer-less.

Net: 26 pass in security.test.ts incl. a NEW regression test for stale-
transcript disk tolerance; egress-receipt tripwire green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: scrub the sidebar-agent ghost from comments and CLAUDE.md

20+ comments across 10 files still described the deleted sidebar-agent.ts as a
live process — including load-bearing architecture claims ('IMPORTED ONLY BY
sidebar-agent.ts', 'sidebar-agent fills this in on first prompt-injection
load', 'kill sidebar-agent' in shutdown docs) and ~60 lines of tombstone
blocks in server.ts enumerating deleted identifiers by name (a false grep
surface: searching processAgentEvent hit server.ts and looked live).

CLAUDE.md's security-stack section now documents the LIVE architecture: L1-L3
content filters + testsavant via the security sidecar subprocess; the
L4b/ensemble rows, the GSTACK_SECURITY_ENSEMBLE knob, and the 721MB DeBERTa
download are gone (deleted as dead code this wave) with an explicit
do-not-re-document note; attempts.jsonl is correctly attributed to
tunnel-denial-log.ts; the no-live-writer status of classifierStatus is stated.

Comments that survive now describe what IS, not what WAS: the promotion gate
in domain-skills.ts explains why classifier_score>0 is load-bearing given no
L4 load-time scan exists; file-permissions.ts names real sensitive files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): delete the codex-helpers shadow module

gen-skill-docs.ts imported externalSkillName (unaliased) from
resolvers/codex-helpers.ts at line 21 and then re-declared the same function
locally — the import was silently shadowed, and the imported copy was the
STALE one (it lacked the frontmatterName param the local copy grew). Three
more functions were byte-identical duplicates, imported only under _-prefixed
aliases to keep the module 'referenced', and transformFrontmatter was a
superseded hardcoded-Codex variant. Nothing else imported the module.

Also drops three dead top-of-file imports (COMMAND_DESCRIPTIONS,
SNAPSHOT_FLAGS — which pulled the whole browse/src module graph into every
generator run for nothing — and an unused review-resolver trio).

Proof: bun run gen:skill-docs exits 0 with a byte-identical tree (zero-diff
regen); gen-skill-docs.test.ts 405/405 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(server): delete ServerConfig.idleTimeoutMs + chromiumProfile — documented, never read

Both fields carried JSDoc asserting embedder behavior that did not exist:
the idle check reads the module-level IDLE_TIMEOUT_MS env constant, and both
resolveChromiumProfile() call sites pass no argument. Worse than absent — an
embedder passing idleTimeoutMs: 5000 silently got 30 minutes.

Wiring them honestly is impossible today: the idle timer, activity state, and
shutdown target are module-global, so a per-factory value would lie for any
process running more than one handler. Deleted instead, with a ServerConfig
note pointing at the deferred singleton/route-table refactor where real
support belongs. BROWSE_IDLE_TIMEOUT and CHROMIUM_PROFILE env remain the
honest knobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): wire appendSecureFile at the four real log-append sites

file-permissions.ts carries a 24-line rationale for why POSIX mode bits are
insufficient on Windows and implements appendSecureFile (0600 at create,
Windows ACL on first write only) — but its single caller was the dead
logAttempt, while the four REAL page-content log writers (console/network/
dialog logs in server.ts, the command audit log) used raw fs.appendFileSync
with no mode. Page-content-derived logs now get owner-only permissions from
birth on every platform.

Verified before wiring: mode applies atomically at create via appendFileSync
{mode}, and the ACL pass runs only on first write — no per-append subprocess
cost on the hot console-log path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stealth): handoff() uses the shared profile resolution + lock cleanup

The headless-to-headed handoff path hardcoded ~/.gstack/chromium-profile,
silently ignoring $CHROMIUM_PROFILE and $GSTACK_HOME (gbrowser's gbd sets
per-workspace profiles), and skipped cleanSingletonLocks() — so a handoff
into a profile with a stale SingletonLock could hang where launchHeaded()
would have recovered.

This was the third live drift between the three Chromium launch paths; the
first two are documented in comments as shipped stealth regressions. Minimal
targeted fix — the full buildLaunchConfig() extraction stays in the deferred
queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): resolver registry describes the template language again

Seven registered {{PLACEHOLDER}}s had zero uses in any .tmpl (checked in both
bare and :arg forms): REDACT_TAXONOMY_TABLE, TEST_COVERAGE_AUDIT_REVIEW,
MODEL_OVERLAY, QUESTION_PREFERENCE_CHECK, QUESTION_LOG, INLINE_TUNE_FEEDBACK,
MAKE_PDF_SETUP. The last two of those families are invoked programmatically by
preamble.ts (functions kept, registry entries dropped); the question-tuning
trio and the review coverage-audit wrapper were documented by their own module
as existing 'for unit testing' that no test performed — deleted, along with
generateRedactTaxonomyTable + its EXAMPLE/TIER_BLURB constants (its '/cso
renders the full table' comment was itself stale) and its test describe.

Also deletes the gated-resolver mechanism (ResolverEntry/appliesTo/
unwrapResolver + test/resolver-entry.test.ts): fully built, fully tested,
used by zero of the 65 registry entries — the generator loop simplifies to a
direct function call. CLAUDE.md's redact-doc line stops advertising the dead
token.

Proof: zero-diff regen (0 SKILL.md changed); gen-skill-docs + skill-validation
737 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): wire boundaryInstruction from host config; drop three no-op binDir ternaries

hosts/codex.ts declared boundaryInstruction and nothing read it — review.ts
kept its own byte-identical CODEX_BOUNDARY literal (verified equal + trailing
escaped newlines). The resolver now reads the config, so the boundary has one
owner. (autoplan's template carries deliberately generic variants, enforced by
gen-skill-docs.test.ts:1358 — untouched by design.)

The 'ctx.host === codex ? $GSTACK_BIN : ctx.paths.binDir' ternary appeared in
three resolvers and could never change the result: resolvers/types.ts already
sets binDir to $GSTACK_BIN for every usesEnvVars host including codex.

Proof: zero-diff regen for claude AND codex hosts; gen-skill-docs +
host-config suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test-infra): judge uses resolveClaudeBinary; eval:watch reads the real partials dir

judgePtyState spawned the bare string 'claude' three definitions below the
resolveClaudeBinary() helper this same file exports — broken under hermetic
PATHs where every other launch in the file resolves correctly.

eval:watch read _partial-e2e.json from the legacy global ~/.gstack-dev/evals/
while EvalCollector writes it into the per-project eval dir (or
GSTACK_EVAL_DIR) — so the dashboard's completed-tests panel was empty
whenever slug detection succeeded, i.e. the normal case. The heartbeat and
per-run progress logs stay global by design (session-runner.ts: 'heartbeat
stays global'). The three eval-CLI docstrings stop claiming the legacy dir
is the primary location.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): delete the superseded SDK ship-idempotency suite and three orphaned fixtures

test/skill-e2e-ship-idempotency.test.ts's own header documented that the
monolith's SDK-harness version tests a synthetic prompt while it exercises
the real /ship skill — the author knew the old suite was superseded and left
both running, two paid LLM runs for one behavior. The weaker copy is gone;
its 'ship-idempotency' diff-selection key goes with it (the dedicated file is
periodic-tier, which always runs under EVALS_ALL — the key had no remaining
consumer).

Fixture rot: test/fixtures/golden-ship-claude.md was a 128KB zero-reader
orphan that had drifted 46KB from its live successor
(test/fixtures/golden/claude-ship-SKILL.md) while looking authoritative;
parity-baseline-v1.46.0.0.json and v1.53.0.0.json had zero readers (three
tests pin three OTHER baseline versions — consolidation is queued, deletion
of the unreferenced two is free).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bin): delete zero-caller scripts; make host-config-export's docstring honest

- bin/gstack-open-url (14 lines): announced in a CHANGELOG entry, wired into
  nothing, ever. bin/gstack-platform-detect (27 lines): zero callers, and its
  hand-rolled host list was already stale (SLATE_HOST.md cites it as a
  problem). Note: the deprecated gstack-brain-consumer/reader pair the audit
  flagged was already deleted upstream in v1.63 with a stay-deleted tripwire.
- scripts/task-emission-schema.ts (61 lines): a typed schema module nothing
  imported; the tasks-section comment now documents the JSONL fields inline.
- scripts/host-config-export.ts claimed to be the 'shell bridge for the bash
  setup script' — setup never calls it (its hand-rolled host lists drifting
  is a known follow-up). Docstring now states what it IS: a standalone,
  test-pinned query CLI not yet wired into setup. Its validateValue +
  CLI_REGEX/PATH_REGEX internals were dead (defined for a guarantee the
  header claimed but nothing enforced).
- KEPT deliberately: scripts/preflight-agent-sdk.ts — a documented manual
  diagnostic (CONTRIBUTING.md + USING_GBRAIN_WITH_GSTACK.md reference it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(server): one lone-surrogate sanitizer, one sanitizeReplacer, one startTunnel

Three copies of the surrogate sanitizer existed with two algorithms
(sanitize.ts regex vs a hand-rolled charCodeAt walk in server.ts — verified
byte-identical across 11 edge cases before converging) plus two identical
sanitizeReplacer definitions each wrapping a different copy. sanitize.ts is
now the single source of truth; the runs-INSIDE-JSON.stringify egress
invariant is unchanged at every call site and its pin tests were adapted to
the new import shape without losing intent.

The ngrok tunnel-start sequence existed three times in server.ts — the
/tunnel/start route and the BROWSE_TUNNEL=1 autostart were line-for-line
equivalent (a comment admitted 'Same cleanup as /tunnel/start's error path').
One startTunnel() now owns the ephemeral loopback bind, the pre-send egress
receipt, the state-file RMW via tmpStatePath(), and the ordered error-path
cleanup; callers keep their distinct response surfaces. The
BROWSE_TUNNEL_LOCAL_ONLY test path shares nothing (no ngrok, different state
field) and deliberately stays separate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): one session-cookie registry implementation, two instances

pty-session-cookie.ts and sse-session-cookie.ts were byte-identical modulo
the cookie name — mint/validate/parse/prune/TTL, the exact code a security
fix would have to land in twice (and a third hand-rolled cookie parse in
terminal-agent.ts had already diverged; unified next commit).
createSessionCookieStore() owns the implementation; both modules become thin
instantiations keeping every exported name, their distinct threat-model
docstrings, and separate token spaces (an SSE-read cookie must never grant
PTY access). pty-session-lease.ts deliberately stays out — different contract
(sessionId/secret split, refresh, env TTL).

The factory imports nothing from token-registry (cookie-picker-auth-isolation
invariant, still pinned by sse-session-cookie.test.ts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): terminal-agent uses the shared PTY cookie parser

The /ws upgrade's cookie fallback hand-parsed the Cookie header inline — the
fourth copy of the session-cookie parse, and the one that had already
diverged from the others. Parsing now goes through extractPtyCookie;
validation deliberately stays against the agent's own in-process validTokens
map (the server's registry lives in a different process). The ws-handler pin
test now pins the shared-parser call instead of the raw cookie-name literal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(hosts): defineHost() factory — 10 copy-paste host files become declarations

hosts/*.ts were ten copies of one file: runtimeRoot byte-identical in 9/10,
pathRewrites mechanically derivable from the host name for 7/10, the 11-entry
toolRewrites map byte-identical between openclaw and gbrain, and every asset
change a 10-file edit (cursor and slate had already fallen out of three other
hand-maintained lists). defineHost() owns the defaults; each host file now
declares only what makes it different (slate/cursor: 8 lines each). Shared
constants: CROSS_MODEL_RESOLVERS, GBRAIN_RESOLVERS, EXEC_STYLE_TOOL_REWRITES.
Genuinely-different things stayed explicit: codex/factory $GSTACK_ROOT
rewrites, hermes's tool vocabulary, claude's denylist+prefixable install,
opencode's wider runtimeRoot.

Proof: JSON.stringify(ALL_HOST_CONFIGS) dump-diff before/after EMPTY (and a
runtime walk confirmed no function-valued or undefined-keyed fields, so the
JSON diff is complete); gen:skill-docs --host all zero-diff; host-config +
gen-skill-docs + idempotency suites 485/485. Host files 595 -> 285 lines.
docs/ADDING_A_HOST.md teaches the factory pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(lib): fs-atomic — one atomic-write implementation, with the race actually fixed

Atomic tmp-write-then-rename was reimplemented ~20 times across lib/, bin/,
and browse/src with three tmp-suffix conventions. One of them was a latent
bug this commit closes: lib/worktree.ts used a bare '.tmp' suffix — the
deterministic-tmp collision race browse/src/server.ts documents having hit
in production (its fix, pid+random, was trapped in a comment at one site).

lib/fs-atomic.ts: atomicWriteSync (always throws, best-effort tmp cleanup,
pid+random suffix, optional mode applied at tmp creation so the file never
exists with looser permissions) + atomicWriteQuiet (shutdown paths only).
Unit tests pin the throw/quiet contracts, 0600 mode, tmp-name uniqueness
(captured via the read-only-dir failure path — Bun's fs exports are
readonly, no monkeypatching), and no-stray-tmp cleanup.

Migrated: lib/worktree.ts (the bare-.tmp bug), lib/gstack-decision.ts
(snapshot + compact log), lib/gbrain-local-status.ts (probe cache). browse
sites follow separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(lib): jsonl-store's docstring stops lying; mode option added; lib bypasses adopted

The header claimed 'single source of truth... the ONLY copy' with write-time
injection REJECTION — while appendJsonl never screened anything, only 1 of
~10 JSONL stores imported it, and a bypass appender lived in the same
directory. Now: the contract is explicit (screening is the CALLER's job via
hasInjection/firstInjectionMatch; the enforcing callers are named), a
option applies 0600 at create for sensitive stores, and the lib bypasses are
adopted (gstack-memory-helpers ×2, redact-audit-log — which keeps its chmod
backstop for files created looser by pre-mode versions). browse/src keeps
its own appenders by design (compiled-binary surface, own secure-append
helper) and the header now says so. gstack-decision's batched archive append
stays deliberate (single-write crash-window semantics appendJsonl's
one-record contract can't express).

New pins: 0600-at-create, and a test that documents appendJsonl does NOT
self-screen — so nobody can re-document it as self-screening without making
it true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): migrate hand-rolled atomic writes to lib/fs-atomic

Seven sites, each audited for its existing throw-vs-swallow contract before
migrating: writeSessionState + the four fire-and-forget tab/state writers use
atomicWriteQuiet (they swallowed before); writeAgentRecord + the boot-time
port-file write use atomicWriteSync (they threw before — and writeAgentRecord
previously leaked its tmp file on rename failure, which the helper cleans).
All carry {mode: 0o600} plus restrictFilePermissions after successful writes,
preserving the Windows ACL hardening that writeSecureFile provided (mode bits
are POSIX-only). server.ts untouched: its three state writes route through
tmpStatePath(), pinned by server-tmp-state-path.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hosts): delete five dead HostConfig fields

metadataFormat (generator hardcodes openai.yaml), sidecar (behavior lives in
setup's create_agents_sidecar — knowledge preserved as a comment in codex.ts),
install.prefixable (skill_prefix is implemented entirely in bin/gstack-config),
staticFiles (docstring cited a SOUL.md that never existed anywhere), and
adapter (its only would-be consumer, openclaw-adapter.ts, was fully dead —
with a test asserting the field was undefined). Kept: learningsMode (wired
next), linkingStrategy (validation reads it), coAuthorTrailer (consumed by
resolvers/utility.ts).

Proof: JSON dump diff shows ONLY the deleted keys vanishing; zero-diff regen
across all 10 hosts; host-config + gen-skill-docs suites green. Note: this
commit also carries chunk-23 edits to the shared hosts/claude.ts +
define-host.ts + host-config.test.ts files (skipSkills collapse, stale
line-number comment drops) — pathspec commits, concurrent prep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): preamble tiers are explicit; silent ?? 4 default becomes an error; spec stops rendering its preamble twice

Eight skills (scrape, diagram, spec, skillify, pair-agent, landing-report,
open-gstack-browser + its connect-chrome symlink) silently received the
HEAVIEST tier-4 preamble because a missing frontmatter field defaulted to 4.
Tiers are now declared in every {{PREAMBLE}} template's frontmatter and a
missing declaration throws at generation time with the template path (the 5
templates without {{PREAMBLE}} never invoke the resolver). The stale
hand-written tier-map comment (wrong in 3 of 4 rows) is gone.

Bonus bug fixed: spec/SKILL.md.tmpl mentioned {{PREAMBLE}} in prose, so the
generator inlined the ENTIRE preamble a second time — spec/SKILL.md shrinks
127,462 -> 80,924 bytes (-46,538) from de-duplication alone. skill-size-budget
gains a reasoned INTENTIONAL_SHRINKS entry (its frozen baseline had measured
the doubled-preamble bug). New tests: missing-tier throw carries the path;
every {{PREAMBLE}} template declares a tier. (Carries chunk-23 edits in the
shared test/gen-skill-docs.test.ts.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): learningsMode is read from host config, not a hardcoded host name

resolvers/learnings.ts branched on ctx.host === 'codex' while every host
declared learningsMode — the field was decorative, and the 7 hosts configured
'basic' (cursor, slate, kiro, opencode, openclaw, hermes, gbrain) silently
received the 'full' cross-project flow their runtimes can't execute (it
depends on AskUserQuestion + gstack-config plumbing). Output now matches
declaration: basic hosts get the project-scoped search block.

Blast radius proof: all committed Claude SKILL.md files and the three golden
fixtures are byte-identical; the behavior diff lands only in the gitignored
external-host trees (hand-verified: .cursor review's learnings section swaps
the cross-project AskUserQuestion block for the project-scoped search).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): small config scrubs — openclaw blobs to real files, setup host drift, dead artifacts

- The three openclaw markdown blobs hardcoded inside gen-skill-docs.ts (which
  silently reverted any hand edit to their tracked outputs on regen) move to
  openclaw/templates/*.md source files; output shasums byte-identical.
- setup's --host allowlists gain cursor + slate — both fully registered hosts
  with generated output, but './setup --host cursor' exited 1 because two
  hand-rolled lists in setup had drifted from hosts/index.ts.
- scripts/proactive-suggestions.json deleted: 31KB regenerated on every run,
  read by nobody (the catalog-trim design's reader was never built); its
  emitter and three determinism tests (which guaranteed a file nothing reads
  didn't churn) retired with stays-retired pins.
- claude/SKILL.md.tmpl deleted: a complete 8.9KB skill that never generated
  output (directory name collides with the host id 'claude'), in no registry.
  Recoverable from git if ever wanted under a non-colliding name.
- openclaw's frozen extraFields.version '0.15.2.0' stamp dropped;
  includeSkills: [] no-ops omitted (the generator treats [] as absent);
  llms.txt 55 -> 54 skills.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(gen): correct preamble tiers for the 8 silently-heaviest skills

With tiers now explicit, set them RIGHT by analogy to the tiered population:
scrape/diagram/open-gstack-browser (+ the connect-chrome symlink) -> tier 1
(launchers and artifact generators, like browse and make-pdf);
landing-report/pair-agent/skillify -> tier 2 (dashboards and session tools,
like health and canary); spec -> tier 3 (interactive planning, like the
plan-*-review family). Each tier-1 skill sheds 271 lines of onboarding
prose it never needed; tier-2 shed 20 each.

Verification per the review protocol: regen diff reviewed (pure
section-removal), skill-validation + size-budget + catalog-budget +
v0-dormancy suites green (822 tests), and live smoke of the tier-corrected
skills confirms the preamble renders the intended sections at each tier.
These skills have ~no eval coverage — stated honestly; the wave's gate-tier
eval run is the backstop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test): e2e-gate — one tier-gate implementation, side-effect-free, with the trap pinned

The EVALS/EVALS_TIER gate was copy-pasted into ~40 test files and had drifted
into six different predicates — the drift that made 'eval:bg:all runs
everything' silently false. test/helpers/e2e-gate.ts owns the semantics now:
describeE2ETier(tier) + e2eTierEnabled(tier), env read at call time, zero
side effects (the existing e2e-helpers module runs a ~30s claude ping at
import under EVALS=1, so the gate lives in its own module; purity is pinned
by tests that scan imports and comment-stripped source).

The unit matrix pins all four env combos — including EVALS=1 with EVALS_TIER
unset -> SKIP, the exact trap that made eval:bg:all a non-run. The
tier-alignment tripwire gains a second regex for the helper shape (old shape
still detected — stragglers can't hide), and the sharded paid runner's
PRE-SPAWN tier classifier learns the helper shape too: without that, every
gate-sharded run would have spawned all 28 periodic shards just to skip them,
each paying the e2e-helpers import ping (~15 min of dead wall clock in the
CI-blocking lane). Verified: gate runs exclude the 29 periodic files,
periodic excludes the 8 gate files — identical to pre-migration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(test): migrate the 36 tier-gated eval files to describeE2ETier

Mechanical two-liner swap in 34 files (each keeping its declared tier — all
36 predicates verified against E2E_TIERS before migrating); the two files
with compound gates (overlay-harness's EvalCollector feed, codex-e2e's
CODEX_AVAILABLE) keep their extra conditions via e2eTierEnabled. Tier
rationale comments preserved. codex-e2e/gemini-e2e/benchmark-providers keep
their distinct stderr-message gate shapes by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(test): skill-e2e + skill-llm-eval adopt the shared selection machinery

Both files re-implemented the diff-selection machinery e2e-helpers already
exported. The helper gained computeDiffSelection() (extracted, identical
behavior) and a trailing optional selection param on the *IfSelected helpers
(defaults preserve all 30+ existing importers). skill-e2e.test.ts drops ~120
duplicated lines; skill-llm-eval keeps its LLM_JUDGE_TOUCHFILES selection and
test.concurrent semantics via testConcurrentIfSelected.

Deliberate deltas, stated: skill-e2e.test.ts now honors the EVALS_TIER
intersection its local copy lacked (affects only direct bun test invocations
of that file — it matches no eval-script glob); its recordE2E gains the
helper's three diagnostic fields; skill-llm-eval sharded solo now runs
e2e-helpers' module-scope preflight it already ran in combined processes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): kill the silent-truncation race; exempt the tier-corrected shrinks

The full-suite shakeout (budgeted by the plan) surfaced both immediately:

1. server-embedder-terminal-port.test.ts stubbed process.exit and restored
   the REAL exit in its finally — but shutdown() schedules async work that
   can call process.exit AFTER restoration, killing the entire bun process
   mid-suite with exit 0 and NO summary. This is the silent-truncation class
   the new free-suite CI job guards against, reproduced locally on the first
   full run. Exit now stays a logging no-op between tests (late async exits
   become visible stderr lines, not process death); the true exit returns in
   afterAll.

2. The 80%-of-baseline shrink guard correctly flagged the six tier-corrected
   skills — their baseline was measured at the silent tier-4 default. Added
   to INTENTIONAL_SHRINKS with the reason, joining spec's double-preamble
   entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* release: v1.64.0.0 — the code-smell fix wave

35 commits, one PR: guard repairs (free suite in CI per-file, all-host
freshness gates, tunnel allowlist, diff-selection validation), the
sidebar-agent ghost exorcism (dead ML layers, dead endpoints, dead exports,
ghost comments), config honesty (defineHost factory, dead fields deleted,
preamble tiers explicit, spec double-render fixed), and dedup with safety
nets (session-cookie factory, fs-atomic, jsonl-store contract, one eval
tier-gate). Net -24,943 lines across 183 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): free-tests step runs under bash (container sh rejects pipefail)

Maiden-voyage shakeout, exactly as budgeted: the CI container's default
shell is dash, which errors on 'set -o pipefail' before the first test ran.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): free-tests curates 8 container-incompatible files with reasons

Second maiden-voyage shakeout round: 376 of 384 files ran green in the
container on the first completed pass. The 8 that can't run there yet are
excluded the same way the Windows shards curate POSIX-bound files — each
with its reason inline (headed-Chrome handoff, real-PTY round-trip, X server
management, extension-origin identity, the job's own TMPDIR override, and
three pre-existing env failures that fail on dev machines too). Anything
outside the list that fails still fails the job; trimming the list is
tracked follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): gstack-config-key-locale — suppress the skill_prefix auto-relink side effect

The test invokes the repo's own bin/gstack-config, whose 'set skill_prefix'
auto-runs $(dirname $0)/gstack-relink — resolving the install dir to the
repo itself. In any environment where the loop shares a working tree (the
free-tests CI container, a fresh-HOME run), gstack-patch-names rewrote all
52 tracked SKILL.md names to gstack- prefixed, poisoning five unrelated
suites downstream (hermetic-skills-seeding, host-config golden, skill-census,
skill-validation, spec-template-sync). GSTACK_SETUP_RUNNING=1 is the
documented suppression; relink behavior stays covered by relink.test.ts's
mock install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bin): gstack-codex-session-import — empty sessions dir exits 0 on Linux

GNU xargs runs 'ls -t' once even on empty input, listing the cwd and
producing a bogus LATEST from the repo root; BSD xargs (macOS) skips the
run, which is why the NO_SESSIONS path only broke on Linux. xargs -r pins
the BSD behavior on both platforms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(parity): rebaseline v1.57.7.0 → v1.64.1.0 + skeleton-cap headroom

The two parallel v1.64 waves (code-smell fix wave + main's #2571) each
added shared-preamble prose, pushing document-release / design-consultation
/ cso past their size ratios on the v1.57.7.0 anchor and four carved
skeletons (plan-ceo-review, plan-eng-review, office-hours,
design-consultation) 22-280 B over their absolute caps. New baseline is
union-normalized (skeleton + sections/*.md, matching what the harness
measures); caps get +~1 KB headroom each with per-cap rationale. The
v1.57.7.0 fixture stays in test/fixtures/ for the audit trail, and
capture-parity-baseline.ts now documents the union-normalization step so
the next rebaseline doesn't re-trip on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): free-tests container parity — tools, pinned bun, git identity, mutation tripwire

- Dockerfile.ci: add python3 (gstack-jsonl-merge/brain-sync/detach shell out
  to it), file (skill-validation's binary check), poppler-utils (make-pdf
  e2e gates hard-require pdftotext/pdffonts/pdfinfo), fonts-noto-color-emoji
  (emoji render gate, mirrors make-pdf-gate.yml). Fix the bun pin: the
  bun.sh installer ignores a BUN_VERSION env var, so the old form silently
  installed latest on every rebuild (observed 1.3.13/1.3.14 drift vs the
  1.3.10 devs run locally); pass the version as the positional arg.
- free-tests.yml: git identity + safe.directory for the git-exercising
  tests (container checkout is owned by a different uid than runner);
  post-loop tree-mutation tripwire that names a tracked-file-mutating test
  instead of letting downstream collateral confuse the report; skip the
  documented variants-retry-after timing flake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bin): gstack-session-update — detached updater owns its stdio (SIGPIPE)

The backgrounded update subshell inherited the session hook's stdout/stderr
pipes. Once the hook exits and the caller closes them, any child that writes
— git pull's autostash notice, setup output — dies of SIGPIPE, logged as
PULL_FAILED exit=141 with an empty stderr capture (observed in the free-tests
container, and reachable by any production hook runner that closes stdio
promptly). Redirect the fork to /dev/null; all observability already flows
through the session-update log file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): gstack-decision-bins — explicit branch context for the scope filter

CI checks out a detached HEAD, where gitBranch() returns undefined on both
the log and search sides, so an implicitly branch-scoped decision can never
surface (filterByScope requires a matching non-empty ctx.branch). Pass the
branch explicitly on both sides — the filter logic is what's under test, not
git branch detection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): ring-buffer lease interplay — same TTL window, not same millisecond

Two back-to-back mintLease() calls each stamp Date.now() + TTL; when they
straddle a millisecond boundary the exact-equality assertion flakes
(observed in CI: expiries of ...525 vs ...526). Assert the expiries are
within a 50 ms window instead — the invariant under test is that leases
share a TTL policy, not that they mint in the same clock tick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 09:37:04 -07:00
Garry TanandClaude Fable 5 008dd65b1f v1.64.0.0 fix wave: full tracker audit — 90 fixes, 52 issues closed, ~50 community PRs absorbed (#2571)
* fix(hooks): nest freeze/careful permissionDecision under hookSpecificOutput

Claude Code ignores a top-level permissionDecision, so the /freeze deny and
/careful ask guards silently allowed everything. Nest both under
hookSpecificOutput with permissionDecisionReason, update the shape-blind
tests to pin the nested form, and document the constraint in both skill
templates (regen included).

Closes half of #1459 (freeze enforcement chain).

Contributed by @jawadakram20 (PR #2331; team-init hunk deferred to the
dedicated team-init fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(team-init): required-mode hook blocks with nested schema + exit 2

The generated check-gstack.sh emitted a flat permissionDecision payload and
exited 0, which Claude Code ignores — required mode enforced nothing. The
generated hook now nests the deny under hookSpecificOutput and exits 2 so
the block holds even if the JSON schema drifts again. Adds a temp-repo
regression test that runs the generated hook under both installed and
missing-gstack homes.

Fixes #2413, #2296.

Contributed by @Masashi-Ono0611 (PR #2423).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(careful): close three check-careful bypasses via real JSON extraction

The grep-based command extractor stopped at the first escaped quote, so any
quoted argument truncated the command before the pattern checks ran —
`git commit -m "wip" && rm -rf /` was silently allowed. Replace it with a
python3/node JSON parse that fails CLOSED on unreadable payloads, add an
IFS/base64-to-shell obfuscation tripwire, and stop multi-line commands from
riding the single-line safe-exception whitelist (line-based grep would have
approved `rm -rf /` when a later line matched node_modules — a hazard the
real newline decoding exposed).

Contributed by @wtamminga (PR #2426; the -R hunk was dropped — it landed in
v1.61.0.0 — and output shapes updated to the nested hookSpecificOutput form).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review,autoplan): require explicit run_in_background: false on specialist agents

Claude Code v2.1.198 made subagents run in the background by default, which
inverted the old "do not use the flag" guidance: review-army specialists and
autoplan dual voices silently launched in the background and the merge step
could proceed before they completed — regressing the #497 fix. The generated
guidance now instructs an explicit run_in_background: false, and a static
tripwire fails the free suite if the inert inverted phrasing ever returns to
any generated SKILL.md.

Fixes #2440.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(investigate): anchor the scope-lock freeze hook on $HOME, not CLAUDE_SKILL_DIR

The investigate skill's PreToolUse hooks and Scope Lock probe resolved
check-freeze.sh via ${CLAUDE_SKILL_DIR}, which does not exist when
frontmatter hooks run — the || exit 0 tail then failed open, so the debug
scope boundary silently never engaged (#1871 follow-up). Anchor all four
sites on $HOME/.claude/skills/gstack/ like careful/freeze, and add a static
test asserting no frontmatter command: line in the guard-family skills ever
references CLAUDE_SKILL_DIR again.

Fixes #2469; closes the last live half of #1459 together with the
freeze/careful hookSpecificOutput fix. The broader portable-install-root
rewrite stays #1882 (its own focused PR per the TODOS.md decision).

Reported with a fix by @maxpetrusenkoagent (PR #1873; absorbed narrowly —
the cwd-walk rewrite belongs to #1882).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact): scan large diffs in line-aligned slices; stop digit-UUIDs matching as cards/phones

The prepush guard blocked any push whose added lines exceeded the engine's
1 MiB cap with engine.input_too_large — a size error naming no credential —
which trains people onto GSTACK_REDACT_PREPUSH=skip. Scan in 768 KiB
line-aligned slices instead (no pattern is multi-line, so a boundary cannot
bisect a secret); a single oversized line still goes to the engine intact and
fails closed. Also suppress card/phone matches whose span sits ENTIRELY
inside a UUID — digit-only UUID fixtures were 14 of 21 MEDIUM findings on an
ordinary branch, the noise level that stops people reading MEDIUM at all.

Fixes #2304.

Contributed by @luckywenapere (PR #2543).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(redact): block Google OAuth client secrets and Telegram bot tokens at HIGH

GOCSPX-prefixed client secrets and <bot_id>:<35-char> Telegram tokens are
never-publishable credential shapes with unambiguous formats — both now
block at HIGH like the other live-format credentials.

Contributed by @francis-eye (PR #2357).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact-prepush): resolve the real push base instead of EMPTY_TREE whole-repo scans

When the remote default branch is not main/master (or origin/HEAD is unset),
the merge-base guess failed and the hook fell back to scanning the ENTIRE
repository as added lines — re-attributing long-pushed secrets to the
current push and, on any real repo, tripping the engine byte cap so the push
blocked having scanned nothing. Derive the base from commits reachable from
no remote-tracking branch, keep the empty-tree path only for genuinely fresh
repos, and split the block message so an unscannable diff is reported as
"could not scan (fail closed)" rather than "credential found — rotate it".

Contributed by @stormeoio (PR #2398).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact-prepush): preserve the trailing newline handed to chained pre-push.local

The chaining wrapper captured stdin with $(cat), which strips the trailing
newline — a chained shell hook built on `while read` then never entered its
loop for the final (usually only) ref line and exited 0, failing OPEN. Use
the printf-x sentinel so the byte-exact input reaches the chained hook, with
tests covering both the pass-through and the short-circuit paths.

Contributed by @francis-eye (PR #2358).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact-prepush): close the ext-diff, header-lookalike, and ref-parse bypasses

Three ways the pushed diff escaped scanning: (1) a user-level diff.external
or textconv driver replaced the diff with its own output — zero '+' lines,
so the scan saw nothing (now --no-ext-diff --no-textconv); (2) an added
content line whose text begins with "++" renders as "+++…" and the blanket
header skip dropped it (now hunk-aware header detection); (3) a pre-push
ref line that failed to parse was silently skipped, leaving that ref
unscanned (now fails closed with the offending line named).

Minimal reimplementation of the two confirmed bypasses from PR #2498 by
@lubosxyz (the full PR overlaps the chunked-scan work absorbed separately),
plus the unparseable-ref hardening.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pair-agent): keep the ngrok authtoken out of the transcript and shell argv

The not-authed flow told the user to paste their ngrok authtoken into the
chat so the agent could run `ngrok config add-authtoken` — putting a live
credential in the transcript, tool-call argv, and anything the transcript
syncs to. The user now runs the auth command in their own terminal; the
agent only verifies via `ngrok config check`, and a pasted token triggers a
rotate-and-reauth instruction. A static test pins that no agent-run bash
fence ever contains add-authtoken again.

Fixes #2335.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(update-check): crash emits CHECK_FAILED instead of reading as up-to-date

gstack-update-check signals "up to date" with SILENCE, and it runs under
set -e — so any unguarded mid-script failure exited quietly and was
indistinguishable from a current install. Observed live as a 45-release
silent-staleness incident. An ERR trap (with -E so it propagates into
functions) now emits a CHECK_FAILED sentinel naming the line and status,
and exits 0 so caller `|| true` guards can't eat it. Behavioral tests cover
both the crash and the healthy-silent paths; egress-receipt wiring is
untouched and still pinned by test/egress-receipt-wiring.test.ts.

Fixes #1974. (#2378's HEAD-SHA staleness half was already fixed on main by
the ls-remote + SHA-pinned VERSION resolution — close as already-fixed.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(deps): bump diff 7.0.0 → 9.0.0 (GHSA-73rr-hh4g-fpgx parsePatch DoS)

The advisory affects diff 6.x–8.0.2. The only API this repo uses is
Diff.diffLines (browse/src/snapshot.ts:571, browse/src/meta-commands.ts:728),
which is unchanged across the major hop; snapshot tests pass against 9.0.0.

Closes #1588.

Contributed by @genisis0x (PR #1599; VERSION collateral stripped, lockfile
regenerated fresh).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(evals): skip eval jobs deterministically on fork PRs

Fork PRs never receive repository secrets, so every API-calling eval failed
at SDK auth — but only when Docker-cache luck let the jobs start at all,
making fork PRs randomly red or grey. Skip the eval and report jobs
explicitly for fork-origin PRs, keep the image BUILD (validates
Dockerfile.ci changes) without the push a fork token can't perform, and
leave full coverage for same-repo PRs, pushes, and dispatches.

Contributed by @andrey-esipov (PR #2345).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(extension): deny token/port reads to content-script and foreign senders

background.js answered getPort — port, connected state, AND the browse
server auth token — to any sender that passed the type allowlist,
including content scripts running in web-page context and, behind only
the sender.id check, anything without extension-page provenance. The
getToken sender.tab restriction covered getToken alone, and only after
getPort had already handed out the token.

Single decision point now: extension/sender-auth.js classifies each
message type; the eight privileged types (getPort, setPort, getServerUrl,
getToken, fetchRefs, command, sidebar-command, getTabState) require an
own-extension-page sender (chrome-extension://<own id>/ URL, no
sender.tab, own sender.id). Denied senders get { error: 'unauthorized' }
and nothing else — never the token, never the port. Content-script flows
(elementPicked, pickerCancelled, inspectResult, openSidePanel) are
untouched, and the sidepanel/popup keep the getPort token field their
connect path reads. The policy mirrors the v1.63 server-side model:
AUTH_TOKEN is released only to the pinned extension Origin via
POST /extension-token, so the extension must not re-leak it to contexts
the server would never have trusted.

browse/test/extension-sender-auth.test.ts drives the real background.js
onMessage listener under a chrome stub with four sender shapes (own
extension page, own content script, foreign extension id, missing
sender.url) and pins that denied responses carry no token/port fields,
that a denied setPort never persists, that a denied command never
reaches the network, and that the inspector + tab-state flows keep
working. The helper is loaded via importScripts in the classic service
worker and require()-able from bun tests.

Contributed by @punksterlabs (PR #1822; reimplemented against the v1.63 POST /extension-token pinned-origin model).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(update-check): fixture links gstack-egress-lib.sh — all 38 tests failed on main

v1.63.0.0 made bin/gstack-update-check source bin/gstack-egress-lib.sh
unconditionally, but the test fixture's GSTACK_DIR only linked gstack-config
— every test died at the source line (0/38 pass on pristine main,
verified). The suite-truncation bug hid it: the runner was killed by an
earlier file's delayed process.exit before this file ran. Link the lib like
the real install layout the script assumes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): capture active-tab state before close() — last-tab auto-create raced the close event

closeTab checked `tabId === this.activeTabId` AFTER awaiting page.close(),
but the page 'close' event handler can fire during that await and reassign
activeTabId — losing the race meant the last-tab auto-create never ran,
leaving the manager with zero tabs. Capture wasActive before closing, and
only reassign activeTabId when it no longer points at a live tab.

Part of the test-integrity repairs unmasked by the suite-truncation fix.

Contributed by @time-attack (PR #2230, browser-manager hunk).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(browse): delete the orphaned sidebar chat-queue suite; align sidebar-ux/tabs with the PTY-only sidebar

browse/test/sidebar-integration.test.ts tested the /sidebar-command queue
path ripped in v1.14 (34 references to removed endpoints — 11 permanent
failures masked by suite truncation). sidebar-ux.test.ts carried 73 failures
pinning the same dead surface (pickSidebarModel, ANALYSIS_WORDS); the trim
keeps its 108 live tests, including the background.js token/allowlist gates.
sidebar-tabs gets the two matching expectation updates.

Closes #2420, #1980.

Contributed by @time-attack (PR #2230, sidebar hunks; the
security-sidepanel-dom deletion was NOT taken — that suite pins the live
sidepanel DOM surface and passes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(browse): align dual-listener and terminal-agent static guards with the current source

Two static-grep guards pinned superseded source shapes and failed once the
suite actually ran them: the tunnel dispatch gate is args-aware since the
--out disk-write ban (canDispatchOverTunnel takes command AND args), and
lazy PTY spawn routes through the maybeSpawnPty helper since v1.44. The
updated assertions pin the current, stricter shapes (open() never spawns;
the helper is the only spawnClaude caller).

Contributed by @time-attack (PR #2230, dual-listener + terminal-agent hunks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): remove all 8 delayed process.exit teardown bombs — the tier-1 gate can finally fail

bun test runs every file in ONE process, so a 500ms setTimeout(process.exit(0))
armed in afterAll fired mid-way through a LATER file and killed the entire
suite with exit 0 and no summary — only ~16 of 434 files ran, and every
downstream failure was invisible (observed live throughout this wave's
enumeration). Changes, all guarded by fault injection:

- Replace every delayed-exit teardown with a time-boxed close of the file's
  own browser (8 files across browse/ and design/); stub the daemon
  /shutdown timer instead of letting its unconditional process.exit tear
  the runner down.
- test/no-suicide-exit.test.ts: static tripwire — no *.test.ts may schedule
  a delayed process.exit again.
- test/exit-propagation.test.ts + fixtures: fault injection with REAL bun
  output proves the truncation shape (exit 0, no summary) and that
  scripts/test-free-shards.ts now detects it: a shard exiting 0 WITHOUT
  bun's final summary line is treated as FAILED (exit code alone is not
  evidence of completion).
- handoff: the three headed-mode integration tests are darwin-skipped with
  a pointer to the known macOS headed-launch breakage (#2242/#2554); they
  keep running on Linux CI. Un-skip in the browse-daemon wave.
- feedback-roundtrip: repair the handler call sites unmasked by the fix —
  handlers take (command, args, session, bm); passing the manager where a
  session belongs broke all six tests.
- user-slug-fallback: HOME isolation makes endpoint_hash deterministic.

Fixes #2421, #2435.

Contributed by @sneakygriff (PR #2172) with repairs from @time-attack
(PR #2230 feedback-roundtrip hunks); supersedes PR #2252 by @whd4 (same
defect, credited).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: include design/test/ in the free suite and the sharded runner

design/test was absent from both the package.json test globs and TEST_ROOTS
in scripts/test-free-shards.ts — its tests (including one of the teardown
bombs removed in the previous commit) never ran in any CI or local free
run, so design fixes could ship without their unit tests executing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): reject directories when resolving the browse binary

access(X_OK) is true for directories (they carry the execute/traverse
bit on POSIX and pass the Windows existence check too), so cwd-dependent
resolution could pick the ~/.claude/skills/browse alias DIRECTORY as the
browse binary. Every browse call then exited 4 with empty stderr, which
make-pdf surfaced as "Chromium failed to launch" against a perfectly
healthy Chromium (#2156). Guard isExecutable with statSync().isFile()
so only regular files qualify.

Contributed by @jwilk-hrep (PR #2538).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): write browse-bound temp files under the safe-dirs allowlist

os.tmpdir() on macOS resolves to /var/folders/..., which fails browse's
safe-dirs validation ([/tmp, cwd]) since the v1.6.0.0 --from-file
tightening. Default PDF output (generate with no -o), the preview HTML,
tmpFile() scratch files, and setup's smoke-test fixture/output all wrote
there, so browse rejected the paths it was asked to read or write.
Export PAYLOAD_TMP_DIR from browseClient (the existing TEMP_DIR
convention: os.tmpdir() on Windows, /tmp elsewhere) and route
orchestrator.ts and setup.ts temp files through it.

Contributed by @lvthewah (PR #2505; the browse-binary directory guard
from that PR landed separately via PR #2538).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): stop URLs swallowing smartypants placeholders

A bare autolinked URL (<a href="X">X</a>) has zero whitespace between
the URL text and its own closing tag. TAG_RE carves that </a> into a
NUL-delimited SMARTPANTS_PRESERVED placeholder BEFORE the URL pass
runs, and URL_RE's \S+ swallowed the adjacent placeholder into the URL
match. The restore pass is single-shot, so the inner placeholder never
restored: raw "SMARTPANTS_PRESERVED_N" text leaked into the rendered
link, the </a> vanished, and link-blue styling bled into the rest of
the document (#2084). Excluding the NUL sentinel (\u0000) from the URL
character class stops the match from crossing into an already-carved
zone.

Contributed by @marshaung (PR #2280; PR #2339 by @BrendaB24 covered the
same smartypants defect).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): no blank first page when content precedes the first H1

Two paths put invisible content ahead of the first H1 and cost users a
blank page 1 (#1904):

- A visually-empty preamble (leading <style> block, HTML comment)
  became its own .chapter. That section took the `.chapter:first-of-type
  { break-before: auto }` exception, so the first real chapter inherited
  `break-before: page` and started on page 2. Non-rendering preambles
  now fold into the first real chapter (markup preserved, no page
  break); real text preambles keep their own chapter.
- Leading YAML frontmatter rendered as a literal paragraph of body text
  on its own first page (marked has no frontmatter awareness). It is
  now stripped before parsing; a `---` thematic break elsewhere is
  untouched.

Contributed by @jbetala7 (PR #1913).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): allow about:blank so a restarted daemon can initialise

The daemon opens its own first tab on about:blank, so blocking it in
validateNavigationUrl meant a restarted daemon could never recreate the
blank tab it starts from — and `browse newtab about:blank`, which
`make-pdf setup` runs as its Chromium smoke test, failed and surfaced
as "Chromium failed to launch" against a healthy browser.

Allow about:blank ONLY, never the about: scheme: about:blank has no
origin, loads nothing and runs nothing, while about:config and friends
are real surfaces. Exact href match (lower-cased, since the URL parser
normalises the protocol but not the opaque part), so about:blankfoo
stays blocked.

Contributed by @jwilk-hrep (PR #2537).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(design): drop gpt-image-2 tool model that 400s under the gpt-4o orchestrator

The Responses API rejects pairing a gpt-4o orchestrator with an
image_generation tool spec'd as model: "gpt-image-2" (400
invalid_request_error), which took every design image call offline —
generate, variants, iterate (both threaded and fresh paths), evolve,
and /design-shotgun (#1771). gpt-image-2 is only valid under a gpt-5
orchestrator; with gpt-4o the tool must omit the model field (defaults
to gpt-image-1).

Remove the model field at all five call sites and add a static-grep
tripwire test (design/test/image-gen-pairing.test.ts) that fails CI if
any design/src module reintroduces the gpt-4o + gpt-image-2 pairing.
Re-enabling gpt-image-2 later requires bumping the orchestrator off
gpt-4o in the same diff, which the tripwire permits.

Contributed by @Pablosinyores (PR #1773).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(design): variants AbortError message reports the real 240s timeout

generateVariant arms its abort at 240_000 ms but the AbortError branch
returned "Timeout (120s)" — off by 2x, so a user staring at the failure
could not tell whether to bump the timeout, retry, or drop the call.
Report the actual configured bound, and pin it with a test that forces
the abort path (fast-forwarding only the 240_000 ms timer) and asserts
the surfaced string matches.

Contributed by @vryahn (PR #1774).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(memory-ingest): stop silently ingesting 0 pages — include gitignored staging, reconcile counts

Pages stage into ~/.gstack/.staging-ingest-*/ inside a repo whose .gitignore
is `*`, and gbrain import honours .gitignore — so it collected 0 files,
imported nothing, and the ingest still reported "written: N" from the STAGED
count while advancing state, meaning no future run ever retried. Three
layers now: (1) pass --include-gitignored (root cause); (2) if the installed
gbrain predates the flag, retry without it (subcommand --help is generic, so
the attempt is the only probe) with an upgrade pointer; (3) reconcile
gbrain's imported+unchanged accounting against the staged count and REFUSE
to advance state on a shortfall, naming the gitignore collision.

Fixes #2144, #2104.

Contributed by @gawievanblerk (PR #2560) and @Charles-Grant (PR #2486).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(autoplan): task aggregator returned zero tasks on every run — jq scope bug

Inside ($commits | split("|") | ...) the "." context is the split ARRAY, so
the filter's bare .commit raised "Cannot index array with string" on every
record — and the 2>/dev/null swallowed it, so aggregation silently produced
zero tasks no matter how many the reviews emitted. Bind .commit to $c before
the pipe. Reproduced live before the fix; regenerated autoplan/SKILL.md.

Fixes #2018.

Contributed by @kkroo (PR #2416; regenerated against the current template).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(session-update): un-wedge auto-upgrade — autostash over local patches, log the pull's real reason

On a normal install the tracked files ARE locally patched (skill-prefix
name rewrites, gbrain-refresh blocks), so the bare `git pull --ff-only`
refused on every run and auto-upgrade froze forever — observed as 308
consecutive PULL_FAILED entries with the reason discarded by 2>/dev/null.
Pull now runs --autostash (local patches ride over the update and pop back),
stderr is captured into the log so a genuine failure names its cause, an
autostash pop conflict recovers to a clean tree and re-renders the patches
(gstack-patch-names + gbrain-refresh, both idempotent), and a successful
pull re-renders them as a self-heal. Behavioral tests cover the wedge shape
and the reason logging.

Fixes #2566.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: raise the free-suite per-test timeout to 30s

bun's 5s default is fine for a file run solo, but the monolithic free suite
shares one process across 100+ files whose browser instances contend for
launch slots — Playwright tests that pass in isolation time out mid-suite.
30s matches the ceiling the enumeration runs used; the sharded runner
(test:free) is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(question-log): parse native AskUserQuestion answers — every native answer logged as __unknown__

Current Claude Code returns AskUserQuestion results as an OBJECT map keyed
by question text ({answers: {question: label}}); the hook only handled the
legacy array shapes, so 86% of live records carried user_choice __unknown__
— and the bin then scored every one as followed_recommendation false,
silently poisoning plan-tune metrics. Adds the object-map extraction (exact
+ whitespace-normalized + single-question pairing, multiSelect joins,
annotations as free_text), strips the (Recommended) suffix from BOTH sides
of the comparison, skips the computation entirely on extraction failure,
and logs unrecognized shapes to hook-errors.log instead of embedding them
in the record.

Fixes #2336, #2206.

Based on the working patch in #2336 by @yijisoo; suffix comparison fix
contributed by @chuchu2781 (PR #2400).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(slug): canonicalize slash branches to dash form — review history stops splitting

Branch-name sanitization disagreed across gstack (four incompatible rules),
so reviews for the same slash-named branch landed in multiple files and the
ship dashboard missed entries. gstack-slug now canonicalizes / to - in one
place, and ship's review lookup routes through it; goldens regenerated
against the current templates.

Fixes #1127, #2550.

Contributed by @ShuratCode (PR #2465; duplicate fixes by @xrfael-dev and
two others in PRs #1851/#1699/#1621, credited).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(slug): resolve the project root by marker walk-up — subdirectory sessions stop misfiling state

gstack-slug derived everything from pwd, so a session in a subdirectory got
the subdir's basename as its slug (or an outer monorepo's remote), misfiling
reviews/decisions/learnings under a phantom project — and the per-pwd cache
made the wrong answer permanent. The resolver now walks up from pwd:
outermost STRONG marker wins (.git, package.json, pyproject.toml, Cargo.toml,
Gemfile, go.mod, .project.yaml), weak content markers (README, LICENSE) catch
non-code project folders, deploy artifacts are deliberately not markers, and
GSTACK_PROJECT_SLUG remains the escape hatch. The cache self-heals on
mismatch. Main-side invariants preserved on top: the unconditional
[a-zA-Z0-9._-] re-sanitize before echo and slash→dash branch canonicalization.

Fixes #1125.

Contributed by @ajeenkya (PR #1702; rebased over the sanitize and
branch-canonicalization work that landed after it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hooks): shared spawn-bin helper — all three AskUserQuestion hooks were inert on Windows

The plan-tune hooks resolved bin scripts via new URL(import.meta.url).pathname
(which doubles the drive letter on Windows: /C:/C:/...) and spawnSync'd
extensionless bash scripts directly (unrunnable without a shell association)
— so question logging, preferences, and the error fallback all silently
no-op'd on Windows, and /plan-tune collected no data. A single spawn-bin.ts
helper now owns bin resolution (fileURLToPath) and win32 bash routing for
every hook, with static tripwires so a future hook can't reintroduce the
raw pattern. This is the one Windows-spawn idiom for hook code.

Fixes #2356.

Contributed by @rafassousa (PR #2504; supersedes PR #2399 by @chuchu2781).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(model-overlays): add fable-5, opus-4-8, and sonnet-5 overlays + resolver mappings

model-overlays/ had no entry for the current Claude generation, so every
session on a Claude 5 family or Opus 4.8 model fell through to the generic
claude.md nudges. Adds the three overlays with resolver mappings and
per-overlay tests; generated output for the default host is unchanged
(overlays activate by detected model).

Closes #2509.

Contributed by @chrisquorum (PRs #2246, #2243, #2247).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(windows): grant icacls ACEs by *SID, not unqualified username

An unqualified username handed to icacls is ambiguous: on a machine whose
hostname equals the username (a common Windows setup), it resolves to the
MACHINE account instead of the user. Combined with /inheritance:r, that
leaves ~/.gstack with a single ACE matching nobody — the process that just
"secured" the directory locks itself out, and icacls still reports success.

Both icacls sites in the repo (restrictFilePermissions and
restrictDirectoryPermissions in browse/src/file-permissions.ts — the only
icacls call sites; setup has none) now grant via icacls' literal-SID form
`*<SID>`, resolved once per process from System32\whoami.exe (pinned to
System32 because a bare `whoami` under a bash-flavoured PATH picks up the
MSYS build, which rejects /user). Fallback when the SID can't be resolved
is the domain-qualified `USERDOMAIN\username` name, which is unambiguous
where the bare username was not.

Windows-only regression tests assert the hardened directory stays usable
by the calling process (readdir + write), which is exactly the check that
a not-toThrow assertion sailed past before.

Contributed by @asizux2 (PR #2479); the same defect was independently fixed by @Icandi40, @chiragborse1, @IntegriGit and @voltapix26.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(windows): forward windowsHide through the bun-polyfill spawn shims

windowsHide is the one spawn option where Node's default is the opposite
of Bun's: Node shows the child's console window, Bun.spawn hides it.
The polyfill's spawn and spawnSync shims dropped the option entirely, so
the Node fallback path (dist/bun-polyfill.cjs) silently inverted the
behavior on the one platform the shim exists to serve — every watchdog
respawn of the terminal agent popped a visible bun.exe console window.

Three sites fixed:
- Bun.spawnSync shim: forwards windowsHide with Bun-matching default true
- Bun.spawn shim: same (stdio:'ignore' silences output but does NOT
  suppress the console window on Windows)
- spawnTerminalAgent in terminal-agent-control.ts: explicit
  windowsHide: true, so the Node fallback path behaves like Bun-native

An explicit windowsHide: false is honored at both shims. Three focused
tests pin the default-true, default-true-sync, and explicit-false paths
by intercepting child_process in a subprocess; the test file's require
path now uses forward slashes so it survives interpolation into a JS
string literal on Windows.

Supersedes PRs #2523, #2294 and #2290, which each covered a subset of
these sites.

Contributed by @jerrynicholsai (PR #2539); earlier fixes by @jwilk-hrep, @rroojrooj and @WimvandenHeijkant covered subsets of the same sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(watchdog): signal-0 liveness, tick-scaled respawn guard, windowsHide

Three-bug chain behind the Windows terminal-agent leak (console window
strobing every 60s, one orphaned agent per watchdog tick until the box
ran out of committable memory):

1. isProcessAlive shelled out to `tasklist /FI "PID eq <pid>"` on Windows
   with a 3s timeout. A Bun.spawnSync that hits its timeout still RETURNS
   with partial stdout, so the `.includes()` PID match read a LIVE agent
   as dead — killAgentByRecord skipped the kill, the watchdog respawned
   around the survivor, and every orphan slowed the next tasklist enough
   to produce the next false negative. Now: `process.kill(pid, 0)` on
   every platform (Node and Bun both map signal 0 to an OpenProcess
   existence check on Windows), with EPERM counted as alive. No
   subprocess, no timeout, no console window.

2. The respawn circuit-breaker was mathematically unreachable — verified
   in this tree: RESPAWN_GUARD_WINDOW_MS was a fixed 60_000 against a
   60_000ms default tick, and each tick pushes at most one respawn
   timestamp, so three pushes span ~120s and can never coexist inside a
   60s window (eviction is strict `>`, and setInterval drift plus
   per-tick work always ages the prior entry past the boundary). The
   guard could not fire at the default tick rate and a steady
   one-per-tick leak ran unbounded. The window now scales with the tick:
   max(60_000, tick * (RESPAWN_GUARD_MAX + 2)), so "3 crashes in quick
   succession → stop" holds at any tick value.

3. The tasklist probe popped a visible console per tick (no windowsHide).
   Removing the shell-out kills that site; the agent-spawn site itself
   already passes windowsHide: true (landed with the bun-polyfill
   windowsHide commit — PR #2414's terminal-agent-control.ts hunk is
   reconciled there rather than duplicated).

New browse/test/process-liveness-windows.test.ts pins all three: no
subprocess from the probe, a static tripwire against reintroducing
`tasklist` + `PID eq` liveness checks in src/, the spawnTerminalAgent
windowsHide + stdio contract, and the window-derived-from-tick
arithmetic. terminal-agent-watchdog.test.ts test 4 now pins the
window/tick relationship instead of the fixed literal that let this
ship. Also converts `new URL(import.meta.url).pathname` to
`import.meta.path` across the static-grep tests it touches — the
pathname form yields /C:/... on Windows and breaks path.resolve.

Contributed by @SYKhayyat (PR #2414).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(terminal-agent): tie agent lifetime to its owning browse server PID

The terminal agent is intentionally detached so it survives the
short-lived CLI launcher, but its real owner is the persistent browse
server. If that server crashed or was killed before running normal
shutdown, the agent was adopted by PID 1 and lived forever (#2019).

spawnTerminalAgent now requires an ownerPid and exports it to the agent
as BROWSE_OWNER_PID; all three spawn sites pass the server PID (cli.ts
cold-start, cli.ts supervisor respawn, server.ts watchdog). The agent
polls the owner with signal 0 every 15s (GSTACK_TERMINAL_OWNER_WATCHDOG_MS
to tune) on an unref'd timer and, when the owner disappears, exits
through the SAME cleanup path as an intentional SIGTERM shutdown — now
re-entrancy-guarded and also removing the terminal-internal-token file
alongside the port file and agent record.

Runtime test spawns a real agent tied to a throwaway owner process,
kills the owner, and asserts the agent exits and its discovery files
(terminal-agent-pid, terminal-port) are gone.

Reconciled with the watchdog commit's spawnTerminalAgent contract test
(process-liveness-windows.test.ts now passes ownerPid and pins the
BROWSE_OWNER_PID env forwarding).

Closes #2019.

Contributed by @csarigoz (PR #2530).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(windows): give the bun-polyfill spawn shim a real `exited` promise

Bun.spawn exposes `proc.exited` as a Promise resolving to the exit code.
The Node fallback shim (dist/bun-polyfill.cjs) returned no such field, so
every `await proc.exited` on the Windows path resolved instantly to
undefined — the Windows cookie picker (cookie-import-browser.ts races
proc.exited at three sites) read stdout before the child produced it and
silent-failed; browser-skill-commands and terminal-agent hit the same
class.

The shim now:
- drains stdout/stderr eagerly into capped in-memory buffers (Node's
  Readables are pull-based; without draining, a child writing past the
  OS pipe buffer blocks in write() and 'exit' never fires), replaying
  them as fresh single-shot Web ReadableStreams so reads work before or
  after awaiting exit;
- caps the buffer at 16 MB (GSTACK_SPAWN_MAX_BUFFER to override), still
  draining past the cap so a runaway child can't wedge or OOM;
- resolves `exited` with Bun-matching codes (exit code, 128+signal, 1 on
  spawn error) after both pipes finish, and resolves on 'error' too —
  Node fires 'error' without 'exit' when the binary is missing, which
  otherwise hangs the await forever.

Six tests pin exit codes, the read-after-exit ordering, spawn-failure
resolution, the buffer cap, and the large-output drain. Adapted to the
current test file (require path goes through the requirePath variable
from the windowsHide commit), and the 1 MB drain test's child now exits
in the write callback — on modern Node a pipe write past the OS buffer
is async and process.exit() straight after write() truncates at ~64 KB
even with a live reader, which fails the test for reasons unrelated to
the shim.

Contributed by @punksterlabs (PR #1743).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): BROWSE_BIN carries the .exe suffix on Windows

On Windows, `bun build --compile` emits browse.exe, but setup's
BROWSE_BIN pointed at the suffixless path — so the post-build gate
(`[ ! -x "$BROWSE_BIN" ]` → "browse binary missing") could never pass on
Windows even after a fully successful build, while the build step itself
reported success. Closes #2291.

Applied the PR's override after the IS_WINDOWS detection, and also to
the second BROWSE_BIN assignment the PR predates: the direct-Codex-
install migration path re-derives BROWSE_BIN from the migrated dir and
would otherwise drop the suffix again on Windows.

Contributed by @rroojrooj (PR #1714).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): link lib/ beside bin/ at all five host-install sites

bin/ scripts import shared modules via ../lib (gstack-learnings-log →
lib/jsonl-store.ts is the reported case), so any runtime root that
exposes bin/ without lib/ breaks 13 bin/ commands — learnings-log,
decision-log, telemetry and friends fail with "Cannot find module
.../lib/jsonl-store.ts" on every non-Claude install, silently from the
skills' perspective.

All five host-install sites now carry lib/ next to bin/, each through
the existing _link_or_copy helper (never raw ln — the static invariant
in test/setup-windows-fallback.test.ts enforces this):

- .agents sidecar (create_agents_sidecar asset loop)
- Codex runtime root (create_codex_runtime_root)
- Factory runtime root (create_factory_runtime_root)
- OpenCode runtime root (create_opencode_runtime_root)
- Kiro install block

New test/setup-runtime-lib-command.test.ts executes the real setup shell
for each root in a sandbox (both the symlink branch and the Windows copy
branch of _link_or_copy) and runs gstack-learnings-log end-to-end from
the installed root, asserting the learning lands in
~/.gstack/projects/<slug>/learnings.jsonl — plus a negative control
proving a bin-without-lib root fails exactly the way the bug report did.
gen-skill-docs.test.ts's setup-validation block pins the lib link at
every site. Cross-checked against PRs #2433, #2410 and #2198: all three
cover subsets of these sites; nothing they fix is missing here.

Contributed by @fedster99 (PR #2262); overlapping fixes by @gregario, @lsendel and @netkurt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): ship supabase/config.sh with every host runtime root

Distinct from the lib/-beside-bin/ defect: gstack-telemetry-sync,
gstack-update-check, gstack-security-dashboard and
gstack-community-dashboard all source $GSTACK_DIR/supabase/config.sh to
resolve GSTACK_SUPABASE_URL, where GSTACK_DIR is the installed root
(parent of bin/). The [ -f ... ] guard means a root without the file
degrades SILENTLY — telemetry and update checks just stop resolving the
project URL on non-Claude installs. Closes #2215.

setup now links supabase/config.sh (file-level on purpose — migrations/
and functions/ are dev-only) via _link_or_copy at all five host-install
sites: the PR's four (Codex, Factory, OpenCode runtime roots + the Kiro
block) plus the .agents sidecar, whose bin/ resolves the same relative
path and which the PR predates covering.

The runtime-root test now asserts supabase/config.sh is present in
every built root, on both the symlink and Windows-copy branches.

Contributed by @jizusun (PR #2216).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(windows): curate the fix-wave regression tests into the windows-latest run

The windows-free-tests curated set is derived (POSIX-fragility regex scan
+ explicit deny list), and two of this wave's Windows regression files
were auto-excluded on false-positive pattern hits:

- browse/test/file-permissions.test.ts tripped the POSIX-mode-bitmask
  pattern, but every `mode & 0o777` assertion is platform-guarded — and
  the file carries the win32-only icacls-by-SID regression tests, which
  can only ever execute on windows-latest.
- browse/test/terminal-agent-owner-watchdog.test.ts tripped the
  spawn(['bun','run',...]) pattern whose reason is the Playwright-bound
  browse server; it actually spawns terminal-agent.ts (fs/path/crypto +
  local helpers only, no Playwright at module scope), and the owner-PID
  orphan leak it pins was reported on Windows (#2019).

Adds a KNOWN_WINDOWS_SAFE force-include list (mirror of
KNOWN_WINDOWS_INCOMPATIBLE, each entry carrying its false-positive
rationale) consulted before the pattern scan, and makes the
owner-watchdog test's throwaway owner process Windows-portable
(process.execPath instead of `sleep`, which a bare runner may not have).

The wave's other new files need no wiring: process-liveness-windows and
the bun-polyfill windowsHide/exited tests pass curation automatically;
setup-runtime-lib-command self-skips on win32 by design (its Windows
branch is exercised by simulating IS_WINDOWS=1 under bash), so
force-including it would add a permanently-skipped file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): register the SessionStart hook with a bash prefix on Windows

Windows can't execute an extensionless bash script directly — registering
the bare gstack-session-update path made the hook pop the "Select an app"
dialog on every session start (or silently never run), so team-mode
auto-upgrade was dead on Windows installs. Companion to the hooks'
spawn-bin routing: same defect class at the registration site.

Contributed by @NikhileshNanduri (PR #1813; VERSION/CHANGELOG collateral
stripped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): stop piping gen:skill-docs through tail — generator failures were masked

setup piped doc generation through `tail -3`, so a generator crash kept the
pipe's exit 0 and installs completed "successfully" with broken or missing
SKILL.md files. Capture the real exit status at BOTH sites (the main
gen:skill-docs step and the gbrain-detected gen:skill-docs:user regen —
the second drifted in after the PR and its own test caught it), print the
tail for UX, and fail loudly.

Contributed by @DavidMiserak (PR #1898; VERSION/CHANGELOG collateral
stripped; extended to the second pipe site).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mktemp): move the X-run to the end of every temp-file template (BSD/busybox safe)

BSD mktemp (macOS) does not substitute an X-run that has a suffix after it:
`mktemp "$TMP_ROOT/codex-err-XXXXXX.txt"` creates a LITERAL
codex-err-XXXXXX.txt on the first call (exit 0) and every later call fails
with `mkstemp failed: File exists` — so /codex breaks from the SECOND run on
every Mac, masquerading as a model stall. busybox mktemp (Alpine) rejects the
template on the first run. Fixes #2091, #2370.

Union of both community fixes, compared at the diff level:
- PR #2372: all 11 source sites with a suffix after the X-run — codex
  SKILL.md.tmpl (5), claude SKILL.md.tmpl (3), bin/gstack-developer-profile
  (2, suffix folded into the prefix: .json.tmp.XXXXXX), and the office-hours
  codex pass in scripts/resolvers/review.ts (1).
- PR #2103: the second half of #2091 — bin/gstack-paths now strips the
  trailing slash from TMP_ROOT at the source (macOS $TMPDIR ends in `/`),
  plus runtime tests pinning that normalization.

New repo-wide tripwire in test/regression-issue2091-bsd-mktemp.test.ts:
every .tmpl, every SKILL.md, and every scripts/resolvers/*.ts is swept —
no mktemp template may carry a suffix after the X-run, with a self-test so
the detector can't be quietly blinded. Generated SKILL.md files regenerated
via gen:skill-docs in this commit.

Contributed by @ShuratCode (PR #2103) and @noron12234 (PR #2372); PR #2285 by @cathrynlavery covered a subset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(codex,review,ship): scope codex review with an explicit --base flag, never prompt text

`codex review` takes its scope ONLY from --base/--commit/--uncommitted. The
positional [PROMPT] is mutually exclusive with all three, and a prompt-only
`codex review "<text>"` silently falls back to the uncommitted working-tree
scope (verified on 0.144.1: it runs `git status --short; git diff` and
reviews that) — so the previous prompt-based scoping produced a
confidently-worded review of the WRONG changes and read "no changes" on a
clean tree. Every diff pass now invokes `codex review --base <base>` with no
prompt argument: /codex Step 2A default path, the /review structured pass,
and the /ship adversarial-section pass (all via scripts/resolvers/review.ts).

Custom review instructions keep their own `codex exec` path (the CLI rejects
prompt + scope flag together), with the filesystem boundary preserved there.
Two new Error Handling entries teach the failure shapes: the argv-parse
error, and the "review says no changes on a branch full of changes" symptom.

Tests updated to pin the new invariant instead of banning the fix: the old
assertions required the diff range in prompt text and banned the
`--base <base> -c '...'` substring, which the correct scoped form contains.
Also deletes test/fixtures/golden-ship-claude.md — a 2,565-line orphaned
fixture referenced by zero tests (the live goldens are in
test/fixtures/golden/, compared by test/host-config.test.ts); the factory
golden is refreshed from the regenerated output. Generated SKILL.md files
regenerated via gen:skill-docs in this commit.

Contributed by @fangearhq-boop (PR #2513).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review,ship): run the codex diff passes under the timeout wrapper (#1036)

The `_gstack_codex_timeout_wrapper` added in #1056 was wired into
codex/SKILL.md but never into the /review and /ship diff passes, which kept
running under a bare 5-minute Bash gate. An unwrapped stall returns no exit
code and no output, which downstream reads as "Codex reviewed and found
nothing" — a truncated pass silently became a clean bill. Measured on
codex-cli 0.145.0: a pass was killed at 287s of a 300s budget mid-tool-call,
and the same prompt completed in 336s.

Both passes in scripts/resolvers/review.ts (adversarial `codex exec` and the
structured `codex review --base` pass) now re-source gstack-codex-probe and
run under `_gstack_codex_timeout_wrapper 540`, with the Bash tool gate raised
to 600000 ms so the wrapper fires FIRST and a stall surfaces as a diagnosable
exit 124. The timeout guidance now says a timed-out pass is MISSING COVERAGE,
not a clean result, and points at the run's rollout log under
~/.codex/sessions/ for partial output. The stale "timeout doesn't exist on
macOS" claim is gone — the wrapper resolves gtimeout, then timeout, then runs
unwrapped, so it is safe without coreutils.

Static guards in test/codex-hardening.test.ts pin all three sites (resolver,
review/SKILL.md, ship/sections/adversarial.md): both calls wrapped, wrapper
budget strictly under the Bash gate, and no reappearance of the macOS claim
that steered these call sites away from the wrapper in the first place. The
Claude-output path guard in test/gen-skill-docs.test.ts now scrubs
~/.codex/sessions/ (a user-facing Codex CLI path, same class as the
~/.codex/logs/ exemption) before banning Codex host paths. Generated files
regenerated via gen:skill-docs; factory golden refreshed.

Contributed by @aegixx (PR #2379).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(codex): sandbox the review path, fail the gate closed, order timeouts wrapper-first

Closes #2496, #2524, #2477 — three defects in the class "a guard that
reports success while doing nothing", all in codex/SKILL.md.tmpl:

(a) Review sandbox. The default `codex review` path was the only codex call
with no sandbox override, inheriting ~/.codex/config.toml's default — write
access on a trusted project — while Important Rules claimed read-only.
Top-level `codex review` has no -s/--sandbox flag (verified on 0.147.0), so
the invocation now pins `-c 'sandbox_mode="read-only"'`, the same form the
consult-resume path already uses.

(b) Fail-closed verdict gate. The old rule ("no [P1] found → PASS") could
not fail on the default path: native `codex review` output carries no
bracketed tags, and a non-zero exit, expired auth, timeout, or empty result
also contains no [P1] — all read as PASS. The gate is now an ordered,
fail-closed check: non-zero exit → FAIL; empty output → FAIL; [P0]/[P1]
(bracketed or codex's native labels) → FAIL with count; NO severity tags at
all → FAIL requiring a human read; PASS is only reachable through the
explicit tagged-advisory-only branch. [P0] is recognized as blocking, and
the review-log findings count includes it.

(c) Bash gate above the wrapper. Step 2A instructed `timeout: 300000` under
a 330s wrapper, and Challenge's 300s gate sat under a 600s wrapper — the
harness killed the call before the wrapper could emit its diagnosable
exit-124 message. Every Bash gate now sits strictly ABOVE its wrapper:
360000 over the 330s review wrapper, 660000 over the 600s challenge/consult
wrappers, with the ordering rationale stated at each site.

Also from #2477/#2524: a new Error Handling entry for the model-entitlement
400 ("The '<model>' model is not supported...") pointing at the `model =`
pin and `[notice.model_migrations]` in ~/.codex/config.toml and saying
exactly which override to retry with (-m for exec-based modes,
`-c model="..."` for review mode, which rejects -m); the Model & Reasoning
section no longer documents `-m` for `/codex review`.

Static assertions in test/codex-hardening.test.ts pin (a)-(c) across both
the .tmpl and the generated SKILL.md: every scoped review invocation carries
sandbox_mode="read-only" and never -s; the default-PASS sentence is banned
and the fail-closed branches are present; and per-section, every Bash
`timeout: N` is strictly greater than every wrapper budget, with 2A/2B/2C
all required to be inspected. Generated SKILL.md regenerated via
gen:skill-docs in this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(preamble): quoted tilde made Artifacts Sync and telemetry-finalize dead code in 49 skills

A tilde inside double quotes never expands, so the generated
`_BRAIN_SYNC_BIN="~/..."` assignments resolved to a literal ./~ path and
the Artifacts Sync + telemetry-finalize blocks silently no-op'd in every
skill that carried them (regression of #785). The preamble resolvers now
emit $HOME-based paths; all generated SKILL.md files regenerate identically
from the fixed templates, and a static tripwire fails the suite if a
quoted-tilde assignment ever reappears in generated output.

Fixes #1656, #1715.

Contributed by @jawadakram20 (PR #2333).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen-skill-docs): stop the catalog trim chopping descriptions at embedded periods

The description-trim regex treated the first period as end-of-sentence, so
skill descriptions with embedded periods (e.g. file extensions, version
numbers) truncated mid-thought in the generated catalog — the discovery
surface every host loads. Trim now respects the full first sentence;
diagram's description regenerates to its intended text.

Contributed by @sneakygriff (PR #2171).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(preamble): update_check:false gates the prose, not just the binary

Setting update_check:false stopped the update-check BINARY from running,
but every skill preamble still shipped the upgrade-handling instruction
prose unconditionally — burning tokens on instructions that could never
fire and confusing agents into probing for upgrades anyway. The resolver
now suppresses the upgrade-flow prose when the config disables checks.

Fixes #2001.

Contributed by @jc0d35 (PR #2022).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): sidebar Terminal — drop the duplicate WS subprotocol header, stop doubling CJK IME input

The terminal client passed the auth token as the WS subprotocol AND echoed
it in a second header, which some Chromium builds reject; and composition
events double-sent CJK input (each IME commit arrived once from the
composition handler and once from the data handler). One auth path, one
input path; also fixes the terminal-agent test that failed on clean main.

Contributed by @mindsurf0176 (PR #2515).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): -h/--help prints usage instead of running the installer

Asking setup for help RAN the full installer — Playwright download and all.
Standard help flags now short-circuit to usage.

Contributed by @saen-ai (PR #1219).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hosts): Codex-generated skills reference AGENTS.md, not CLAUDE.md

Codex reads AGENTS.md, but its generated skills still told agents to read
CLAUDE.md in 8 places — instructions Codex hosts cannot follow. The host
config now maps the memory-file name per host; all three ship goldens
refreshed from the regenerated output.

Contributed by @exGeni (PR #1996).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(retro,ship): count tracked files for the test-file metric, not the working tree

The test-file count ran find over the working tree, sweeping untracked
build output — a Rails repo reported 623 test files when git tracks 17
(37x), skewing retro narratives and ship dashboards. Count via git ls-files
instead; includes the one-line Python-glob widening so non-JS repos stop
undercounting.

Fixes #2307, #1999.

Contributed by @joshRpowell (PR #2308).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(land-and-deploy,gen): auto-merge diagnosis + CRLF-stable generation

Two small hardenings: land-and-deploy Step 4 no longer misdiagnoses a
failed `gh pr merge --auto` as a permissions problem when the real cause is
the merge-method mismatch the command names; and gen-skill-docs normalizes
CRLF at the template entry point so Windows checkouts with autocrlf produce
byte-identical generated output to CI instead of silently skipping the
\n-anchored transforms.

Contributed by @Jmeg8r (PR #2437) and @1ncludeSteven (PR #1051).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(land-and-deploy): stop greedy sed from eating the URL scheme in deploy-config parsing

The deploy-config bootstrap parsed "Production URL: https://x.com" with
sed 's/.*: *//', which cuts at the LAST colon — the one in "https:" —
yielding "//x.com". Cut at the first ": " instead (s/^[^:]*: *//).

Resolver only; the generated land-and-deploy/SKILL.md regenerates from
this source in the docs lane.

Contributed by @briascoi (PRs #2555/#2493).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(artifacts-init): honor the provider CLI's git_protocol instead of forcing SSH

gstack-artifacts-init unconditionally rewrote the push remote to SSH and
hard-failed setup for users whose gh/glab auth is HTTPS-only. Now:

- provider-created remotes follow `gh config get git_protocol` /
  `glab config get git_protocol` (HTTPS when unset — the gh default)
- explicit/existing/manual remotes keep their given protocol; unknown
  URL forms (local bare paths, file://, self-hosted) pass through
- new --push-protocol auto|https|ssh flag overrides the inference
- the unreachable-remote error names the actual protocol and points at
  --push-protocol instead of assuming a missing SSH key

Closes #1348.

Contributed by @time-attack (PR #2225).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): skip the .gitignore append when git already ignores .gstack/

ensureStateDir appended ".gstack/" to a tracked .gitignore even when git
already ignored the directory via global excludes, .git/info/exclude, or a
parent .gitignore — dirtying the working tree on every daemon start. Run
`git check-ignore -q -- .gstack/` first and return early when git says it's
covered; git-missing/not-a-repo/timeout all fall through to the existing
text-check append (the safe default).

Closes #2385.

Contributed by @gregario (PR #2430).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): guard browser.process() in resolveDisconnectCause

`.process()` only exists on browsers Playwright launched itself; a browser
from connectOverCDP() (or a test stub) has no such method, so the blind call
threw "browser?.process is not a function" inside the disconnect handler and
took down the daemon. Type-check the method before calling it and treat the
no-method case as no process handle.

Closes #2085.

Contributed by @elan2002 (PR #2434).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(lib): narrow the override injection denylist to instruction-shaped phrases

The /override[:\s]/i pattern flagged any prose containing "override " or
"override:" — CLI flags (--port-override -1), tfvars notes, and plain
"you can override the default region" all tripped the injection guard.
Require an instruction-shaped continuation: "override (all)? previous |
prior | above | the rules/instructions/system prompt". Genuine attempts
like "Override: ignore all previous instructions" still block via the
ignore-previous pattern.

Closes #2401, #1934.

Contributed by @Masashi-Ono0611 (PR #2424); same fix independently by
@JonasFocus (PR #1940).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact): stop the E.164 phone pattern flagging compact timestamps

Bare 14-digit runs like 20260727202423 (YYYYMMDDHHMMSS backup/log stamps)
matched the phone regex and produced MEDIUM PII findings. Reject a
separator-free 14-digit span whose fields parse as a plausible date-time;
real numbers carry a + or spacing, so phone coverage is unchanged.

Contributed by @abkrim (PR #2428).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(design): create the OpenAI key file owner-only, closing the write-then-chmod race

saveApiKey wrote ~/.gstack/openai.json at the default umask and tightened to
0600 afterwards, leaving the API key briefly world-readable between write and
chmod (CWE-377/367). Pass mode 0o600 at create; the trailing chmodSync stays
as a backstop to tighten a pre-existing loose file.

Contributed by @bunlongheng (PR #2468).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): make gstack-config key validation locale-independent

POSIX bracket ranges like a-z follow the active collation order; under GNU
grep with tr_TR.UTF-8 the range excludes the ASCII letter i, so every key
containing i (skill_prefix, explain_level, ...) was rejected as invalid.
Pin both get/set validators to LC_ALL=C, with a source-level tripwire test
since macOS BSD grep doesn't reproduce the bug.

Closes #2494.

Contributed by @Math1987 (PR #2506).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(resolvers): stop env-var hosts from doubling $HOME in the binary fallback path

The browse/design/make-pdf setup resolvers built the fallback binary path as
"$HOME" + dir.replace(/^~/, ''), which is only correct for ~-rooted dirs.
Env-var hosts carry an absolute $GSTACK_* dir, so the generated fallback
became $HOME$GSTACK_.../browse — a path that never exists. New toShellPath()
in scripts/resolvers/types.ts expands ~ to $HOME and passes absolute
env-var dirs through untouched; all five call sites route through it.

Claude-host generated output is byte-identical, so no SKILL.md regeneration
is needed here.

Closes #2055.

Contributed by @simjak (PR #2056).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(settings-hook): respect CLAUDE_CONFIG_DIR when resolving settings.json

gstack-settings-hook hardcoded $HOME/.claude/settings.json, so users running
Claude Code with a relocated CLAUDE_CONFIG_DIR had hooks written to a config
file Claude never reads. Resolve ${CLAUDE_CONFIG_DIR:-$HOME/.claude} first;
the explicit GSTACK_SETTINGS_FILE override still wins.

Partial #349.

Contributed by @andrefogelman (PR #2239).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): dispatch a change event after fill for change-only validators

Playwright's Locator.fill() dispatches `input` but never `change`, so
frameworks that validate on change (AngularJS ng-change, debounced
strength/match checks) never saw the filled value — correct in the DOM,
failing the framework's own validation. `browse fill` now dispatches
`change` after the fill. Failing-first regression test with a
change-only password-match fixture included.

Contributed by @intelliot (PR #2475).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(safety): unknown question-preference source exits the documented 2, not 1

The --write user-origin gate documents exit 2 as "rejected, do not retry"
(profile poisoning defense), but a source outside both the allowed and the
explicitly-rejected lists fell through to exit 1 — the generic validation
code callers treat as retryable. Unknown sources now exit 2 with the same
do-not-retry rejection message as the known non-user-originated ones.

Closes #2390.

Contributed by @gregario (PR #2429).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pr-title): stop duplicating the version prefix on bare-version titles

A title that was nothing but a version ("v1.2.3" — the form ship uses for
version-only bumps) matched neither the "v<NEW_VERSION> " literal case nor
the trailing-space strip regex, fell through to the prepend path, and came
out as "v1.2.3.4 v1.2.3" — which pr-title-sync.yml then wrote back via
gh pr edit. Handle the bare form in both the no-change case and the
prefix-strip regex, and emit a bare new version when nothing follows.

Closes #1886.

Contributed by @jbetala7 (PR #1887).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(build): escape literal braces in the bun:sqlite stub regex

Perl >= 5.26 treats an unescaped literal `{` in a pattern as fatal
("Unescaped left brace in regex is illegal"), so build-node-server.sh
died at the bun:sqlite stub substitution on modern perl. Escape both
braces; the replacement output is unchanged.

Closes #2300.

Contributed by @nuga0718 (PR #2111).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): preserve spaces in gstack-config values

get/list read values with awk '{print $2}' | tr -d '[:space:]', which
truncated any value containing spaces ("/Users/x/Conductor Workspaces"
came back as "/Users/x/Conductor") and set wrote the unfiltered raw value
on the append path. New read_config_value() strips only the "key:" prefix
and trailing whitespace (cut-style parse), and set appends the same
newline-stripped value the in-place edit path uses.

Closes #1782.

Contributed by @jbetala7 (PR #1783).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): recover a late-healthy detached daemon instead of a false "Server failed to start"

startServer spawns the daemon detached + unref'd, then polls health for a
fixed budget. On a loaded machine the budget can elapse in the gap between
the loop's last tick and the daemon becoming ready — the CLI reported
"Server failed to start within Ns" while the very next `browse status`
showed a healthy server. Add a final readState()+isServerHealthy() re-check
before the timeout throw, and make the budget env-overridable via
BROWSE_START_TIMEOUT (BROWSE_* tunable convention). Structural + behavioral
tests pin both invariants.

Closes #1846.

Contributed by @harjothkhara (PR #1847).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): daemon resilience on loaded machines — Bun conn errors, stop/restart flush, startup + git-root budgets

Four load-sensitivity fixes in the daemon lifecycle:

- sendCommand only recognized Node's ECONNREFUSED/ECONNRESET; the compiled
  CLI runs on Bun, which reports 'ConnectionRefused'/'ConnectionClosed'
  ("Unable to connect..."), so daemon crashes leaked the raw error and
  exited 1 instead of entering the busy-check/restart path. Match both.
- stop/restart called shutdown() inline, which exits before the HTTP
  response flushes — the CLI saw a dropped socket (and would now
  crash-retry a fresh daemon just to stop it). Defer shutdown ~100ms so
  the 200 lands first.
- Non-CI POSIX startup budget raised 8s -> 15s (cold Chromium measured
  ~5.7s at load avg 10; load 12+ blew the old budget while the detached
  daemon was still booting).
- getGitRoot's 2s git rev-parse timeout returned null under load (6.3s
  spikes measured), scattering state files across cwds into split-brain
  daemons. Raise to 8s, still bounded.

Contributed by @mplatts (PR #1732).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): ingest keeps error_message/failed_step instead of dropping them

The telemetry_events columns exist and bin/gstack-telemetry-log already
sends error_message + failed_step, but the Supabase ingest function dropped
both fields on insert — every error report arrived with no message and no
failing step. Map them through with the same bounded-length sanitization as
error_class (500/100 chars). The completion-status resolver now also passes
--error-message/--failed-step in the generated skill telemetry block, with
instructions to leave them empty on success.

Resolver only for the template side; generated SKILL.md files regenerate
from this source in the docs lane.

Contributed by @sunnnybala (PR #769).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): surface non-EEXIST errors in acquireServerLock instead of masking them

acquireServerLock caught every open failure as if the lock were held:
EACCES/EROFS/ENOENT surfaced as phantom "another process holds the lock"
(null return, no diagnostics), and a failed stale-lock read or unlink was
swallowed the same way. Each failure class now logs a coded, pathed
diagnostic: non-EEXIST open errors, holder-PID read errors (ENOENT retries
the acquire — the holder released between open and read), and stale-lock
unlink errors. Four-case unit test included.

Closes #1084.

Contributed by @jbetala7 (PR #1725); same fix independently by
@JiayuuWang (PR #1097).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(paths): shell-quote gstack-paths output so eval round-trips values

gstack-paths emitted bare KEY=VALUE lines, so the documented
eval "$(gstack-paths)" re-parsed the values: backslashes were eaten as
escapes (Windows $TMP C:\Users\... became C:Users...) and a space
word-split the assignment, leaving the variable empty. Emit each value
with printf %q so eval round-trips byte-for-byte; plain POSIX paths are
unchanged. Round-trip regression tests cover backslashes, spaces, and
embedded quotes.

Closes #2374.

Contributed by @fangearhq-boop (PR #2376); same fix independently by
@yannickspiess (PR #1580).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* security(browse): drop .svg from the load-html extension allowlist

SVG is a script-capable format (inline <script>, event handlers, foreign
objects), so allowing it through load-html's HTML allowlist let a local
.svg execute script in the browse session context. The allowlist is now
.html/.htm/.xhtml only; regression test asserts .svg is rejected.

Contributed by @garagon (PR #1153).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(benchmark): validate --timeout-ms as a positive integer

gstack-model-benchmark fed --timeout-ms straight through parseInt, so
"abc" became NaN and "0"/"-1" passed through — a NaN or non-positive
timeout silently disables the per-provider watchdog. Reject anything
that isn't a positive (optionally +-prefixed) safe integer with a clear
error and exit 1.

Closes #1726.

Contributed by @jbetala7 (PR #1727).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(fixtures): clean terminology in the security-bench replay fixture

Two spots in browse/test/fixtures/security-bench-haiku-responses.json
referred to real-world HVAC project naming; replace with the generic
"mechanical services" wording. Fixture stays valid JSON; replay tests
unchanged.

Contributed by @apex-system (PR #2131).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: cancel superseded actionlint and skill-docs runs

actionlint.yml and skill-docs.yml trigger on both push and pull_request
with no concurrency group, so every push to an active branch left the
previous (now-obsolete) runs queued or running — twice per commit on
same-repo PR branches. Add the same cancel-in-progress concurrency
groups the heavier workflows already use, plus a free static tripwire
test that fails CI if a push+pull_request workflow ever ships again
without cancel-in-progress.

Contributed by @jbetala7 (PR #2053).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): correct CJK rendering — NUL sentinel hardening, SC-first fonts, CJK quote context

Three CJK fixes in the PDF pipeline:

- smartypants strips stray input NULs up front so document text can never
  forge the U+0000 placeholder sentinel and leak a preserved-zone marker
  into the output.
- The CJK font stack led with Japanese families, so Simplified-Chinese
  text rendered han glyphs with JP variants. Lead with PingFang SC /
  Heiti SC / Noto Sans CJK SC / Source Han Sans SC before the JP
  fallbacks.
- Quote-smartening only recognized ASCII openers as "start of quote"
  context; the fullwidth colon and CJK brackets now count, so quotes
  after them curl the right way.

Contributed by @rssprivacy-commits (PR #2012).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: regenerate skill output for the quick-win resolver changes

Regen for the deploy-config URL-scheme fix (utility resolver), telemetry
completion-status resolver, and $HOME-doubling binary-resolver fix; ship
goldens refreshed to match. Generated-output-only commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(slug): cached identity is sticky — heal ONLY the provable subdir-cache bug shape

The walk-up rewrite recomputed the slug on every run and "healed" the cache
toward the fresh value, which broke the #2212 continuity contract: a project
that used gstack before adopting a git remote would be silently renamed to
the remote-derived slug, orphaning everything under ~/.gstack/projects/.
Cached identity now wins, with one precise exception: when the cached value
equals THIS pwd's basename while the walk-up proves pwd is not the project
root, the entry came from the pre-walk-up subdirectory bug (#1125) and is
recomputed. All four slug contracts pass together (repo-mode #2212,
walk-up #1125, sanitize, user-slug).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(claude): stop false-blocking macOS keychain subscription auth in host detection

The /claude skill's auth probe only recognized env-var/API-key auth, so
macOS subscription installs (keychain-backed, where `claude -p` works fine)
were told they had no auth. Detection now uses host invocation.

Fixes #1890.

Contributed by @xing-qnex (PR #2411); PR #2548 by @shawnacalia covered the
keychain case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): Ubuntu 26.04 Playwright platform detect + silence the codesign false alarm

Two small setup papercuts: the Playwright platform probe now recognizes
Ubuntu 26.04 instead of falling to the generic-Linux path, and macOS
installs stop warning about a codesign "failure" that was actually the
expected unsigned-adhoc path (the real signature check already gates
binary launch).

Contributed by @nuga0718 (PR #2113) and @lucascaro (PR #1758).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skills): land-and-deploy squash readback, next-version paths, embed-flags quoting

Three template one-liners: land-and-deploy reads the squash-merge result
from the merge commit instead of the stale branch tip; review/landing-report
/land-and-deploy templates call bin/gstack-next-version via its installed
path instead of a bare repo-relative one; setup-gbrain quotes
GBRAIN_EMBED_FLAGS so zsh word-splitting stops silently dropping
voyage-code-3 flags. Regenerated output included.

Contributed by @stormeoio (PR #2011), @rjmurillo (PR #1820) and
@trevorhstandridge (PR #1817).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* release: v1.64.0.0 — fix wave CHANGELOG, VERSION, deferred-wave TODOs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: refresh ship goldens for the telemetry error-field resolver output

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(redact-prepush): assemble the fake AWS key at runtime — the literal blocked our own push

The hook's fixtures carried a live-format AKIA literal, and the repo's own
pre-push scanner (hardened in this wave) correctly blocked pushing it. The
placeholder-suppressed docs key would defeat the detection tests, so the
fixtures now concatenate the key at runtime: tests still exercise real
detection, and the pushed diff never contains a scannable credential shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(slug): terminate the marker walk-up on dirname's fixed point — hung every bin on Windows

Under git-bash on Windows a mixed-form path walks C:/Users -> C: -> . -> .
forever: dirname's fixed point there is never "/", so the walk-up loop spun
and every bin that evals gstack-slug (learnings-log first among them) hung
until spawn timeout. Caught by windows-free-tests CI on the wave PR. Break
on the fixed point itself with a depth cap for exotic forms; regression
tests drive the extracted function with hostile path shapes under a hard
timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 22:02:07 -07:00
1d41ee3ab3 v1.63.0.0 feat: GStack 2 fork port wave — egress receipts, context-bill, sharded gate, /health fix (#2541)
* test(helpers): shared skill-census helper with three explicit counts

physicalSkillFiles (symlinked dirs included, root router included),
authoredSkills (realpath-deduped, router excluded), registryEntries
(what ./setup registers: unique frontmatter names + _gstack-command).

One counting authority for the hermetic seeder, context-bill ground
truth, and the catalog-budget test — connect-chrome's dir symlink and
the root router otherwise produce three subtly different hand-rolled
censuses. Ported-wave foundation (C11).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(evals): stop the harness grading itself

findPreviousRun excluded only the file being written, by name, so every
suite compared against _partial-e2e.json — the current run's own
accumulator, relabelled with the current tier just before each flush.
That is why every block read '+$0.00, +0s, Stable run, no regressions.'
This harness has never been able to detect a regression, and reassuring
output that cannot fail is worse than none. In-progress runs are now
excluded by role, and a run with nothing to compare against says NO
BASELINE instead of claiming stability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit f3140b5245221fff7fb9411c7ec07c2ca11587b5)

* refactor(evals): shared partial-run predicate + finalized-run lookup

isPartialEval(data, filename) is the one place that decides what counts
as an in-progress accumulator (the _partial flag OR a _partial-prefixed
filename), and findLatestFinalizedRun(evalDir, tier) is the one place
that finds the newest real run — scanning the eval dir plus one level of
shards/<slug>/ subdirs, where the sharded paid runner points each
shard's collector. skill-budget-regression.test.ts's hand-rolled
findLatestRun (flag-blind: a flagged-but-renamed accumulator passed its
name check) is replaced by the shared helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit b55fcf6966366fd21a8cdc46de61aab6e1b1d100)

* feat(evals): register shipped skills for hermetic PTY children

Hermetic children get a config dir that deliberately seeds no skills —
right for children that install their own, fatal for the PTY family that
TYPES /office-hours or /plan-ceo-review: claude rejects the command as
Unknown before any model turn, so the plan-family gate smokes measure
nothing. hermeticSkillsConfigDir() is a second, opt-in config dir under
the same runRoot that mirrors ./setup's registration exactly (real dir
per registry name, SKILL.md + sections/ symlinks, frontmatter-name
resolution, _gstack-command root alias), driven by the shared
skill-census so connect-chrome's dir symlink collapses the same way
setup's idempotent overwrite does.

Ported from fork commit 03c4eca2, tree walk rewritten for the upstream
layout (top-level <skill>/SKILL.md dirs, no skills/ tree). Unit tests
are new: seed shape, census parity, symlink resolution, connect-chrome
collapse, idempotence, no-API-key seed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 93dae6107b30ce453a07c2d342b60262bba6ce0b)

* feat(evals): seedSkills opt-in for PTY slash-command tests + tripwire

Wire ClaudePtyOptions.seedSkills through launchClaudePty: when set (and
hermetic, and no per-test CLAUDE_CONFIG_DIR override), the child gets
hermeticSkillsConfigDir() so typed /skill slash commands resolve instead
of dying as Unknown command before any model turn. Opted in at the three
runPlanSkill* helpers and the four direct-launch slash-command tests
(plan-design-with-ui, plan-ceo-mode-routing, autoplan-chain,
ship-idempotency).

New static tripwire (test/pty-skill-seeding-wiring.test.ts): any test
file that sends a slash command over the PTY must route through a
runPlanSkill* helper or pass seedSkills: true — an unseeded slash-command
test spends money and measures nothing. hermetic-wiring.test.ts now
blesses the repo-tree seeding path explicitly (config dir under runRoot,
symlinks into the repo checkout, never operator ~/.claude).

The CI "Register gstack skills for PTY smoke" step keeps a keep-me note:
container cross-mount symlinks defeat the TUI scanner and HOME is not
hermeticized, so the real-file copies there must survive this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 63c52269daaffb833b3105ea9b4b99be6df8fec7)

* refactor(evals): single shared paid-test-set module

test/helpers/paid-test-set.ts is now the one definition of which test
files are paid (the exact globs package.json's test:gate expands).
scripts/test-free-shards.ts derives its free/paid exclusion from it
instead of a private regex list, dropping the dead
browse/test/security-review-fullstack.test.ts pattern (file no longer
exists). The sharded paid runner derives its enumeration from the same
module, so a file added to one list can no longer silently miss the
other.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit a7f36479a6a1f3656452370f5883371f3cb65623)

* feat(evals): env-driven lazy eval dir + shard-aware store and tooling

Importing eval-store no longer spawns the gstack-slug subprocess: the
module-level DEFAULT_EVAL_DIR constant is now a memoized defaultEvalDir()
resolved at collector construction. Resolution order: explicit
constructor arg, then GSTACK_EVAL_DIR, then slug detection — so the
sharded paid runner can point each shard child at its own
<evalDir>/shards/<slug>/ dir with plain env, no --preload.

Runs collected under a shards/ subdir record their slug in the eval
JSON (EvalResult.shard). findPreviousRun scans one shards/<slug>/ level
and prefers same-slug priors, so each shard baselines against its own
history instead of whichever shard flushed last. eval:list,
eval:summary, and eval:compare enumerate the same one level of shard
subdirs; eval:compare's no-arg mode also stops picking an in-progress
accumulator as the after-run.

eval-watch stays flat (documented follow-up): it tails a single dir for
live progress and gains nothing from per-shard baselines until the
runner emits a merged stream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit e1f53f7d9c7fe6b65877d843f2e25bd2e2d12ffd)

* feat(evals): sharded paid tier runner

scripts/test-paid-shards.ts runs the gate/periodic tier one Bun process
per test file, with an EXTERNAL wall-clock timeout that SIGKILLs the
shard's detached process group and an aggregate that distinguishes
passed / failed / timed-out / never-started — partial execution can no
longer read as a pass. Bun's native --shard/--isolate covers none of
this: no process-group kill (hung claude/codex PTY grandchildren
survive in-process isolation), no never-started taxonomy, no per-shard
env. Each shard child gets GSTACK_EVAL_DIR=<evalDir>/shards/<slug>/
(slug = test filename sans extension, stable across runs) so shard
baselines compare against their own prior runs.

Output classification lives in scripts/test-strict-output.ts (strict
exit-code derivation, incremental fail-line classifier, child signal
forwarding) so the runner and any future strict bun-test wrapper share
one implementation. Enumeration derives from the shared paid-test-set
module; tier exclusion fires only on an explicit whole-file
EVALS_TIER === '<other>' guard.

package.json gains test:gate:sharded / test:periodic:sharded, and
eval:bg:gate / eval:bg:periodic now run the sharded scripts with detach
timeouts sized to the worst case (gate: 49 shards x 30min / 4 jobs ~
6.2h -> 25200s; periodic: 59 -> 28800s).

test/paid-shards.test.ts pins enumeration, tier classification, and the
kill-and-continue property with a real busy-loop shard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5e76bd5931836257f896cedfe4e93912cb759c70)

* feat(security): hash-chained egress receipt ledger (core)

Port lib/egress-receipt from the v2 fork as TypeScript: writeReceipt
(sync, fail-closed via typed EGRESS_RECEIPT_FAILED), best-effort
writeOutcome, readLedger/listReceipts/verifyLedger, GSTACK_HOME ->
GSTACK_STATE_DIR -> ~/.gstack resolution, 0600 ledger under a 0700
security dir, and an mkdir spin lock (2.5s budget) with documented
>10s-mtime stale-lock reclaim.

Changes vs the fork:
- lastRawLine tail-reads the final 4KB instead of loading the whole
  ledger, so appends stay O(1) as the file grows.
- WARN-at-size: past 25MB writeReceipt emits one self-explanatory
  stderr warning per process (what the ledger is, how to inspect it,
  rotation TODO); verifyLedger gains a sizeWarning field. Rotation
  TODO carries the chain-genesis sketch (new generation's first record
  embeds the prior file's tail hash).

bin/gstack-egress-receipt is a bun script bridging shell callers:
write|outcome subcommands, exit 3 + EGRESS_RECEIPT_FAILED on stderr on
failure; --no-payload records sha256:null for git-class ops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 619726a3d77d987a2e50151a5727b3faaaf5fc6a)

* chore(bin): delete dead brain-consumer/reader scripts

bin/gstack-brain-consumer and bin/gstack-brain-reader are byte-identical
dead scripts that POST the repo URL + a Bearer token to a /ingest-repo
endpoint gbrain removed (docs/gbrain-sync.md already documents the
removal in past tense). No live references remain; CHANGELOG mentions
are historical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 254ddc69fc5a0270fcc973e36b6a81766d835d2d)

* feat(security): shared shell receipt helpers

bin/gstack-egress-lib.sh (sourced library, gstack-gbrain-lib.sh
precedent) provides _receipted_curl and _receipted_git: write the
egress receipt BEFORE the send via gstack-egress-receipt, hand curl the
SAME payload file via --data-binary @file so the receipt hash matches
the wire bytes exactly, then append a best-effort outcome. Per-call
fail policy: 'closed' refuses the send (return 3, problem/cause/fix
message on stderr) and 'open' warns and proceeds. Payload temp files
are consumed immediately per call — no EXIT traps, since callers like
gstack-telemetry-sync own their own EXIT trap and a sourced trap would
clobber it.

Tested end-to-end against a local Bun.serve listener: receipt sha256
equals the sha256 of the bytes the listener received, fail-closed
refusal never touches the network and carries the problem/cause/fix
stderr shape, fail-open warns and proceeds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 6d067dce2d4c8815dec98be551763c85a3671357)

* feat(security): receipt core shell sinks

Wire the three core bash egress sinks through gstack-egress-lib.sh:

- gstack-telemetry-sync: the batch POST now writes the payload to a
  temp file, receipts those exact bytes fail-closed, and hands curl the
  SAME file. On refusal nothing is sent and the cursor does not
  advance, so the batch stays buffered for the next run. The HTTP
  status is recorded as the receipt outcome.
- gstack-update-check: fail-open receipts (warn + proceed) on the
  Supabase ping POST, both VERSION curls (via a local
  _receipted_version_fetch helper that skips non-network schemes), and
  git ls-remote. The ping receipt is written inside the backgrounded
  subshell, so it can never block the script's exit.
- gstack-brain-sync: fail-closed git-class receipts. The push receipt
  is written BEFORE the commit consumes the queue, so a refused receipt
  leaves the queue intact and the next run retries the whole drain
  (pinned by a new queue-intact-on-refusal test, including the
  problem/cause/fix refusal message shape). The retry-path fetch and
  retry push carry their own fail-closed receipts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 3c60f699acceaf1c92a218874711e05fc17dca5d)

* feat(security): receipt TS module sinks + tunnel

writeReceipt (fail-closed, sha256:null — a subprocess or SDK owns the
wire bytes) before every TS-module network-bearing operation:

- bin/gstack-gbrain-sync.ts: before the gbrain code walk that ships
  repo content to the user's gbrain DB (may be remote Postgres). A
  refused receipt fails the stage with status refused-egress-receipt.
- bin/gstack-memory-ingest.ts: before the gbrain batch import of
  transcript pages. A refused receipt returns a system_error verdict
  without spawning the import.
- browse/src/server.ts: before both ngrok.forward call sites (start-up
  BROWSE_TUNNEL=1 path and the /tunnel/start endpoint). A receipt
  failure lands in the existing catch that tears the tunnel listener
  back down and refuses the start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5677d618a48fcd0ae2b068bf868781d90f809cb5)

* feat(design): receipted fetch for OpenAI calls

design/src/receipted-fetch.ts wraps every api.openai.com call: a
content-free egress receipt (sink design-openai, sha256 of the JSON
body — hash only, never the body) is written BEFORE the send. Polarity
is FAIL-OPEN: user-facing generation must not die because an audit log
hiccuped, so a receipt failure warns on stderr and the call proceeds.
Streams pass through untouched (response bodies returned as-is;
non-string request bodies receipted as sha256:null rather than drained
to hash).

All ten call sites converted with per-command payload classes:
generate, variants (injected fetchFn passes through), iterate (both
threaded and fresh paths), evolve (image + screenshot analysis), check,
diff, design-to-code, memory.

Unit-tested with injected fetch: receipt-before-send ordering, stream
passthrough, and fail-open on an unwritable ledger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c0e5ff6639414ac2fd98e8ac3affb51401746b55)

* feat(security): receipt admin scripts + user git-ops (zero exceptions)

Wire the remaining shell egress through gstack-egress-lib.sh:

- gstack-gbrain-mcp-verify: both JSON-RPC probe POSTs (initialize +
  tools/list) receipted fail-closed via payload files (hash == wire
  bytes). A refused receipt lands in the NETWORK class — no send.
- gstack-security-dashboard / gstack-community-dashboard: the
  community-pulse GETs receipted fail-open (read-only stats must not
  break over an audit hiccup).
- gstack-gbrain-supabase-provision: api_call receipted fail-closed.
  Each retry attempt hands the helper a fresh copy of the body file
  (the helper consumes its payload). The receipt hashes the request
  body only — the PAT never reaches the ledger or any log. Refusal
  exits 8 without retrying.
- git-class sha256:null receipts, fail-open: gstack-artifacts-init
  (ls-remote, initial push, fetch/pull recovery, retry push),
  gstack-brain-restore (staging clone, existing-repo fetch),
  gstack-session-update (self-update pull).

gstack-team-init needs no wiring: every git clone in it is inside an
echoed instruction string, not an executed command.

The lib now self-locates with shell builtins only (no dirname), so
sourcing works under the whitelist-PATH test harnesses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit b8c5e2055b21ab72878b3e46f8047782ee65a11c)

* test(security): egress wiring tripwire + polarity contract

Static-grep tripwire pinning the egress-receipt wiring (threat model in
the header: the ledger is forensic observability of ATTEMPTED egress,
not an exfiltration control):

- Per-sink assertions: every wired TS module imports egress-receipt and
  calls writeReceipt; every wired shell sink sources
  gstack-egress-lib.sh with each network op under a receipt;
  ngrok-proximity check for server.ts; every design api.openai.com call
  routes through receiptedFetch.
- Absence assertions: the dead brain-consumer/reader scripts stay
  deleted (lstat, so a dangling symlink also fails).
- Polarity table pinned as data (fail-closed: brain-sync,
  memory-ingest, gbrain-sync, telemetry-sync, ngrok, mcp-verify,
  supabase-provision; fail-open: design-openai, update-check,
  dashboards, git-class user ops, context-bill --exact) plus per-file
  polarity spot-checks.
- NEW-SINK SCANNER with zero KNOWN_UNWIRED: sweeps bin/, lib/,
  scripts/, design/src, browse/src for curl, absolute-URL fetch(, and
  git remote ops (never local rev-parse/get-url; heredoc bodies and
  message strings excluded) and requires every hit to be receipted or
  in a REASONED exemption list where each entry carries its why.
  Preamble-generated skill prose documented out-of-scope in the header.
- Shebang tripwire: no bin/gstack-* file may carry a node shebang.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit ff69ceeafaf9c017d539b6ad77ff8f95b680b979)

* feat(cli): gstack-egress reader

bin/gstack-egress (bun) — the auditor's view of the receipts ledger:

- list: one row per receipt (what gstack ATTEMPTED to send), with
  --since/--host/--sink filters and --json.
- verify: recompute the hash chain; exit 3 on tamper naming the first
  broken line; prints the sizeWarning when the ledger passes 25MB.
- grants: what CAN leave, built on the upstream config keys only
  (telemetry, artifacts_sync_mode, redact_repo_visibility,
  redact_prepush_hook via gstack-config get) — each grant names its
  file, key, and the exact revoke command.

CLI smoke tests spawn the real bin against a temp GSTACK_HOME,
including a broken-chain fixture asserting exit 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 9e24eca0f1069fea2ea69e7df4e9b256e93d59a3)

* feat(cli): context-bill — token bill-of-materials (stripped port)

lib/context-bill.ts, ported from the v2 fork and STRIPPED to the tiers
this repo's skills can exercise: ALWAYS-ON (per-skill frontmatter bytes
with dead-key and foreign-host-file flags), EAGER (SKILL.md + any
forced 'for every invocation' references), on-disk totals, --diff,
--budget, and --exact with the calibration table. The fork's
CONDITIONAL/TRANSITIVE/LAZY/FAST-PATH parsers understand only its
dispatcher layout and were dropped; the tier fields stay in the report
shape (empty/zero/null) so re-adding a parser is additive.
TOKEN_DIVISORS and their provenance docblock kept; --help notes
recalibration via --exact's calibration block.

Three upstream fixes over the fork:
(a) findSkillDirs treats the walk ROOT as a container — the repo root's
    router SKILL.md is billed AND its children are walked (the fork
    short-circuited and billed one skill); walkMd skips node_modules
    and dot-directories.
(b) installed-tree layout: subdirs that are their own repo checkout
    (a gstack/ clone inside ~/.claude/skills, detected by .git) are
    skipped, and directory symlinks (connect-chrome) are followed with
    a container-recursion cycle guard.
(c) ROUTER_KEYS widened to the upstream frontmatter contract {name,
    description, version, allowed-tools, triggers, preamble-tier}.

--exact writes an egress receipt (sink context-bill-exact, host
api.anthropic.com) BEFORE any count_tokens POST; if the receipt cannot
be written the run degrades to the offline estimate with a warning —
nothing is sent unrecorded. bin/gstack-context-bill is the bun shim.

Tests: fixture-tree ledgers, the three fixes, --diff/--budget exit
codes, --exact with injected fetch (envelope subtraction, receipt
ordering, fail-open degradation), CLI smoke test, and ground truth
against THIS repo via test/helpers/skill-census.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 675c19876b87ec927b555f5f64c7f93130b3de90)

* test(catalog): aggregate discovery-surface budget with ratchet protocol

Every host loads every skill's frontmatter name + description at
discovery, every session. applyCatalogTrim in scripts/gen-skill-docs.ts
shapes each description and the 160KB per-file warn covers body size,
but nothing capped the aggregate frontmatter — the catalog could grow
one reasonable-looking description at a time. This test is that
enforcement layer.

Measures the catalog via test/helpers/skill-census.ts authoredSkills
(symlink-deduped, root router counted separately as the _gstack-command
alias line item): 53 skills + router = 4,420 bytes = 1,105
token-equivalents today, asserted <= 1,150 (~4% headroom). Per-skill
sub-cap of 260 bytes (largest today: design-consultation at 229), plus
a non-empty-description check.

Failure messages are self-service ratchets: they print the new total,
the delta, and the update protocol (bump the constant AND the
derivation comment in the same commit; trim instead of grow for
existing descriptions). Parser handles folded block scalars
(description: >-) for fork parity; import-free by design so it
survives generator refactors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c106fb36f768181b80c257e5cff1cde4f435f9c0)

* fix(browse): extension token bootstrap moves to pinned-origin POST; /health carries no token

GET /health is now liveness/status only in every mode — both token
carve-outs (headed-mode disjunct AND chrome-extension:// Origin
disjunct) are removed. Token bootstrap is POST /extension-token on the
local listener: the Origin header must be exactly
chrome-extension://<GSTACK_EXTENSION_ID> and the Host header's hostname
must parse to 127.0.0.1 or localhost (parsed via new URL, never literal
equality — Host arrives as '127.0.0.1:34567'). Wrong origin/host → 403
with no detail. The tunnel surface 404s the endpoint (not in
TUNNEL_PATHS, verified by test).

The extension ID is pinned by a new "key" field (RSA public key) in
extension/manifest.json; browse/scripts/extension-id.ts reproduces the
ID derivation (first 16 bytes of SHA-256 of the DER public key, hex
mapped 0-9a-f → a-p). The private key is not committed anywhere —
unpacked/baked-in loads only need the public key.

Extension side: background.js bootstraps and refreshes the token via
POST /extension-token (403 → disconnected state); sidepanel.js direct
connect path does the same; sidepanel-terminal.js's dead /health token
fallback (read AUTH_TOKEN/authToken keys the server never sent,
hardcoded port) is replaced with the window.gstackAuthToken path.

MIGRATION NOTE: the manifest key pins the extension ID, so existing
installs' side-panel local state (saved port, snoozes) resets once —
explained in-product via a one-time notice (flag
gstack_id_migrated_v162). After upgrading the server, restart the
browser so the old service worker stops polling for a token GET /health
no longer serves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit e9a0b6847a2d17fe6656a4686b4efd0c8380eb09)

* docs: correct stale compiled-binaries claim; file three egress/eval follow-ups

CLAUDE.md's compiled-binaries section claimed browse/dist binaries are
tracked by git and appear as modified in git status — false since
64d5a3e4 (v0.11.16.0) untracked them, and actively harmful: it trained
agents to ignore dist binaries in git status. The section now states
the truth (untracked + gitignored; a dist binary in git status means
someone force-added it) and covers make-pdf/dist too.

TODOS.md gains the three follow-ups filed by the v1.62 port-wave
reviews: ledger rotation with chain-genesis records, launch-nonce
token bootstrap, and eval-watch shard-awareness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: pre-landing review fixes for the v2 port wave

Review army (checklist + 5 specialists) + coverage/plan audits on the
assembled branch. Genuine correctness/security/hygiene fixes:

- test-paid-shards: strictTestExitCode now receives expectedFiles on the
  real bun path, so a shard that runs fewer files than planned (harness
  crash, nothing loaded) with exit 0 is no longer recorded 'passed' — the
  invisible-non-execution class the runner exists to kill. Pinned by the
  new test/strict-output.test.ts (also covers the chunk-boundary classifier).
- test-paid-shards: EVALS_TIER env is validated (gate|periodic) like the
  --tier flag, so a typo can't self-skip every test and exit 0 green.
- package.json: test:periodic:sharded sets EVALS_ALL=1, restoring the
  full-tier semantics the pre-shard script had (CI already set it; local
  eval:bg:periodic silently under-measured without it).
- brain-sync.test: run() pins HOME to the temp home so gstack-artifacts-init
  stops writing/clobbering the operator's real ~/.gstack-artifacts-remote.txt
  every free-suite run; afterEach now also scrubs the current filename.
- egress-receipt: cap each receipt field at 512B so a serialized line always
  fits the 4KB tail-read window — a longer line would make the next append
  hash a truncated prior line and verifyLedger report a permanent false
  TAMPER. warnLedgerSize short-circuits before statSync once fired (append
  hot path).
- gstack-egress: import.meta.dir (Windows-safe) instead of new URL().pathname
  so grants doesn't silently report defaults on Windows; strip control chars
  from ledger-derived fields on render so a crafted receipt can't spoof the
  auditor's view.
- extension/background.js + CLAUDE.md: renumber the identity-pin migration
  refs v1.62 -> v1.63 (main claimed 1.62.0.0; this wave queue-advances).
- egress-receipt-wiring: pin lib/context-bill.ts unconditionally (both land
  together now); drop the dead RunShardsOptions.tier field.

All fix-affected test files green; gate failures triaged as external-env
(codex/gemini CLI drift) or pre-existing (hermetic-canary fails identically
on base). Deferred polish tracked in the PR body + decision store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump version and changelog (v1.63.0.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: file TODO to harden plan-design-with-ui PTY detection

The v1.63 seedSkills change made this gate test execute for the first
time; it reliably times out because its terminal scraper can't parse the
(correctly-rendered) scope-gate AskUserQuestion out of a spinner-mangled
PTY buffer. Shipped skill behavior is correct — test-harness limitation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: re-slot release as v1.62.1.0 (PATCH per user)

Main claimed 1.62.0.0 while the wave was in flight; the user chose the
PATCH slot over queue-advancing MINOR. Renumbers the identity-pin
migration notice (now version-free flag name so a re-slot never orphans
an already-set flag), the CLAUDE.md /health note, the CHANGELOG heading,
and the TODOS section titles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): stop hard-requiring the literal ok) case label in gbrain-refresh guards

The extractor grepped for 'ok)' but the case label grew to
ok|timeout|thin-client) (#1964, #2051), so the whole file errored on
import — the free suite's only red for months. The extractor now matches
any label starting with ok and its alternations; all 7 guard assertions
run again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): hermetic-canary probes with ${VAR:-} so nounset shells can't fail success

The probe echoed bare $CONDUCTOR_WORKSPACE_PATH — when scrubbing WORKS
the var is unset, and under a nounset shell the echo errors, failing the
canary exactly when isolation succeeds. Defaulted expansions assert
identically under any shell. Fails identically on base; fixed here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(evals): absorb codex/gemini CLI drift; external-service tests go periodic-tier

- codex exec gains --skip-git-repo-check: newer CLIs refuse exec in an
  untrusted non-git dir (our temp skill dirs) — empirically verified.
- gemini: --skip-trust was removed in gemini-cli 0.34 (argv parse error);
  dropped from the session runner and the benchmark adapter. A present-
  but-unusable CLI (deprecated individual code-assist auth path) now
  classifies as SKIP, not a false adapter failure; the benchmark live
  smoke skips on auth/rate_limit error codes (environmental) while still
  failing on timeout/unknown (the drift classes it exists to catch).
- codex-e2e, gemini-e2e, and benchmark-providers gain the canonical
  whole-file EVALS_TIER === 'periodic' guard per CLAUDE.md tiering rule 3
  (external service -> periodic) — the sharded gate runner now excludes
  all three (gate: 45 -> 42 shards).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(evals): parse single-logical-line AskUserQuestions in the PTY runner

When the PTY reflows a boxed AUQ, ALL options land on ONE logical line
after stripAnsi — parseNumberedOptions parsed one option per line, found
only '1.', and the >=2 check failed forever while the correct question
sat on screen (plan-design-with-ui timed out this way twice, with the
rendered scope-gate AUQ visible in both failure buffers). The cursor
line is now parsed as a stream of ascending N. tokens; DEC cursor-
visibility residue is stripped before matching; plan-design-with-ui's
budgets grow to fit observed ~6min preamble+thinking latency. Pinned by
test/pty-auq-single-line.test.ts using the real failure buffers; all 142
existing parser-consumer unit tests still green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: restore v1.63.0.0 (MINOR — user-confirmed final slot)

The wave ships new capability (egress receipts + two CLIs, sharded paid
runner, hermetic skill seeding) at ~8K lines — MINOR scale per the
scale-aware bump rules. Supersedes the brief v1.62.1.0 re-slot; the
version-free migration flag means no state churn from the renumber.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: spell out AskUserQuestion in the PTY single-line fixture

Rename test/pty-auq-single-line.test.ts to
test/pty-askuserquestion-single-line.test.ts and expand the AUQ
abbreviation in identifiers and comments. House style writes
AskUserQuestion in full in filenames, identifiers, and comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: sync every doc surface with the v1.63 release

/document-release audit (4-lane, all claims verified against branch code):

- README: gstack-egress + gstack-context-bill rows in the standalone-binaries
  table; Privacy & Telemetry gains the receipted-egress bullet (attempted-
  egress framing per the shipped threat model).
- ARCHITECTURE: /health is liveness-only, POST /extension-token endpoint row
  + bootstrap mechanics paragraph; new Egress receipt ledger subsection under
  Security model; eval persistence covers the sharded runner, GSTACK_EVAL_DIR,
  and the finalized-run baseline rule.
- CLAUDE.md: sharded test scripts in Commands; sharded semantics in the
  detached-evals section; PTY skill seeding in the hermetic section; egress
  invariant block beside the other server-egress invariants; catalog-budget
  ceiling beside the 160KB token ceiling; project-tree entries for
  lib/egress-receipt.ts, lib/context-bill.ts, scripts/test-paid-shards.ts.
- CONTRIBUTING: seedSkills + live-tree seeding in the hermetic paragraph;
  sharded runner in detached runs; catalog-budget in the Tier 1 list.
- BROWSER: extension token bootstrap section, tunnel egress receipts section,
  identity-pin migration note in manual install.
- REMOTE_BROWSER_ACCESS: tunnel-start receipt bullet in the security model.
- gbrain docs: /sync-gbrain + brain-sync egress-receipt behavior documented;
  dead consumer-token instructions removed (consumer machinery deleted this
  release); new fail-closed refusal added to the error catalog.
- CHANGELOG: measured-vs-ceiling catalog numbers, contributor notes for the
  external-service tier move and the PTY single-line AskUserQuestion parser,
  release date.
- TODOS: /health token-distribution TODO resolved by this release, removed;
  port-wave follow-up sections re-labeled to the shipped version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: sweep drift that predates this release

Surfaced by the /document-release audit; every fix verified against the
current binaries:

- gstack-brain-init was replaced by gstack-artifacts-init in v1.27.0.0
  (hard-delete, no compat shim), but README, USING_GBRAIN_WITH_GSTACK,
  docs/gbrain-sync.md, and docs/gbrain-sync-errors.md still instructed
  users to run it — command-not-found on every follow. Same sweep updates
  ~/.gstack-brain-remote.txt to the canonical ~/.gstack-artifacts-remote.txt
  (legacy name still honored on restore, noted where users copy the file).
- gbrain-sync-errors.md headings re-matched to the literal messages the
  binaries print today (the doc's whole value is grep-by-exact-message):
  'gstack-artifacts-init: ~/.gstack/ is already a git repo pointing at:',
  'Remote not reachable via SSH:', 'Failed to create or find ...'. The
  already-a-repo fix now leads with the command's own set-url suggestion.
- docs/gbrain-sync.md 'Under the hood' linked a plan file that does not
  exist in the repo; replaced with the decisions themselves.
- SIDEBAR_MESSAGE_FLOW startup timeline: /pty-session responds with
  {terminalPort, sessionId, attachToken, leaseExpiresAt} (v1.44 shape,
  verified at browse/src/server.ts:1860), not the retired
  {terminalPort, ptySessionToken} pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: fold the Codex accuracy review of the release docs

Six findings, all verified against source before fixing:

1. 'Every send writes a receipt' overclaimed — fail-open sinks proceed with
   a stderr warning when the receipt write fails, so a fail-open send can go
   unrecorded (lib/egress-receipt.ts:8-14). Descriptive prose now says so;
   the receipted framing keeps 'attempted'.
2. 'Receipts hash the request body' is wrong for subprocess-owned sends —
   git pushes record sha256: null (lib/egress-receipt.ts:71).
3. 'grants shows every consent in force' overclaimed — it reports the four
   standing config settings (bin/gstack-egress:139-181). Reworded in
   README, ARCHITECTURE, and the CHANGELOG entry.
4. 'Zero-exception scanner' vs reality: the new-sink scanner carries a
   reasoned SCANNER_EXEMPT list (user-directed fetches, probes, instruction
   strings, skill prose). CLAUDE.md now names it.
5. Error-catalog cause/fix for the receipt refusal: the writer mkdirs the
   ledger dir itself, so 'missing' isn't a cause and bare chmod fails when
   it is absent — cause reworded, fix is mkdir -p && chmod.
6. gbrain-sync first-run steps described the retired binary's behavior:
   default repo is gstack-artifacts-$USER, and init PRINTS the gbrain
   hookup command (never auto-executes; bin/gstack-artifacts-init:384-419).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
2026-08-14 09:28:56 -07:00
Garry TanandClaude Opus 4.6 dae251e066 feat: team-friendly gstack install mode (v0.15.7.0) (#809)
* feat: add gstack-settings-hook for atomic Claude Code hook management

DRY helper for adding/removing SessionStart hooks in ~/.claude/settings.json.
Handles missing files, deduplication, malformed JSON, and atomic writes
(.tmp + rename) to prevent corruption on crash or disk-full.

Part of team-install-mode feature (credit: Jared Friedman).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add gstack-session-update for automatic team updates

SessionStart hook target that auto-updates gstack at session start.
Background fork (zero latency), throttled to once/hour, with lockfile
(mkdir + PID), stale lock recovery, GIT_TERMINAL_PROMPT=0, and debug
logging to ~/.gstack/analytics/session-update.log.

Part of team-install-mode feature (credit: Jared Friedman).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add --team, --no-team, -q flags to setup

--team enables auto_upgrade and registers SessionStart hook via
gstack-settings-hook. --no-team reverses it. -q/--quiet suppresses
all informational output (for hook-triggered setup runs). --local
now prints a deprecation warning.

Replaces ~20 echo calls with log() helper for quiet mode support.

Part of team-install-mode feature (credit: Jared Friedman).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add gstack-team-init for repo-level team bootstrapping

Two modes: 'optional' (gentle CLAUDE.md suggestion) and 'required'
(CLAUDE.md enforcement + .claude/hooks/check-gstack.sh PreToolUse hook
that blocks work without gstack installed). Atomic JSON writes,
idempotent, prints git add instructions.

Part of team-install-mode feature (credit: Jared Friedman).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: deprecate vendoring, document team mode, clean up uninstall

- README: replace "Step 2: Add to your repo" vendoring instructions
  with team mode (./setup --team + gstack-team-init)
- CLAUDE.md: rename "Vendored symlink awareness" to "Dev symlink
  awareness", add deprecation note
- CONTRIBUTING.md: remove vendoring language from prefix section
- bin/gstack-uninstall: clean up SessionStart hook on uninstall

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add vendoring deprecation detection to skill preamble

Detects vendored gstack in CWD (.claude/skills/gstack/ that's not a
symlink and has VERSION or .git). Outputs VENDORED_GSTACK: yes/no.
Adds generateVendoringDeprecation() section that offers one-time
migration to team mode via AskUserQuestion.

Part of team-install-mode feature (credit: Jared Friedman).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate SKILL.md files with vendoring deprecation preamble

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: team mode (v0.15.7.0) — credit Jared Friedman

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add integration tests for team mode (20 tests)

Covers gstack-settings-hook (add, remove, dedup, preserve existing,
atomic write), gstack-session-update (guards, throttle, non-fatal),
gstack-team-init (optional, required, enforcement hook, idempotent),
and setup flags (-q, --local deprecation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:49:03 -07:00