From c24121663732644c56280474a1720475dbe53127 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 5 Sep 2026 14:45:28 -0700 Subject: [PATCH] v1.80.0.0 fix: setup survives a failed Chromium install, hooks share one state root, gstack never clobbers a skill it did not create (#2802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(freeze): hook reads the same state root /freeze writes — fails closed under GSTACK_HOME (#1459, #1509) check-freeze.sh resolved its state dir as ${CLAUDE_PLUGIN_DATA:-$HOME/.gstack} while every writer (/freeze, /guard, /unfreeze, /investigate) resolves through bin/gstack-paths, GSTACK_HOME first. With GSTACK_HOME set, /freeze wrote freeze-dir.txt under GSTACK_HOME, the hook read $HOME/.gstack, found no file, and allowed everything — a deny-tier boundary failing open. One resolver now: gstack_hook_state_root() in careful/bin/hook-extract.sh (already sourced by both check-freeze.sh and check-careful.sh) implements the exact gstack-paths chain, including the CLAUDE_PLUGIN_ROOT guard that keeps a CLAUDE_PLUGIN_DATA leaked from another plugin from redirecting our state. check-freeze.sh and gstack_hook_log_fire both call it; nothing spawns gstack-paths from a hook. Tests: the GSTACK_HOME deny regression, GSTACK_HOME-over-CLAUDE_PLUGIN_DATA precedence, plugin-root guard both ways, and a byte-parity check against bin/gstack-paths across six env combinations. Existing freeze tests now pass CLAUDE_PLUGIN_ROOT like a real plugin install would. Idea from PR #1509 (@NikhileshNanduri); implemented natively against the shared resolver rather than a second fallback chain. Co-Authored-By: Claude Fable 5.1 * fix(relink): never delete or link over a skill gstack does not own (#2119) gstack-relink runs on every ./setup. Its cleanup did `rm -rf` on any same-name entry whose SKILL.md was a symlink, with no readlink check, and its link step did `mkdir -p` then `ln -snf` onto any existing SKILL.md — on Linux that replaces a user's real file with a symlink into gstack (macOS refused by accident). setup's Windows mode-flip cleanup deleted any real dir whose name matched a gstack skill. A personal `qa` skill, or a fork installed under another path, was destroyed by the installer of a tool it never asked for. Ownership is now proven, never assumed. An entry is ours when it is a symlink resolving into INSTALL_DIR or RENDER_DIR, a real dir whose SKILL.md is such a symlink, or a real dir carrying the .gstack-owned marker setup now writes for Windows copy installs (legacy copies count when byte-identical to the source or carrying gen-skill-docs' AUTO-GENERATED header). Anything else — including an entry whose readlink fails — is foreign: left untouched, reported on stderr, and listed in relink's summary line. The same rule replaces setup's Windows name-match deletion; setup:1040 and gstack-uninstall:204 already gated on readlink, so this closes the last unguarded deleter of the class. Tests: foreign real dir in flat mode, foreign flat entry on a prefix flip, foreign directory symlink, RENDER_DIR-targeted entry (ours), marker-carrying copy (ours), marker-less copy (foreign); the Windows cleanup test now proves provenance three ways and keeps the user's own same-name skill. Idea and two regression cases from PR #2119 (@smblight); implemented on the destination entry, not only the symlink target. Co-Authored-By: Claude Fable 5.1 * fix(setup): Chromium bootstrap is best-effort and bounded — skills always register (#1900, #1901, #1902, #913, #2233) setup runs under `set -e`, and the Chromium bootstrap in section 2 sat ahead of skill registration in section 4 with a bare `bunx playwright install chromium`, an unbounded download, and an explicit `exit 1` after the post-install launch probe. On an offline, proxied, or AppArmor-restricted box the user ended with ZERO skills registered and a re-run that died at the same line; a wedged download hung setup indefinitely. Every browser failure now records a reason code in _PW_FAIL_REASON and setup continues: skipped (GSTACK_SKIP_PLAYWRIGHT=1, #913), chromium-install, chromium-install-timeout (the download is bounded by the existing _wait_with_deadline helper, default 600s, env GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT, process tree killed via _kill_tree), chromium-install-locked (another setup holds the lock: this one registers skills and re-probes next time instead of exiting), windows-no-node, windows-node-modules, post-install-launch (with the GSTACK_CHROMIUM_NO_SANDBOX=1 hint for Ubuntu 24.04's userns policy, #2157). The daemon font refresh is skipped when Chromium is unavailable. The final summary names the skills that need the browser (/qa, /qa-only, /design-review, /browse, make-pdf, /pair-agent) and the fix for the recorded reason, and logs the reason code (never a path) through gstack-telemetry-log when telemetry is on. Tests: static invariants over the anchor-sliced block (no exit, every reason code, deadline helper, trap chaining, guarded refresh, summary contents) plus an integration harness that executes the real block with a stubbed probe and installer: install failure, hang killed at the deadline with the tree kill recorded, non-numeric knob fallback, live lock (continues, installer not run, lock preserved), stale lock reclaimed, post-install probe failure, and the skip flag. Credit @DavidMiserak (PR #1900) for the best-effort shape; re-implemented on the current block. Co-Authored-By: Claude Fable 5.1 * docs(designs): preserve the time-attack fork-port residual evaluation The read-only evaluation of what remains portable from time-attack/gstack (583 raw candidates, 415 canonical, 287 with a residual, 48 adversarially refuted, 14 standing) lived only on a throwaway VM. This records the report, the lite residual index, the absorbed/superseded ledger, the refuter verdicts, and SHAS.md with the fork tip, upstream HEAD, merge-base, and a sha256 per file, so every scheduled fix in this wave series traces to its evidence. Co-Authored-By: Claude Fable 5.1 * docs: file the fork-port residual deferrals and document the Chromium bootstrap knobs TODOS.md gains the seven items the CEO and eng reviews of the fork-port residual plan deliberately deferred (shared ownership helper, config-key reader tripwire, "pre-existing" vocabulary, opt-in reply_language, .auth.json writer removal, the fork-derived-change rule for CONTRIBUTING, hook slug parity audit), each with rationale, and updates the two residual bullets for PR #2232 and PR #2233 with their dispositions. README's Troubleshooting section explains the best-effort Chromium bootstrap and its three knobs. Co-Authored-By: Claude Fable 5.1 * fix(relink): canonicalize link targets before the ownership check Pre-landing review finding: the ownership gate compared readlink output textually against INSTALL_DIR and RENDER_DIR, so two shapes of gstack's OWN entries read as foreign and were left behind on a mode flip — a legacy relative link (`gstack/qa/SKILL.md`, resolved against $PWD instead of the link's directory) and an entry linked against the real path of a symlinked install dir (~/.claude/skills/gstack -> checkout). Both now resolve: relative targets anchor at the link's directory, the directory part is canonicalized with pwd -P (the basename stays verbatim so a dangling managed target is not misread), and both spellings of each root are accepted. Two regression tests. Co-Authored-By: Claude Fable 5.1 * fix(telemetry): one-shot setup events never sweep other sessions' pending markers gstack-telemetry-log finalizes every .pending- marker that is not the caller's own as outcome:unknown and deletes it. setup's onboarding events (_setup_welcome, _setup_playwright) have no session of their own, so a Chromium bootstrap failure during a live skill session recorded a false unknown for that session and removed its marker. New --no-sweep flag skips the stale-marker pass; both setup call sites use it (the synthetic --session-id did not prevent the sweep). Surfaced by the Codex adversarial pass. Co-Authored-By: Claude Fable 5.1 * fix(hooks): partial upgrades fail closed for freeze and fall back for careful A hook script and its sourced helper can be copied at different times. With an older careful/bin/hook-extract.sh that lacks gstack_hook_state_root: - check-freeze.sh now emits a deny ("fail closed, re-run ./setup or /unfreeze") instead of dying under set -e with no decision JSON. - check-careful.sh falls back to ${GSTACK_HOME:-$HOME/.gstack} so project rules under the plain chain still load and a decision is always emitted (a warn hook must never break on a stale helper). gstack_hook_state_root prints its root without a trailing newline and both callers capture it with a printf-x sentinel, so a GSTACK_HOME ending in a newline round-trips byte-for-byte with the writer's %q form. gstack_hook_log_fire stays on ${GSTACK_HOME:-$HOME/.gstack}/analytics, the same two-step chain every other analytics writer and reader uses, so the usage log remains one file under a plugin install. Co-Authored-By: Claude Fable 5.1 * fix(setup): never link over, copy over, or reap a skill gstack does not own (#2119) The relink gate alone left three destructive sites open: - link_claude_skill_dirs runs BEFORE relink on every ./setup and used `ln -snf` (Linux replaces a user's real SKILL.md with a symlink into gstack) or, on Windows, rm -rf + cp followed by a marker that made the user's directory "ours" on the next flip. It and _install_alias_skill_md now consult _claude_entry_is_ours first and skip loudly. - cleanup_prefixed_claude_symlinks kept a bare name-match deletion and a `*gstack*` substring match. Symlink arms use anchored `gstack/` segment patterns; the Windows real-file arm proves provenance (marker, byte-identity with our source, or the full two-line gen-skill-docs banner within the first 40 lines, never a one-line substring another generator could emit). cleanup_old_claude_symlinks uses the same banner rule. - gstack-relink's fast path judged absolute targets before canonicalizing, so `/x/gstack/../foreign/SKILL.md` counted as ours; dot-segment targets now canonicalize first. Its banner rule matches setup's. The `.gstack-owned` marker records the owning payload's realpath. Entries skipped by setup or relink are listed in the final setup summary. Chromium bootstrap refinements from the pre-landing review: an INT/TERM trap kills the installer's process tree; the Windows npm chain no longer masks an install failure; GSTACK_SKIP_PLAYWRIGHT=1 is reported as a choice rather than a failure and sends no telemetry; the timeout knob is normalized (0, 000, non-numeric, or more than nine digits fall back to the 600s default instead of killing on the first poll or never killing). Co-Authored-By: Claude Fable 5.1 * docs: README Chromium note outside the CLAUDE.md fence; report banner stripped; deferrals name the four gate sites - README: the Chromium troubleshooting paragraph sat inside the CLAUDE.md snippet code fence, so copy-paste put it into users' CLAUDE.md. Moved to the troubleshooting list. - docs/designs/fork-port-residual-2026-09/REPORT.md: the scratch-run preamble banner is gone; SHAS.md re-hashed. - TODOS: the ownership-gate deferral names the four sites and the marker-path idea for the fork-with-banner residual. Co-Authored-By: Claude Fable 5.1 * docs(todos): the bootstrap block coverage gap is pinned except the quarantine helper Co-Authored-By: Claude Fable 5.1 * fix(setup,relink): ownership proof has two strengths; weak proof never deletes a directory or discards a differing file The first #2119 gate treated a byte-identical or banner-bearing real-file SKILL.md as full ownership, so a prefix flip could rm -rf a user's directory (their own qa skill started from a gstack SKILL.md, plus my-templates/) and the link pass could replace their customized file with a symlink. Two strengths now: - STRONG: the .gstack-owned marker (we created the directory), or a directory holding nothing but symlinks and the marker (deleting it loses no data). Only strong proof removes a directory whole. - WEAK: byte-identity with our source or the two-line gen-skill-docs banner on a real file. Weak proof covers that SKILL.md and our runtime-asset links only; a differing file is moved to ${GSTACK_HOME:-~/.gstack}/backups/skills/// before we link over it, and setup/relink print one summary line naming what moved. The marker is written on every platform now (path-independent proof for Windows copies and for checkouts whose path carries no gstack segment), but only for a directory gstack creates: a directory we merely link into (unclaimed, or a legacy install) never becomes deletable whole. A directory with no SKILL.md at all is unclaimed: the link pass may add our file, the cleanup pass has nothing to remove. Also from the review passes: the banner check reads 8192 bytes, not 40 lines (investigate, office-hours, plan-ceo-review and design-consultation carry the banner past line 40 and were left "foreign" on pre-marker Windows installs); a link into a checkout named without a gstack segment (git worktree add ../gstack-) is ours when that tree carries setup + VERSION + bin/; relink's fast path is gone so both files canonicalize before judging; relink's root alias (_gstack-command) is gated and stamped like every other entry; relink reports the bare entry name with setup's wording and setup dedupes when forwarding (_run_relink_quiet); the summary names the browser skills as examples. Co-Authored-By: Claude Fable 5.1 * fix(setup): Chromium-install lock reclaim is atomic and pid-validated; abandoned locks expire; the tree kill walks /proc without pgrep - A pid file holding "", "-1" or "0" counted as a live holder (kill -0 -1 signals every process and succeeds), locking Chromium out for good. A pid must be a positive integer; anything else is stale. - Two setups judging the same lock stale raced on rm -rf + mkdir and the loser deleted the winner's fresh lock. The stale dir is renamed first (atomic), so exactly one reclaims. - A lock dir with no pid file (killed between mkdir and echo) was never reclaimed; it now expires once older than the install bound. - _kill_tree needed pgrep; debian-slim and git-bash ship none, so the bound killed only the wrapper subshell and the installer kept running. Without pgrep the children are found by walking /proc/*/stat. - The timeout knob is normalized in one place with one comment; the trap's exit 130 is the only exit the block may contain. Co-Authored-By: Claude Fable 5.1 * fix(freeze): an unexpected non-zero death denies via an EXIT backstop instead of exiting with no decision set -e plus a failing pipeline (a tool on PATH exiting non-zero, a deleted cwd) ended the deny-tier hook with no JSON, which Claude Code treats as non-blocking: the edit outside the boundary proceeded. The EXIT trap now prints a deny for any non-zero exit that happens before a decision was written; every deliberate output sets _FREEZE_DECIDED first so a late failure never prints a second object. Tests also pin careful's state-root precedence (GSTACK_HOME over CLAUDE_PLUGIN_DATA, plugin data when CLAUDE_PLUGIN_ROOT names gstack) and the specific "out of date" deny for a helper without gstack_hook_state_root. Co-Authored-By: Claude Fable 5.1 * refactor(telemetry): guard the stale-marker sweep with an if, not a break inside the loop Co-Authored-By: Claude Fable 5.1 * docs(todos): the ownership gate lives in six sites, and the cleanup arms inline their own chain Co-Authored-By: Claude Fable 5.1 * test: the two remaining linker harnesses extract the ownership helpers; the marker is the one allowed dotfile setup-claude-skill-assets and user-render-out-dir-install slice link_claude_skill_dirs out of setup without the helpers it now calls, so the extracted function died with "command not found" (or, inside an if, degraded into "foreign, skipped"). Both harnesses now carry the full helper set and the globals. The hidden-files census allows .gstack-owned, which the linker writes for directories it creates rather than copying from the skill source. Co-Authored-By: Claude Fable 5.1 * fix(setup,relink): weak proof never costs the user a file — assets, flips, failed backups, foreign dir links, alias markers Third review cycle on the ownership model, every item reproduced against a fixture before the fix: - Runtime assets (sections/, templates/, checklist.md, ...) were refreshed with rm -rf regardless of who owned the directory, so an unclaimed or weakly-owned directory lost the user's same-named real files. Real assets are now replaced only in a directory gstack created or strongly owns (marker, or SKILL.md symlink into gstack), plus the legacy Windows real-copy shape; elsewhere they are kept and reported. Symlinks are never content and are always refreshed. - The prefix-flip cleanup deleted a customized banner-bearing SKILL.md that the link pass would have backed up. Both cleanups now compare the file against the source (raw, or with its name: line rewritten to the entry name, which is how alias and prefixed copies legitimately differ) and move a differing file to the backup root. - A failed backup (unwritable root) returned success and the caller linked over the file anyway. It now fails, and the entry is left untouched and reported. - A foreign DIRECTORY symlink whose target had no SKILL.md fell through to the "unclaimed directory" rule and was replaced by a real directory. A symlink that does not resolve into gstack is foreign, full stop. - The alias installers stamped .gstack-owned into pre-existing directories; they now follow the same created-or-already-marked rule. - A directory counts as "only links" only when every link resolves into gstack: a user's own symlink makes it mixed, so their link survives. - The gstack-tree heuristic requires bin/gstack-relink, not just a VERSION file, a setup script and a bin/ directory. Co-Authored-By: Claude Fable 5.1 * fix(setup): lock reclaim hands a fresh lock back; a live holder past the bound is stale; /proc walk strips through the last paren - Reclaim renamed the lock by path after judging it stale, so a second setup that had already reclaimed and re-created it lost its fresh lock and two installers ran. After the rename the moved directory's pid is re-read: a new live holder, or a fresh lock whose pid is not written yet, is moved straight back. - A pid file whose process is alive but whose lock is older than the install bound is stale too (the holder is past its own deadline, or the pid was recycled to an unrelated long-lived process); it was locked forever. - The /proc fallback stripped the comm field to the FIRST ") ", so a comm containing ") " hid a child from the kill. proc(5) says the last paren. Co-Authored-By: Claude Fable 5.1 * fix(freeze): mark the decision written after the helper prints, not before If gstack_hook_decision ever failed between the flag and its output the backstop would have stayed silent; setting the flag after the print keeps the deny backstop armed until a decision is actually on stdout. Co-Authored-By: Claude Fable 5.1 * chore: bump version and changelog (v1.80.0.0) Co-Authored-By: Claude Fable 5.1 * docs: update project documentation for v1.80.0.0 README troubleshooting + manual uninstall cover the skill ownership gate (.gstack-owned marker, ~/.gstack/backups/skills//, foreign same-name skills left untouched). CLAUDE.md and CONTRIBUTING carry the ownership and best-effort Chromium bootstrap invariants for people editing setup and gstack-relink. PROJECT_STRUCTURE gains careful/, freeze/, guard/, unfreeze/, gstack-upgrade/, gstack-relink, and the setup/relink/hook test files. TESTING_INTERNALS documents the anchor-sliced setup harness convention. Co-Authored-By: Claude Fable 5.1 * fix(setup): the final summary reports customized SKILL.md files moved to the backup root The linker moved a weakly-proven, customized SKILL.md aside before linking over it but never said so; only relink printed a "Moved N" line, and by the time relink runs the file is already a symlink. The summary now names each moved file and where it went, next to the foreign-entry report. Co-Authored-By: Claude Fable 5.1 * test: run assembled setup harness scripts from a temp file, not `bash -c` argv (Windows MSYS2 8 KB truncation) windows-free-tests (run 33907177851) failed in test/setup-alias-name-uniqueness.test.ts with bash: -c: line 178: unexpected EOF while looking for matching `' The harness slices functions out of `setup` and passed the joined script as one `bash -c` argv element. The ownership gate grew that script from 6.7 KB to 15.7 KB, and on Windows bash is an MSYS2 program: when its parent is a non-MSYS process (bun), msys-2.0.dll's build_argv() runs any argument containing `?*["'(){}` through globify()/glob(), which copies the pattern into a fixed `Char patbuf[8192]` and silently stops after 8192 - MB_CUR_MAX (8186 chars under C.UTF-8); GLOB_NOCHECK then returns the truncated text as the argument. Character 8186 lands inside the single-quoted sed token on line 178. Rebuilding the exact script with CI path shapes and cutting it at 8186-8190 characters reproduces the identical message locally; cmd.exe's 8191-UTF-16 cap and CreateProcess's 32767 do not fit the evidence. Fix: test/helpers/bash-script.ts writes the script to a temp file and runs `bash ` — a short glob-free argument that never enters globify. Every setup harness that assembled a script for `bash -c` (11 files, 22 sites) uses it; timeouts and env are preserved verbatim, spawn/timeout errors are appended to stderr, temp cleanup is best-effort. `spawnSync('bash', [])` already passes on windows-latest in setup-help, uninstall-windows-copies and the migration tests. The Windows-curated list is byte-identical before and after. Co-Authored-By: Claude Fable 5.1 * docs(test-free-shards): the rerun-refresh harness spawns bash via test/helpers/bash-script.ts, not bash -c Co-Authored-By: Claude Fable 5.1 --------- Co-authored-by: Claude Fable 5.1 --- CHANGELOG.md | 45 + CLAUDE.md | 14 + CONTRIBUTING.md | 6 +- README.md | 28 + TODOS.md | 71 +- VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- bin/gstack-relink | 266 ++- bin/gstack-telemetry-log | 11 + careful/bin/check-careful.sh | 10 +- careful/bin/hook-extract.sh | 31 +- docs/PROJECT_STRUCTURE.md | 11 +- docs/TESTING_INTERNALS.md | 20 + .../fork-port-residual-2026-09/REPORT.md | 212 ++ .../fork-port-residual-2026-09/SHAS.md | 21 + .../records-other.json | 2104 +++++++++++++++++ .../fork-port-residual-2026-09/refuters.json | 338 +++ .../residual-index-lite.json | 1 + .../fork-port-residual-2026-09/summary.json | 28 + freeze/bin/check-freeze.sh | 39 +- package.json | 2 +- scripts/test-free-shards.ts | 4 +- setup | 508 +++- test/helpers/bash-script.ts | 60 + test/hook-scripts.test.ts | 306 ++- test/relink.test.ts | 560 +++++ test/setup-alias-name-uniqueness.test.ts | 25 +- test/setup-bun-cmd-and-pipe-bugs.test.ts | 4 +- test/setup-claude-skill-assets.test.ts | 24 +- test/setup-cleanup-orphans.test.ts | 66 +- test/setup-conductor-worktree.test.ts | 9 +- test/setup-emoji-font.test.ts | 8 +- test/setup-link-ownership.test.ts | 545 +++++ test/setup-playwright-best-effort.test.ts | 649 +++++ test/setup-windows-fallback.test.ts | 7 +- test/setup-windows-rerun-refresh.test.ts | 4 +- test/telemetry.test.ts | 14 + test/user-render-out-dir-install.test.ts | 20 +- 38 files changed, 5938 insertions(+), 137 deletions(-) create mode 100644 docs/designs/fork-port-residual-2026-09/REPORT.md create mode 100644 docs/designs/fork-port-residual-2026-09/SHAS.md create mode 100644 docs/designs/fork-port-residual-2026-09/records-other.json create mode 100644 docs/designs/fork-port-residual-2026-09/refuters.json create mode 100644 docs/designs/fork-port-residual-2026-09/residual-index-lite.json create mode 100644 docs/designs/fork-port-residual-2026-09/summary.json create mode 100644 test/helpers/bash-script.ts create mode 100644 test/setup-link-ownership.test.ts create mode 100644 test/setup-playwright-best-effort.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 17970e7dd..ed065605f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # Changelog +## [1.80.0.0] - 2026-09-04 + +**Setup finishes even when Chromium cannot be installed.** +**gstack never deletes or overwrites a skill it did not create.** + +Three defects that a downstream fork kept tripping over are fixed at the source. `./setup` used to abort at the Playwright step on any box where the Chromium download failed or hung (offline, proxied, AppArmor-restricted), and because that step ran before skills were registered, those users ended with zero skills. The `/freeze` deny hook read a different state directory than `/freeze` wrote whenever `GSTACK_HOME` was set, so the boundary silently allowed everything. And both `./setup` and `gstack-relink` would replace or delete any skill entry that happened to share a name with a gstack skill, including a skill you wrote yourself. + +Now the Chromium install is best-effort and bounded. It runs under a 600 second deadline you can change with `GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT`, skip outright with `GSTACK_SKIP_PLAYWRIGHT=1`, and every failure becomes a reason code in the final summary that names the skills that need a browser. Skills always register. Both PreToolUse hooks resolve the same state root the writers use, and freeze fails closed on any unexpected death instead of exiting with no decision. Ownership is proven, never assumed from a name: an entry is only touched when it is a symlink into gstack, carries the `.gstack-owned` marker gstack writes for directories it creates, or is a real file gstack generated. Even then a generated file you have since customized is moved to `~/.gstack/backups/skills//` before gstack links over it, and a directory is only removed whole when nothing of yours is inside. + +### The numbers that matter + +Source: the free suite (`bun run test`), specifically `test/setup-playwright-best-effort.test.ts`, `test/setup-link-ownership.test.ts`, `test/relink.test.ts` and `test/hook-scripts.test.ts`, run against this tree. + +| Metric | Before | After | Δ | +|---|---|---|---| +| Skills registered when the Chromium download fails or hangs | 0 | all | setup never aborts at the bootstrap | +| Longest a wedged Chromium install can block `./setup` | unbounded | 600s default | `GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT`, `GSTACK_SKIP_PLAYWRIGHT=1` | +| `/freeze` boundary with `GSTACK_HOME` set | allowed every edit | denies | one state-root resolver for hooks and writers | +| Sites that could delete or replace a same-name skill without proof of ownership | 5 | 0 | setup linker, alias installer, both flip cleanups, relink | +| Data lost when gstack replaces a generated SKILL.md you customized | the file | none | moved to `~/.gstack/backups/skills//` | +| Free tests pinning these behaviors | 0 | 103 | across 10 files | + +The first row is the one you feel: a laptop on hotel wifi, a CI runner behind a proxy, or a fresh Ubuntu 24.04 box now ends `./setup` with every skill installed and one clear line saying which ones will not work until Chromium is present. + +What this means for anyone installing or upgrading: run `./setup` anywhere and get skills. Keep your own `qa` or `ship` skill next to gstack's and it survives every upgrade and prefix flip, reported by name instead of silently replaced. Set `GSTACK_HOME` and `/freeze` means what it says. + +### Itemized changes + +#### Fixed +- **`./setup` no longer aborts when Chromium cannot be installed** (#1900, #1901, #1902, #913). The Playwright step is best-effort and bounded. Reason codes: `skipped`, `chromium-install`, `chromium-install-timeout`, `chromium-install-locked`, `windows-no-node`, `windows-node-modules`, `post-install-launch`. A wedged installer is killed with its whole child tree (pgrep, or a /proc walk where pgrep is missing); Ctrl-C mid-install kills it too. A stale install lock is reclaimed atomically, a garbage pid file counts as stale, and a lock with no recorded holder expires after the install bound. Contributed by @DavidMiserak (#2233). +- **`/freeze` enforces its boundary under `GSTACK_HOME`** (#1459). The hook resolves the state root exactly as `bin/gstack-paths` does (GSTACK_HOME, then CLAUDE_PLUGIN_DATA only when CLAUDE_PLUGIN_ROOT names gstack, then `~/.gstack`), a trailing newline in the path round-trips, a helper from an older install denies instead of exiting 127, and an EXIT backstop denies on any unexpected failure. `/careful` reads its project patterns from the same root and falls back safely on a stale helper. Contributed by @NikhileshNanduri (#1509). +- **gstack never deletes or links over a skill it does not own** (#2119). `./setup`'s linker and alias installer, both prefix-flip cleanups, and `gstack-relink` all prove ownership first and report a foreign entry by name in the final summary. Runtime assets (sections, templates, checklists) inside a directory gstack did not create are kept, not replaced. A checkout named without a `gstack` path segment (a `git worktree add ../gstack-feature`) still counts as gstack's. Contributed by @smblight. +- **Windows copy installs carry a `.gstack-owned` marker** so provenance no longer rests on the directory name. The marker is written on every platform, only for directories gstack creates. +- **Setup's one-shot telemetry events no longer finalize other sessions' in-flight markers** as `outcome: unknown`. + +#### Added +- `GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT=` and `GSTACK_SKIP_PLAYWRIGHT=1` for `./setup`; documented in the README troubleshooting list together with `GSTACK_CHROMIUM_NO_SANDBOX=1`. +- A final setup summary that names the browser-dependent skills when Chromium is unavailable, lists any same-name skills left untouched, and reports any customized SKILL.md moved to `~/.gstack/backups/skills//`. +- `gstack-telemetry-log --no-sweep` for events that own no session. + +#### For contributors +- The time-attack fork evaluation that surfaced these defects is preserved under `docs/designs/fork-port-residual-2026-09/` (report, residual index, refutation verdicts, hashes). Waves B through E2 of that plan are scheduled work. +- New free tests: `test/setup-link-ownership.test.ts`, plus large additions to `test/setup-playwright-best-effort.test.ts`, `test/relink.test.ts`, `test/hook-scripts.test.ts`, `test/telemetry.test.ts`. Anchor-sliced harnesses now fail loudly on `command not found` instead of degrading into "foreign, skipped". +- `bin/gstack-relink` and `setup` carry the same ownership rule in two copies; the shared helper is filed in TODOS.md. + ## [1.79.0.0] - 2026-09-01 **/ship can no longer be stranded by a backgrounded subagent.** diff --git a/CLAUDE.md b/CLAUDE.md index 115a52282..66a67f5d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -254,6 +254,20 @@ Names are either short (`qa`) or namespaced (`gstack-qa`), controlled by `skill_prefix` in `~/.gstack/config.yaml`. Pass `--no-prefix` or `--prefix` to skip the interactive prompt. +**Ownership gate (#2119):** `setup` writes a `.gstack-owned` marker into every +skill directory it creates, and `setup` (the linker, the alias installer, and +both prefix-flip cleanups) and `bin/gstack-relink` only delete or link over an +entry they can prove is gstack's. Strong proof (a symlink resolving into gstack, +or the marker) allows deleting or refreshing the whole directory. Weak proof (a +real SKILL.md byte-identical to the source, or carrying gen-skill-docs' two-line +banner) covers only that one file, and a weakly-proven file that differs is +moved to `~/.gstack/backups/skills///SKILL.md` before gstack links +over it. Anything else is a foreign skill: skipped, and named in setup's final +summary. The rule lives in two copies (`setup` and `bin/gstack-relink`); keep +them in sync until the shared helper filed in TODOS.md lands. Pinned by +`test/setup-link-ownership.test.ts`, `test/setup-cleanup-orphans.test.ts`, and +`test/relink.test.ts`. + **Note:** Vendoring gstack into a project's repo is deprecated. Use global install + `./setup --team` instead. See README.md for team mode instructions. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba2d5e0d5..1502420ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -460,6 +460,8 @@ When Conductor creates a new workspace, `bin/dev-setup` runs automatically. It d - **`.claude/skills/` is gitignored.** The symlinks never get committed. - **Never write raw `ln -snf` in `setup`.** Every link site in `setup` MUST route through the `_link_or_copy SRC DST` helper near the `IS_WINDOWS` detection. The helper preserves `ln -snf` on Unix and switches to `cp -R` / `cp -f` on Windows without Developer Mode, where plain `ln -snf` produces frozen file copies that don't refresh on `git pull`. `test/setup-windows-fallback.test.ts` enforces this with a static invariant — a single raw `ln` call outside the helper body fails CI. - **Synchronous subagent dispatches must state the flag.** Claude Code runs Agent-tool subagents in the background by default (since v2.1.198), so any template step that dispatches a subagent and consumes its output must carry `run_in_background: false`. Use the `{{FOREGROUND_DISPATCH_NOTE}}` placeholder (`scripts/resolvers/constants.ts`) instead of hand-writing the guidance, and add the generated carrier file to `GENERATED_WITH_GUIDANCE` in `test/run-in-background-guidance.test.ts` in the same commit — its structural scanner fails CI on any generated dispatch imperative that lacks the flag. +- **Never delete or link over a skill entry `setup` cannot prove is gstack's.** Every destructive site in `setup` (the linker, the alias installer, both prefix-flip cleanups) and in `bin/gstack-relink` goes through the ownership helpers (`_claude_entry_is_ours` / `_claude_entry_owned_strongly` in `setup`, `_entry_is_ours` / `_entry_owned_strongly` in relink). A symlink into gstack or the `.gstack-owned` marker proves the whole directory; a byte-identical or generated-banner SKILL.md proves only that file, and a differing one is moved to `~/.gstack/backups/skills//` first. `test/setup-link-ownership.test.ts`, `test/setup-cleanup-orphans.test.ts`, and `test/relink.test.ts` pin it. The rule is duplicated in the two scripts until the shared helper filed in TODOS.md lands: change both. +- **`./setup` never fails on Chromium.** The Playwright bootstrap (section `# 2` of `setup`) is best-effort and bounded: every failure becomes a reason code (`skipped`, `chromium-install`, `chromium-install-timeout`, `chromium-install-locked`, `windows-no-node`, `windows-node-modules`, `post-install-launch`) printed in the final summary alongside the browser-dependent skills, and skill registration always runs. `GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT=` (default 600) bounds the download; `GSTACK_SKIP_PLAYWRIGHT=1` skips it, the right knob for a no-browser box or a setup-only test loop. Anything you add after the bootstrap must stay independent of the browser. `test/setup-playwright-best-effort.test.ts` pins the block. ## Testing your changes in a real project @@ -513,7 +515,9 @@ cd .claude/skills/gstack && ./setup --no-prefix # switch to /qa, /ship cd .claude/skills/gstack && ./setup --prefix # switch to /gstack-qa, /gstack-ship ``` -Setup cleans up the old symlinks automatically. No manual cleanup needed. +Setup cleans up the old symlinks automatically. No manual cleanup needed. Only +entries gstack created are removed: a skill of your own that shares a name (a +hand-written `qa/`, say) is left in place and named in setup's final summary. ### Alternative: point your global install at a branch diff --git a/README.md b/README.md index 898d4b4d4..c894bae14 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,11 @@ while IFS= read -r dir; do ;; esac done +# Directories gstack created carry a .gstack-owned marker (the only signal on +# Windows, where installs are file copies with no symlink to read) +for marker in ~/.claude/skills/*/.gstack-owned; do + [ -f "$marker" ] && rm -rf "$(dirname "$marker")" +done # Alias skills install as copies (no symlink to detect) — remove by name rm -rf ~/.claude/skills/_gstack-command ~/.claude/skills/connect-chrome 2>/dev/null @@ -530,6 +535,29 @@ Data is stored in [Supabase](https://supabase.com) (open source Firebase alterna On Windows without Developer Mode (MSYS2 / Git Bash), `setup` falls back to file copies instead of symlinks because `ln -snf` produces frozen copies that don't refresh on `git pull`. **Re-run `cd ~/.claude/skills/gstack && ./setup` after every `git pull`** so your skill files match the repo. `setup` prints a one-line note reminding you. Unix and WSL keep symlinks and don't need the re-run. +**Chromium install failed or hung during `./setup`?** The browser is best-effort: +setup records the reason, finishes registering every skill, and prints which +skills need Chromium (`/qa`, `/qa-only`, `/design-review`, `/browse`, make-pdf, +`/pair-agent`). Fix the cause and re-run `./setup`. Knobs: +`GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT=` raises the download bound +(default 600) on slow links; `GSTACK_SKIP_PLAYWRIGHT=1` skips the Chromium +install entirely (CI, no-browser boxes); `GSTACK_CHROMIUM_NO_SANDBOX=1` is the +fix when Chromium installs but cannot launch because the host blocks +unprivileged user namespaces (Ubuntu 24.04+ AppArmor default, #2157). + +**Setup ended with "Not registered (a skill you own already uses the name; left untouched)"?** +gstack only deletes or links over a skill entry it can prove it created: a +symlink into gstack, a directory carrying the `.gstack-owned` marker `./setup` +writes into every directory it creates, or a SKILL.md that is byte-identical to +gstack's or carries the generated `` banner. A +`qa/` or `ship/` you wrote yourself is left untouched by `./setup`, +`gstack-relink`, and both prefix-mode flips, and the linker names it in the +final summary. Rename or move yours, or switch modes (`./setup --prefix` / +`--no-prefix`) so the names stop colliding. If you started your own skill from +a generated gstack SKILL.md and then edited it, that file is moved to +`~/.gstack/backups/skills///SKILL.md` before gstack's is +linked in, never deleted. + **Claude says it can't see the skills?** Make sure your project's `CLAUDE.md` has a gstack section. Add this: ``` diff --git a/TODOS.md b/TODOS.md index 13c84ff67..c3fbb1478 100644 --- a/TODOS.md +++ b/TODOS.md @@ -2,6 +2,55 @@ ## NEXT PRIORITY +### P2: fork-port residual wave deferrals (filed at Wave A, 2026-09-03) + +Filed from the time-attack/gstack residual evaluation +(docs/designs/fork-port-residual-2026-09/REPORT.md) and its CEO + eng reviews. +Waves B–E2 of that plan are scheduled work, not TODOs; these are the items the +reviews deliberately deferred, each with rationale: + +- **Shared `_gstack_owned_link` helper** — the ownership gate now exists in + six places (setup's `_claude_entry_is_ours` / `_claude_entry_owned_strongly` + used by link_claude_skill_dirs and _install_alias_skill_md, while + cleanup_old_claude_symlinks and cleanup_prefixed_claude_symlinks inline their + own marker/cmp/banner chain and readlink `case`; bin/gstack-relink + `_entry_is_ours`; bin/gstack-uninstall's per-entry loop). Extract one sourced + helper so the destructive-path guard cannot drift, and while there: make the + `.gstack-owned` marker's recorded install path load-bearing (today any marker + counts, so a Windows fork copy carrying gstack's generated header is still + treated as ours on a mode flip). Effort S. Priority P2. Depends on: none. +- **Config-key reader tripwire** — `transcript_ingest_mode=off` sat unread for + months while setup-gbrain advertised it. A free test that asserts every key + in bin/gstack-config's default table is read by at least one binary (or is + explicitly listed as prose-only) makes a dead consent switch a red test. + Effort S. Priority P2. Depends on: Wave E1 landing the reader. +- **"Pre-existing" failure vocabulary** — scripts/resolvers/preamble/ + generate-test-failure-triage.ts classifies from `git diff --name-only` and + never asks for a base-branch run. Rewrite T1 to verified/unverified with the + base branch's CI status (`gh run list --branch `) as default evidence + and a failing-files-only worktree run as an opt-in. Effort M → S with CC. + Priority P2. Depends on: none. +- **Opt-in `reply_language` config key** (#679) — render into the Writing + Style section only when set; keep identifiers and commands in English; add + the mixed-language tests the issue asked for. Not an always-on voice line + (community-PR guardrail). Effort S. Priority P3. +- **Remove the `~/.gstack/.auth.json` writer** — browser-manager.ts:638-640 + says the component-baked GBrowser extension reads it. Confirm GBrowser + bootstraps via `POST /extension-token`; if so, delete the writer plus a + migration that removes the orphaned credential file. Effort S. Priority P3. + Depends on: GBrowser source check. +- **CONTRIBUTING rule for fork-derived changes** — a change lifted from a fork + enters upstream only behind a test verified red on upstream HEAD first, with + credit to the original author; cherry-picks allowed when the fork commit + carries that test. 34 of 48 top fork candidates died under refutation; the + rule is what made the survivors safe. Effort S. Priority P2. +- **Hook slug-derivation parity audit** — question-preference-hook keyed + project prefs by cwd basename while the writer keyed by owner-repo (Wave E1 + fixes it via `slugFromCacheOnly`). Audit question-log-hook and every other + Claude hook that buckets by project for the same mismatch. Effort S. + Priority P3. Depends on: Wave E1. + + ### P1: ZeroEntropy sunset — gbrain's default embedding provider dies Sept 4, 2026 (#2365) **What:** ZeroEntropy (acquired by Notion) shuts down September 4, 2026. gbrain's @@ -196,10 +245,11 @@ The wave's Step-7 coverage audit (5 subsystem agents, ~700 changed paths, shipped verified by hand or adjacent tests); each is a cheap pin against silent regression: -- **setup Playwright bootstrap block** — `_clear_playwright_quarantine`, - `_PW_LOCK` stale-holder reclaim, `_kill_tree`/`_wait_with_deadline`, Ubuntu - 26.04 platform override: zero test references. The P0 #2554 heal's shell - half. Effort S each. +- **setup Playwright bootstrap block** — `_PW_LOCK` stale-holder reclaim, + `_kill_tree`/`_wait_with_deadline`, and the platform override are now pinned + by test/setup-playwright-best-effort.test.ts (fork-port Wave A). Still + unpinned: `_clear_playwright_quarantine` (the P0 #2554 heal's shell half). + Effort S. - **redact-prepush `scanAddedLines` slicing** — the >1MiB catch-up-diff chunk path (the reason the function exists) is unexercised; a regression reintroduces blocking-while-unscanned. Effort S. @@ -3944,10 +3994,15 @@ globs (D). What remains, re-filed individually: darwin-skipped handoff tests in browse/test/handoff.test.ts — verify whether the v1.67 XProtect + rebrand work un-blocks them, then un-skip or fix. Effort S. -- Transcript trust/scope/source isolation (PR 2232, issue 2140) — needs the - never-double-store review. Effort M. +- Transcript trust/scope/source isolation (PR 2232, issue 2140) — split: + the `transcript_ingest_mode` reader (off skips, B → --all-history, unset + unchanged) ships in fork-port Wave E1; repo-scoping and `--source-id` + isolation still need the never-double-store review plus a gbrain flag + probe. Close the PR after E1 with a pointer here. Effort M. - Versionless-repo onboarding (#1474, issues 2343/2334) — the #2501 JSON version-path half landed; the no-version-file-at-all flow did not. - Playwright bootstrap abort/timeout absorbs (PRs 2233/2359, issues - 1902/2136) — partially superseded by v1.67's bounded bootstrap; verify - and close or extract the remainder. + 1902/2136) — DONE in fork-port Wave A: the install is best-effort and + bounded (GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT, default 600s), lock contention + is a reason code, skills always register. Close #2233, #1900, #1901, #1902, + #913 with the receipt (test/setup-playwright-best-effort.test.ts). diff --git a/VERSION b/VERSION index a1b96f74e..fcf906d89 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.79.0.0 +1.80.0.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index fd164655c..cd5e47ecd 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.79.0.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.80.0.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/bin/gstack-relink b/bin/gstack-relink index 7a3b4b0e6..ecc609cae 100755 --- a/bin/gstack-relink +++ b/bin/gstack-relink @@ -42,20 +42,232 @@ PREFIX=$("$GSTACK_CONFIG" get skill_prefix 2>/dev/null || echo "false") # every skill back to the canonical (blockless) source. RENDER_DIR="${GSTACK_USER_RENDER_DIR:-${GSTACK_HOME:-$HOME/.gstack}/render/claude}" -# Helper: remove old skill entry (symlink or real directory with symlinked SKILL.md) +# ─── Ownership gate ─────────────────────────────────────────────────────────── +# relink runs on every ./setup and used to `rm -rf` any same-name entry with a +# symlinked SKILL.md and `ln -snf` over any existing SKILL.md — so a user's own +# skill that happened to share a name (a personal `qa`, a fork under another +# path) was deleted or had its SKILL.md replaced by a symlink into gstack +# (#2119; Linux replaces a real file with `ln -snf`, macOS refuses by accident). +# setup (link_claude_skill_dirs, cleanup_old_claude_symlinks, +# cleanup_prefixed_claude_symlinks) applies the same rule; keep the two files +# in sync until the shared helper filed in TODOS.md lands. gstack-uninstall +# has its own stricter inventory+banner gate. +# +# Proof comes in two strengths. STRONG (a symlink resolving into gstack, or the +# .gstack-owned marker) means we created the entry: it may be deleted whole or +# refreshed in place. WEAK (byte-identity with our source, or gen-skill-docs' +# two-line banner on a real file) proves only that the SKILL.md came from us: +# it authorizes touching that one file, never deleting the directory, and a +# differing file is moved to ${GSTACK_HOME:-~/.gstack}/backups/skills// +# before we link over it (a user who started their own skill from a gstack +# SKILL.md looks exactly like a pre-marker legacy copy). +# +# An entry is OURS when: +# - it is a symlink resolving into $INSTALL_DIR or $RENDER_DIR (as written or +# as realpath), or into any path with a `gstack` segment — the convention +# setup and gstack-uninstall use, so a sibling worktree's entries and a +# moved checkout's dangling links still count as ours, or +# - it is a real dir whose SKILL.md is such a symlink, or +# - it is a real dir with a real-file SKILL.md proven by the .gstack-owned +# marker (Windows copy installs), byte-identity with our source, or +# gen-skill-docs' generated header (legacy copies made before the marker). +# Anything else — a foreign symlink, a real dir with a hand-written SKILL.md, +# or an entry whose readlink fails — is FOREIGN: never deleted, never linked +# over, reported on stderr. +# The install/render roots as written AND as resolved: a standalone relink +# detects INSTALL_DIR as ~/.claude/skills/gstack, which may itself be a symlink +# to a checkout, while setup linked entries against the checkout's real path. +# Both spellings are ours. (Roots stay quoted inside the case patterns, so a +# glob character or space in a path is matched literally.) +_INSTALL_REAL="$(cd "$INSTALL_DIR" 2>/dev/null && pwd -P || printf '%s' "$INSTALL_DIR")" +_RENDER_REAL="$(cd "$RENDER_DIR" 2>/dev/null && pwd -P || printf '%s' "$RENDER_DIR")" +_target_is_ours() { + # $1 = an ABSOLUTE path a symlink resolves to; ours when it lives under one + # of our roots. "$ROOT"/* requires the separator, so /home/u/gstack2/x never + # matches a /home/u/gstack root. + local root + case "$1" in + "$INSTALL_DIR"/*|"$RENDER_DIR"/*|"$_INSTALL_REAL"/*|"$_RENDER_REAL"/*) return 0 ;; + gstack/*|*/gstack/*|*/.gstack/render/claude/*) return 0 ;; + esac + # A checkout named without a `gstack` segment (git worktree add + # ../gstack-): the target's skill root is a gstack tree if it + # carries setup + VERSION + bin/gstack-relink (a hand-written skill repo with + # a VERSION file does not). Same rule as setup's _gstack_target_is_ours. + root="${1%/*/SKILL.md}" + if [ "$root" != "$1" ] && [ -f "$root/VERSION" ] && [ -f "$root/setup" ] && [ -f "$root/bin/gstack-relink" ]; then return 0; fi + return 1 +} + +# readlink of a RELATIVE symlink (older installs wrote `gstack/qa/SKILL.md`) +# is relative to the link's own directory, not to $PWD. Anchor it there, then +# canonicalize the DIRECTORY part (pwd -P) so `..` segments and symlinked +# path components (a `gstack` alias dir, a symlinked install) compare against +# the real roots. The basename is kept verbatim: canonicalizing it would follow +# the final link and turn every dangling target into "not ours". +_link_target_abs() { + local link="$1" dest d b d_real + dest="$(readlink "$link" 2>/dev/null || true)" + [ -n "$dest" ] || return 1 + case "$dest" in + /*) ;; + *) dest="${link%/*}/$dest" ;; + esac + d="${dest%/*}"; b="${dest##*/}" + if d_real="$(cd "$d" 2>/dev/null && pwd -P)"; then + printf '%s\n' "$d_real/$b" + else + printf '%s\n' "$dest" + fi +} + +# _entry_owned_strongly ENTRY — we created this entry: a symlink resolving +# into gstack, or a real dir carrying the .gstack-owned marker or a SKILL.md +# symlink into gstack. +_entry_owned_strongly() { + local entry="$1" dest + if [ -L "$entry" ]; then + dest="$(_link_target_abs "$entry")" || return 1 + _target_is_ours "$dest" + return $? + fi + [ -d "$entry" ] || return 1 + [ -f "$entry/.gstack-owned" ] && return 0 + if [ -L "$entry/SKILL.md" ]; then + dest="$(_link_target_abs "$entry/SKILL.md")" || return 1 + _target_is_ours "$dest" + return $? + fi + return 1 +} + +# _entry_is_ours ENTRY SKILL — strong proof, or WEAK proof on a real-file +# SKILL.md (SKILL names the gstack skill this entry would serve, so the copy +# can be compared against our own source). +_entry_is_ours() { + local entry="$1" skill="${2:-}" src + _entry_owned_strongly "$entry" && return 0 + # A symlink that did not resolve into gstack is someone else's; never follow + # it into the "unclaimed directory" rule below. + [ -L "$entry" ] && return 1 + if [ -d "$entry" ] && [ ! -L "$entry/SKILL.md" ]; then + # No SKILL.md at all: an UNCLAIMED directory (a weak cleanup left the + # user's other files behind, or the dir was never a skill). Adding our + # SKILL.md overwrites nothing, so the link pass may proceed; the cleanup + # pass has nothing of ours to remove (see _cleanup_skill_entry). + [ -e "$entry/SKILL.md" ] || return 0 + if [ -f "$entry/SKILL.md" ]; then + for src in "$RENDER_DIR/$skill/SKILL.md" "$INSTALL_DIR/$skill/SKILL.md"; do + [ -n "$skill" ] && [ -f "$src" ] && cmp -s "$entry/SKILL.md" "$src" && return 0 + done + # Pre-marker legacy copy: gen-skill-docs' full two-line banner near the + # top (same rule as setup's _gstack_generated_header), not a one-line + # substring another generator could emit. A gstack fork rendering the + # same banner is the accepted, filed residual. + case "$(head -c 8192 "$entry/SKILL.md" 2>/dev/null)" in + *''*) return 0 ;; + esac + fi + return 1 + fi + return 1 +} + +FOREIGN_SKIPPED=() +_report_foreign() { + # Same wording and bare name as setup's own line, so setup can dedupe when it + # forwards relink's output. + echo " skipped ${1##*/}: existing entry is not gstack-managed (foreign skill with the same name) — left untouched" >&2 + FOREIGN_SKIPPED+=("${1##*/}") +} + +# Weakly-proven real files we would otherwise overwrite go here, one summary +# line at the end. mv, not cp: the link that follows needs the path free. +BACKUP_ROOT="${GSTACK_HOME:-$HOME/.gstack}/backups/skills/$(date +%Y%m%dT%H%M%S)" +BACKED_UP=() +_backup_skill_md() { + # Non-zero when the file could NOT be moved: the caller leaves the entry alone. + local file="$1" name="$2" + mkdir -p "$BACKUP_ROOT/$name" 2>/dev/null || return 1 + mv -f "$file" "$BACKUP_ROOT/$name/SKILL.md" 2>/dev/null || return 1 + BACKED_UP+=("$name") + return 0 +} + +# Helper: remove an OLD skill entry from the opposite prefix mode. Only entries +# we can prove are ours are removed; anything else is reported and kept. _cleanup_skill_entry() { - local entry="$1" + local entry="$1" skill="${2:-}" e dest src + [ -e "$entry" ] || [ -L "$entry" ] || return 0 + # Unclaimed dir (no SKILL.md, no marker): nothing of ours to clean. + if [ -d "$entry" ] && [ ! -L "$entry" ] && [ ! -e "$entry/SKILL.md" ] && [ ! -L "$entry/SKILL.md" ] && [ ! -f "$entry/.gstack-owned" ]; then + return 0 + fi + if ! _entry_is_ours "$entry" "$skill"; then + _report_foreign "$entry" + return 0 + fi if [ -L "$entry" ]; then rm -f "$entry" - elif [ -d "$entry" ] && [ -L "$entry/SKILL.md" ]; then - rm -rf "$entry" + elif [ -d "$entry" ]; then + # Whole-directory removal needs proof that nothing of the user's is inside: + # the marker (we created the dir) or a directory holding nothing but links. + if [ -f "$entry/.gstack-owned" ] || { [ -L "$entry/SKILL.md" ] && _dir_only_links "$entry"; }; then + rm -rf "$entry" + else + # Otherwise only what is ours goes: the SKILL.md, the marker, and our + # runtime-asset links. The user's files stay, and so does the directory + # if it is not empty afterwards. A real SKILL.md that differs from our + # source (raw, or with its name: rewritten to the entry name) is a + # customized file: moved to the backup root, never deleted. + if [ -f "$entry/SKILL.md" ] && [ ! -L "$entry/SKILL.md" ] && [ -n "$skill" ]; then + src="$INSTALL_DIR/$skill/SKILL.md"; [ -f "$RENDER_DIR/$skill/SKILL.md" ] && src="$RENDER_DIR/$skill/SKILL.md" + if [ -f "$src" ] && ! cmp -s "$entry/SKILL.md" "$src" \ + && ! sed "1,/^---\$/ s/^name:[[:space:]].*/name: ${entry##*/}/" "$src" | cmp -s - "$entry/SKILL.md"; then + if ! _backup_skill_md "$entry/SKILL.md" "${entry##*/}"; then + echo " kept ${entry##*/}/SKILL.md: could not back up the customized file — left untouched" >&2 + return 0 + fi + fi + fi + rm -f "$entry/SKILL.md" "$entry/.gstack-owned" + for e in "$entry"/* "$entry"/.[!.]* "$entry"/..?*; do + [ -L "$e" ] || continue + dest="$(_link_target_abs "$e")" || continue + if _target_is_ours "$dest"; then rm -f "$e"; fi + done + rmdir "$entry" 2>/dev/null || echo " cleaned ${entry##*/}/SKILL.md (other files in that directory were left in place)" + fi fi } +# _dir_only_links DIR — deleting DIR whole loses no real data: every entry is +# a symlink or our marker. +_dir_only_links() { + # Every entry must be a symlink resolving into gstack, or our marker: a + # user's own link (notes.md -> ~/notes) makes the directory mixed. + local d="$1" e dest + for e in "$d"/* "$d"/.[!.]* "$d"/..?*; do + { [ -e "$e" ] || [ -L "$e" ]; } || continue + [ "${e##*/}" = ".gstack-owned" ] && continue + [ -L "$e" ] || return 1 + dest="$(_link_target_abs "$e")" || return 1 + _target_is_ours "$dest" || return 1 + done + return 0 +} _link_root_skill_alias() { local target="$SKILLS_DIR/_gstack-command" [ -f "$INSTALL_DIR/SKILL.md" ] || return 0 + # Same ownership gate as every other entry (#2119): a user's own + # `_gstack-command` skill is reported and left alone, never overwritten. + if { [ -e "$target" ] || [ -L "$target" ]; } && ! _entry_is_ours "$target" ""; then + _report_foreign "$target" + return 0 + fi + local pre=0 + if [ -e "$target" ] || [ -L "$target" ]; then pre=1; fi [ -L "$target" ] && rm -f "$target" mkdir -p "$target" # Copy-then-rewrite, never a symlink (#2511): a symlinked alias re-serves @@ -65,6 +277,12 @@ _link_root_skill_alias() { # write through it into the generated source. rm -f "$target/SKILL.md" sed "1,/^---\$/ s/^name:[[:space:]].*/name: _gstack-command/" "$INSTALL_DIR/SKILL.md" > "$target/SKILL.md" + # The rewritten copy is a real file on every platform: the marker, not the + # banner, is what proves it ours on the next run — written only for a + # directory we created (or already marked), never one we merely wrote into. + if [ "$pre" -eq 0 ] || [ -f "$target/.gstack-owned" ]; then + printf '%s\n' "$_INSTALL_REAL" > "$target/.gstack-owned" 2>/dev/null || true + fi } _link_root_skill_alias @@ -90,23 +308,53 @@ for skill_dir in "$INSTALL_DIR"/*/; do *) link_name="gstack-$skill" ;; esac # Remove old flat entry if it exists (and isn't the same as the new link) - [ "$link_name" != "$skill" ] && _cleanup_skill_entry "$SKILLS_DIR/$skill" + [ "$link_name" != "$skill" ] && _cleanup_skill_entry "$SKILLS_DIR/$skill" "$skill" else link_name="$skill" # Don't remove gstack-* dirs that are their real name (e.g., gstack-upgrade) case "$skill" in gstack-*) ;; # Already the real name, no old prefixed link to clean - *) _cleanup_skill_entry "$SKILLS_DIR/gstack-$skill" ;; + *) _cleanup_skill_entry "$SKILLS_DIR/gstack-$skill" "$skill" ;; esac fi target="$SKILLS_DIR/$link_name" + # A destination that already exists and is NOT ours is a foreign skill that + # shares our name. Never `ln -snf` over its SKILL.md (on Linux that replaces + # a real file with a symlink into gstack) and never mkdir into it — skip + # loudly and leave registration of that one name to the user. + if { [ -e "$target" ] || [ -L "$target" ]; } && ! _entry_is_ours "$target" "$skill"; then + _report_foreign "$target" + continue + fi + # Remember whether WE are creating this directory: only then may the marker + # below make it deletable whole. A directory we merely link into (unclaimed, + # or a legacy install) never gets one — legacy all-links dirs are removed by + # the only-links rule instead. + _pre_exists=0 + if [ -e "$target" ] || [ -L "$target" ]; then _pre_exists=1; fi # Upgrade old directory symlinks to real directories [ -L "$target" ] && rm -f "$target" # Create real directory with symlinked SKILL.md (absolute path) mkdir -p "$target" skill_md_src="$INSTALL_DIR/$skill/SKILL.md" [ -f "$RENDER_DIR/$skill/SKILL.md" ] && skill_md_src="$RENDER_DIR/$skill/SKILL.md" + # A real-file SKILL.md we can only WEAKLY prove ours and whose content + # differs from what we are about to serve is moved aside, not overwritten. + if [ -f "$target/SKILL.md" ] && [ ! -L "$target/SKILL.md" ] && ! _entry_owned_strongly "$target" \ + && ! cmp -s "$target/SKILL.md" "$skill_md_src"; then + if ! _backup_skill_md "$target/SKILL.md" "$link_name"; then + echo " skipped $link_name: could not back up its customized SKILL.md — left untouched" >&2 + FOREIGN_SKIPPED+=("$link_name") + continue + fi + fi ln -snf "$skill_md_src" "$target/SKILL.md" + # Provenance marker on every platform (path-independent proof; on Windows + # without Developer Mode `ln -snf` degrades to a copy and this is the only + # proof), but only for a directory we created or already owned. + if [ "$_pre_exists" -eq 0 ] || [ -f "$target/.gstack-owned" ]; then + printf '%s\n' "$_INSTALL_REAL" > "$target/.gstack-owned" 2>/dev/null || true + fi SKILL_COUNT=$((SKILL_COUNT + 1)) done @@ -124,3 +372,9 @@ if [ "$PREFIX" = "true" ]; then else echo "Relinked $SKILL_COUNT skills as flat names" fi +if [ ${#BACKED_UP[@]} -gt 0 ]; then + echo "Moved ${#BACKED_UP[@]} pre-existing SKILL.md file(s) to $BACKUP_ROOT before linking gstack's: ${BACKED_UP[*]}" +fi +if [ ${#FOREIGN_SKIPPED[@]} -gt 0 ]; then + echo "Skipped ${#FOREIGN_SKIPPED[@]} foreign entr$( [ ${#FOREIGN_SKIPPED[@]} -eq 1 ] && echo y || echo ies) (not gstack-managed, left untouched): ${FOREIGN_SKIPPED[*]}" +fi diff --git a/bin/gstack-telemetry-log b/bin/gstack-telemetry-log index 05d9c9866..7b95e5ea7 100755 --- a/bin/gstack-telemetry-log +++ b/bin/gstack-telemetry-log @@ -10,6 +10,11 @@ # gstack-telemetry-log --skill qa --duration 142 --outcome success \ # --used-browse true --session-id "12345-1710756600" # +# --no-sweep one-shot events with no session of their own (setup's +# onboarding events) must not finalize OTHER sessions' in-flight +# .pending markers as outcome:unknown — only a skill's own +# epilogue may sweep. +# # Env overrides (for testing): # GSTACK_STATE_DIR — override ~/.gstack state directory # GSTACK_DIR — override auto-detected gstack root @@ -48,9 +53,11 @@ SEC_PAYLOAD_HASH="" SEC_CONFIDENCE="" SEC_LAYER="" SEC_VERDICT="" +NO_SWEEP="" while [ $# -gt 0 ]; do case "$1" in + --no-sweep) NO_SWEEP=1; shift ;; --skill) SKILL="$2"; shift 2 ;; --duration) DURATION="$2"; shift 2 ;; --outcome) OUTCOME="$2"; shift 2 ;; @@ -93,6 +100,9 @@ fi # ─── Finalize stale .pending markers ──────────────────────── # Each session gets its own .pending-$SESSION_ID file to avoid races # between concurrent sessions. Finalize any that don't match our session. +# --no-sweep (one-shot events from setup) skips this: a marker that is not +# ours is another live session's, not a stale one. +if [ -z "$NO_SWEEP" ]; then for PFILE in "$PENDING_DIR"/.pending-*; do [ -f "$PFILE" ] || continue # Skip our own session's marker (it's still in-flight) @@ -117,6 +127,7 @@ for PFILE in "$PENDING_DIR"/.pending-*; do "$P_TS" "$P_SKILL" "$P_SID" "$P_VER" "$P_OS" "$P_ARCH" >> "$JSONL_FILE" 2>/dev/null || true fi done +fi # Clear our own session's pending marker (we're about to log the real event) [ -n "$SESSION_ID" ] && rm -f "$PENDING_DIR/.pending-$SESSION_ID" 2>/dev/null || true diff --git a/careful/bin/check-careful.sh b/careful/bin/check-careful.sh index b6bebb9d3..11993f110 100755 --- a/careful/bin/check-careful.sh +++ b/careful/bin/check-careful.sh @@ -265,7 +265,15 @@ fi # ERE per line; blank lines and #-comments skipped; an invalid regex is # skipped (never fatal — the hook must not break on a typo in config). if [ -z "$WARN" ]; then - _GSTACK_HOME_DIR="${GSTACK_HOME:-$HOME/.gstack}" + # Same state root the writer (/careful via gstack-paths) uses — see + # gstack_hook_state_root in hook-extract.sh (#1459 class). + if command -v gstack_hook_state_root >/dev/null 2>&1; then + _GSTACK_HOME_DIR="$(gstack_hook_state_root; printf x)"; _GSTACK_HOME_DIR="${_GSTACK_HOME_DIR%x}" + else + # Older hook-extract.sh (partial upgrade): the plain chain beats dying + # under set -e with no decision JSON — rules under $HOME/.gstack still load. + _GSTACK_HOME_DIR="${GSTACK_HOME:-$HOME/.gstack}" + fi _PATTERN_FILES="$_GSTACK_HOME_DIR/careful-patterns.txt" # Short-circuit: resolving the project slug costs a subprocess + git call on # EVERY Bash command while /careful is active — only pay it when some diff --git a/careful/bin/hook-extract.sh b/careful/bin/hook-extract.sh index 6ab8d611e..594b4d6c0 100644 --- a/careful/bin/hook-extract.sh +++ b/careful/bin/hook-extract.sh @@ -63,10 +63,39 @@ gstack_hook_decision() { printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s","permissionDecisionReason":%s}}\n' "$_ghd_decision" "$_ghd_encoded" } +# gstack_hook_state_root +# Print the gstack state root, resolved with EXACTLY the chain bin/gstack-paths +# uses (GSTACK_STATE_ROOT): GSTACK_HOME, then CLAUDE_PLUGIN_DATA only when +# CLAUDE_PLUGIN_ROOT names gstack (a CLAUDE_PLUGIN_DATA leaked from another +# plugin via CLAUDE_ENV_FILE must not redirect our state), then $HOME/.gstack, +# then a project-local .gstack. Hooks run on every Edit/Bash call, so this is +# pure bash — never spawn gstack-paths from a hook. The writers (/freeze, +# /guard, /unfreeze, /investigate) resolve through gstack-paths; a reader that +# used a different chain failed OPEN whenever GSTACK_HOME was set (#1459). +# test/hook-scripts.test.ts pins parity against gstack-paths. +# Printed WITHOUT a trailing newline: callers capture with a sentinel +# (`r="$(gstack_hook_state_root; printf x)"; r="${r%x}"`) so a root that +# itself ends in a newline round-trips exactly as gstack-paths' %q does — +# otherwise writer and reader would again disagree on the directory. +gstack_hook_state_root() { + if [ -n "${GSTACK_HOME:-}" ]; then + printf '%s' "$GSTACK_HOME" + elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ] && printf '%s' "${CLAUDE_PLUGIN_ROOT:-}" | grep -qi "gstack"; then + printf '%s' "$CLAUDE_PLUGIN_DATA" + elif [ -n "${HOME:-}" ]; then + printf '%s' "$HOME/.gstack" + else + printf '%s' ".gstack" + fi +} + # gstack_hook_log_fire SKILL PATTERN # Append a hook_fire analytics record (pattern name only, never command # content). Respects GSTACK_HOME so tests never pollute the operator's real -# analytics file. Best-effort: failures never affect the hook decision. +# analytics file. Deliberately NOT gstack_hook_state_root: every other +# analytics writer and reader (gstack-skill-start, gstack-retro-metrics, +# gstack-analytics) uses this two-step chain, and the usage log must stay one +# file. Best-effort: failures never affect the hook decision. gstack_hook_log_fire() { _ghlf_dir="${GSTACK_HOME:-$HOME/.gstack}/analytics" mkdir -p "$_ghlf_dir" 2>/dev/null || true diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 6d043e7dc..dd801ade4 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -36,6 +36,7 @@ gstack/ │ ├── fixtures/ # Ground truth JSON, planted-bug fixtures, eval baselines │ ├── skill-validation.test.ts # Tier 1: static validation (free, <1s) │ ├── gen-skill-docs.test.ts # Tier 1: generator quality (free, <1s) +│ ├── setup-*.test.ts, relink.test.ts, hook-scripts.test.ts # Tier 1: setup linker ownership + Chromium bootstrap (anchor-sliced from setup), gstack-relink, PreToolUse hooks (free) │ ├── skill-llm-eval.test.ts # Tier 3: LLM-as-judge (~$0.15/run) │ └── skill-e2e-*.test.ts # Tier 2: E2E via claude -p (~$3.85/run, split by category) ├── qa-only/ # /qa-only skill (report-only QA, no fixes) @@ -54,7 +55,11 @@ gstack/ ├── investigate/ # /investigate skill (systematic root-cause debugging) ├── spec/ # /spec skill (five-phase spec → GitHub issue, optional agent spawn, /ship auto-closes) ├── retro/ # Retrospective skill (includes /retro global cross-project mode) -├── bin/ # CLI utilities (gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, etc.) +├── careful/ # /careful skill; bin/check-careful.sh (PreToolUse destructive-command hook) + bin/hook-extract.sh (shared hook helpers: payload extraction, deny JSON, gstack_hook_state_root) +├── freeze/ # /freeze skill; bin/check-freeze.sh (PreToolUse edit-boundary hook; sources careful/bin/hook-extract.sh, fails closed) +├── guard/, unfreeze/ # /guard (careful + freeze in one), /unfreeze +├── gstack-upgrade/ # /gstack-upgrade skill + migrations/ (run after ./setup during an upgrade) +├── bin/ # CLI utilities (gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, etc.) ├── document-release/ # /document-release skill (post-ship doc updates + Diataxis coverage map) ├── document-generate/ # /document-generate skill (Diataxis doc generator: tutorial/how-to/reference/explanation) ├── cso/ # /cso skill (OWASP Top 10 + STRIDE security audit) @@ -70,14 +75,14 @@ gstack/ ├── extension/ # Chrome extension (side panel + activity feed + CSS inspector) ├── lib/ # Shared libraries (worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/) ├── patches/ # bun `patchedDependencies` patches (playwright-core windowsHide) -├── docs/designs/ # Design documents +├── docs/designs/ # Design documents (incl. fork-port-residual-2026-09/ evaluation evidence) ├── setup-deploy/ # /setup-deploy skill (one-time deploy config) ├── .github/ # CI workflows + shared composite actions (.github/actions/) + Docker image (claude CLI pinned) │ ├── workflows/ # evals.yml (E2E on Ubicloud), quality-gate.yml (secret scan), dependency-review.yml, osv-scanner.yml, skill-docs.yml, actionlint.yml, and 8 more (windows, periodic evals, release gates, ci-image) │ └── docker/ # Dockerfile.ci (pre-baked toolchain + Playwright/Chromium) ├── contrib/ # Contributor-only tools (never installed for users) │ └── add-host/ # /gstack-contrib-add-host skill -├── setup # One-time setup: build binary + symlink skills +├── setup # One-time setup: build binary + best-effort Chromium bootstrap + link skills (ownership-gated) ├── SKILL.md # Generated from SKILL.md.tmpl (don't edit directly) ├── SKILL.md.tmpl # Template: edit this, run gen:skill-docs ├── ETHOS.md # Builder philosophy (Boil the Ocean, Search Before Building) diff --git a/docs/TESTING_INTERNALS.md b/docs/TESTING_INTERNALS.md index dbab9d8b4..8b1497859 100644 --- a/docs/TESTING_INTERNALS.md +++ b/docs/TESTING_INTERNALS.md @@ -122,6 +122,26 @@ in the test trees must carry a `timeout`, enforced by `test/spawnsync-timeout-tripwire.test.ts` with a shrink-only exemption ratchet. +**Anchor-sliced `setup` harnesses.** `setup` is one large bash script, so the +free tests that pin its linker, cleanup, and Chromium-bootstrap behavior never +run the whole thing. They slice the source by anchor (`extractFn(name)` takes +`name() {` through the next `\n}\n`; `test/setup-playwright-best-effort.test.ts` +slices the `# 2. Ensure Playwright's Chromium is available` block up to +`# 2b.`), join the extracted functions with stubbed collaborators, and execute +the REAL bash under a temp `HOME` with stubbed probes and installers. Two rules +keep the harness honest: renaming a function or anchor comment in `setup` fails +the test with `function not found` / `anchor not found` instead of silently +testing nothing, and `test/setup-link-ownership.test.ts` and +`test/setup-playwright-best-effort.test.ts` throw on any `command not found` on +stderr as harness drift (a helper the test forgot to extract) rather than +letting it degrade into a pass. Files: `test/setup-link-ownership.test.ts`, +`test/setup-cleanup-orphans.test.ts`, `test/setup-playwright-best-effort.test.ts`. +`test/relink.test.ts` shells out to a copy of the real `bin/gstack-relink` +against a temp `GSTACK_INSTALL_DIR` / `GSTACK_SKILLS_DIR`, and +`test/hook-scripts.test.ts` runs the real `careful/bin/check-careful.sh` and +`freeze/bin/check-freeze.sh` with JSON payloads on stdin (including the +`GSTACK_HOME` state-root parity against `bin/gstack-paths`). + ## Cloud sandboxes (Vercel / Conductor cloud workspaces) Syscall-supervised sandboxes need environment setup before `bun run test` can diff --git a/docs/designs/fork-port-residual-2026-09/REPORT.md b/docs/designs/fork-port-residual-2026-09/REPORT.md new file mode 100644 index 000000000..b6086bb4d --- /dev/null +++ b/docs/designs/fork-port-residual-2026-09/REPORT.md @@ -0,0 +1,212 @@ +# Is there anything else worth taking in from time-attack/gstack? + +Evaluation of the time-attack/gstack fork (Sina Matian, MIT; fork main 0aca1f77, tip of the "GStack 2 public beta" release merged as PR #53 at 41927ca8, 2026-08-10; merge-base 7c9df1c5) against upstream garrytan/gstack main v1.79.0.0 (2026-09-01). Sources: /tmp/ta-eval/summary.json, residual-index.json (287 items, read in full), records-other.json (128 items), records-residual.json (per-item lookups), refute-batch-1..6.json (48 items) and the appended adversarial refuter verdicts. Upstream claims below were re-checked by grep at HEAD where cited as file:line. + +## 1. Bottom line + +After the seven directly merged fork PRs, the iOS suite, port waves 1 and 2 (v1.63, v1.65) and the security sweep (v1.67.1), the fork's 583 raw candidates collapse to 415 canonical items, of which 128 are already absorbed, superseded or not applicable and 287 carry some residual. The panel rated 269 of those 287 as take or take-partial, but when the 48 top-ranked candidates were put through adversarial refutation only 14 survived, and two of those are the same defect counted twice, so the honest answer is about a dozen genuine ports plus a cluster of ten small grep-verified bin/browse fixes, with a further 39 panel-P1 items that were never refuted and are listed in section 5a so nothing is silently misfiled. Almost none of it is "GStack 2": the surviving items are correctness bugs and consent gaps in upstream's own code that the fork happened to notice, and roughly a third of the shortlist is really "merge a stale community PR the fork carried" (#1509, #2119, #1900, #1777, #1096). The top five by user impact are: (1) ./setup aborting before any skill is registered when Playwright's Chromium download fails (setup:3 `set -e`, setup:806, setup:850 vs skills at setup:1657); (2) the /freeze deny hook reading a different state dir than /freeze writes, so the boundary fails open whenever GSTACK_HOME is set (freeze/bin/check-freeze.sh:35); (3) /ship Step 5/6 still hardcoding `bin/test-lane`, Rails and a Ruby eval runner (ship/sections/tests.md.tmpl:9-113); (4) the question-preference PreToolUse hook keying project preferences by cwd basename while the writer keys by owner-repo, so the "never ask again" enforcement layer never fires for any repo with a remote (hosts/claude/hooks/question-preference-hook.ts:300-306); (5) `transcript_ingest_mode=off` being a documented consent switch that no binary reads (bin/gstack-config:181 is the only occurrence). Safely ignorable: the entire GStack 2 architecture (six dispatchers, fifteen-vector scale system, SHARED-JUDGMENT and VERIFICATION contracts, zero-question budget), the web-data provider stack, PRIVACY.md, issue forms and labels, the multi-OS CI lane, the Node .cjs team hook, and the persona-fleet methodology already parked in TODOS.md. + +## 2. Ranked shortlist — worth taking + +Sorted by panel priority, then user_value_score + fit_score. Every item here either survived refutation (refuted=false) or is a cheap fix whose upstream premise I re-verified by grep at HEAD; the latter are flagged as such and clustered. + +Footnote on the survivor count: the 14 survivors reflect the final (appended) refutation pass. The residual-index.json `prior_refuter` field records an earlier pass with a different boundary on 7 of the 48: s1-prewave-transcript-ingest-mode-off and s1-prewave-terminal-ws-extension-id-pin were refuted=true in the earlier pass and refuted=false in the final one; s1-prewave-setup-install-deadline-and-continue, s1-prewave-readme-update-check-disclosure, s1-prewave-review-loosened-inputs-stale-strings, s6-branches-web-data-official-api-first-rule and s1-prewave-auth-json-file-write-removal had no earlier verdict (prior_refuter is null, not false) and are refuted=true in the final pass. This report follows the final pass throughout. + +**1. /freeze hook state-dir chain (gap2-skillwave-hooks-envelope-and-state-dir-chain-1509), P0, val 8 / fit 10.** The hookSpecificOutput envelope half of #1509 is absorbed, but freeze/bin/check-freeze.sh:35 still reads `STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"` while every writer (freeze/SKILL.md.tmpl:61-64, guard/SKILL.md.tmpl:70-73, unfreeze/SKILL.md.tmpl:31-35, investigate/SKILL.md.tmpl:131-134) resolves through bin/gstack-paths GSTACK_HOME-first. GSTACK_HOME is a documented production knob (bin/gstack-config:16, setup:2135) and the same hook's analytics helper already honours it (careful/bin/hook-extract.sh:71). Result: with GSTACK_HOME set, /freeze prints "boundary set" and the hook hits the no-file allow branch, contradicting its own header ("a boundary that fails open is not a boundary"). Fork refs: bbad1bab (check-freeze.sh hunk), c896016b (test pin); upstream PR #1509 and issue #1459 both OPEN. Port: one line, `STATE_DIR="${GSTACK_HOME:-${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}}"`, mirroring hook-extract.sh; do not port the fork's 12-line probe-and-eval of gstack-paths inside a hook that fires on every Edit. Update test/hook-scripts.test.ts:690-893 to pass `{GSTACK_HOME, CLAUDE_PLUGIN_DATA}` and add the missing regression (GSTACK_HOME dir with freeze-dir.txt, HOME elsewhere, must deny). Credit @NikhileshNanduri, close #1459/#1509. Effort: human 2-3 hours, CC+gstack 15 min. Refuter's strongest counter: only the fork's shape (subprocess in a hot hook) is wrong; the defect itself could not be refuted. + +**2. /ship hardcoded test lanes (s9-skills-ship-hardcoded-test-lanes), P1, val 8 / fit 10.** ship/sections/tests.md.tmpl:9-22 is still a literal bash block that runs `bin/test-lane 2>&1` and `npm run test 2>&1` and warns about `RAILS_ENV=test bin/rails db:migrate`; Step 6 (:44-113) matches `app/services/*_prompt_builder.rb`, runs `EVAL_JUDGE_TIER=full bin/test-lane --eval`, and ships a Haiku/Sonnet/Opus price table; ship/SKILL.md.tmpl:381 hardcodes the `tests`/`vitest` label pair for the evidence check. This is a direct violation of upstream CLAUDE.md "Platform-agnostic design" and survived both port waves because v1.66.1 wrapped the hardcode in evidence labels instead of removing it. Fork refs: 8bc0a04f, d72133e1 (0aca1f77:skills/ship/references/sections/ship/tests.md); upstream issue #1069 and PR #1102 (@JiayuuWang) OPEN. Nothing from the fork transfers (its version is a 2.0 legacy overlay); this is upstream's own deletion-heavy rewrite: Step 5 consumes the command Step 4 (`{{TEST_BOOTSTRAP}}`) resolved from CLAUDE.md or asked and persisted, one evidence label per resolved command; Step 6 conditional on a CLAUDE.md-declared eval command; Step 16 evidence check becomes repeated `--label/--expect-cmd` pairs; same fix at land-and-deploy/sections/readiness-gate.md.tmpl:111-116. Regenerate the three ship goldens; extend test/ship-test-detection-markers.test.ts to reject `bin/test-lane|RAILS_ENV|_prompt_builder|EVAL_JUDGE_TIER` in rendered output. Effort: human ~2 days, CC+gstack 1-1.5 hours. Refuter's counter: TEST_BOOTSTRAP already says "use CLAUDE.md's command"; does not hold because the literal block still runs afterwards regardless. + +**3. Playwright Chromium failure becomes a named warning, not a mid-setup abort (gap3-setup-playwright-best-effort-not-fatal), P1, val 9 / fit 9.** setup:3 `set -e` plus the bare `bunx playwright install chromium` at setup:806 (and the explicit `exit 1` at :850) sit under "# 2." (setup:764), ahead of "# 4. Install for Claude" (:1657), migrations (:1943) and hooks (:1990). An offline, proxied, AppArmor-restricted or Bun-on-Windows box ends with zero skills and a re-run that dies at the same line, even though every later step is already best-effort (emoji font :853-864 is warn-only). Playwright already retries the download five times internally, so what reaches this path is real unavailability, exactly when the 35+ non-browser skills matter most. Issues #1900, #1901, #1902, #913, PR #2233 all OPEN on this behaviour. Fork ref: ce9f0f9a (David Miserak; reachable only from origin/backup/pre-isolated-wave-review-2026-07-14). Port: re-implement on upstream's block, not cherry-pick: wrap the install in `if ! ( ... )` so set -e cannot abort, a `_PW_FAIL_REASON` accumulator (chromium-install | windows-no-node | windows-node-modules | post-install-launch), keep `_PW_LOCK` contention fatal (setup:815-819), keep `_clear_playwright_quarantine` and the EXIT-trap chaining, loud stderr naming the unavailable skills (/qa, /design-review, /browse, make-pdf, /pair-agent) repeated in the final summary, optionally honour GSTACK_SKIP_PLAYWRIGHT=1 (#913). Add the static test the TODOS.md:199-202 entry asks for. Credit @DavidMiserak, close #1900/#1901/#1902/#2233. Effort: human 2-3 hours, CC+gstack 15-20 min plus one free-suite run. Refuter's counter: the fork's message text is false in upstream ordering and its block-wide "no exit 1" test would flag the lock exit; both are port corrections, not reasons to keep the abort. + +**4. gstack-relink ownership guard (gap3-relink-ownership-guard-on-prefix-flip), P1 (refuter says P2), val 8 / fit 9.** bin/gstack-relink:46-53 `_cleanup_skill_entry` does `rm -f` on any symlink and `rm -rf` on any dir with a symlinked SKILL.md with no readlink check, is called on every flat name in prefix mode (:93) and every gstack-* name in flat mode (:99), and setup runs relink as a self-healing step on every install (setup:1679-1685). Upstream already adopted the opposite policy at every sibling site (setup:1040-1057 readlink gate, bin/gstack-uninstall:204-230), so this is the one remaining unguarded deleter of a class fixed three times. Fork refs: 5ed18036 (smblight, upstream PR #2119 MERGEABLE), 4d5f0309. Port: re-implement, gating on the resolved target being inside `$INSTALL_DIR` or `$RENDER_DIR` (relink:43, the fork's `*/gstack/*` pattern misses the render dir and lexically matches non-owned paths); port smblight's two regression tests plus a RENDER_DIR case; decide the flat-mode collision policy at relink:104 (loud skip rather than silent overwrite). Credit @smblight. Effort: human ~2 hours, CC+gstack 15-20 min. Refuter's counter: blast radius is narrower than the panel implied (only prefix-mode users with foreign symlinked skills sharing a name, and only the registration link is lost); accepted, hence P2, but the bug stands. + +**5. Merge two stale community PRs the fork carried: taste direction-aware confidence (s8-codediff-taste-direction-aware-confidence, val 6 / fit 10) and setup-deploy Render key bytes (s1-prewave-setup-deploy-render-key-bytes, val 6 / fit 10), both P1 (refuter: P2/P3 housekeeping).** bin/gstack-taste-update:199-201 computes `confidence = approved_count/(total+1)` for both buckets, so a rejected preference is pinned at 0 forever; cmdShow's rejected sort at :243 is a no-op and the drift warning at :204 is unreachable; upstream's own test hand-seeds confidence 0.8 to dodge it (test/taste-engine.test.ts:269-270). Fork 944dcab2 is a strict subset of upstream PR #1777 (@jbetala7, MERGEABLE). setup-deploy/SKILL.md.tmpl:104 still says `echo $RENDER_API_KEY | head -c 4`; fork fc8dded2 is byte-identical to upstream PR #1096 (@JiayuuWang, MERGEABLE). Port: merge #1777 (rebase, line moved 196 to 201) and #1096 as-is, regen SKILL.md, close #1776/#1078. Skip the fork's executable snippet test and tree-wide regex (false positives on test/evidence.test.ts:117). Effort: human 1 hour total, CC+gstack 15 min. Refuter's counter: nothing here is from the fork and severity is overstated (Render keys carry a fixed prefix, unverified); true, but both fixes are correct and free. + +**6. make-pdf preview fires remote image GETs despite the offline promise (s8-codediff-make-pdf-preview-offline-gate), P1, val 6 / fit 10.** make-pdf/src/orchestrator.ts:361-362 writes `rendered.html` verbatim to the preview path; `opts.allowNetwork` is read only on the generate path (:170); make-pdf/src/cli.ts:197 parses `--allow-network` into PreviewOptions where it is dead; render.ts:256-258 deliberately leaves remote `` for the inliner that preview skips by design (:331-345). So `$P preview` beacons once per hotlinked image with no flag passed, and shows images the PDF will not contain. Fork ref: 8fae162d (~30 lines, reuses IMG_TAG_RE/SRC_RE and buildBlockedRemotePlaceholder in diagram-prepass.ts, unit test included). Port: cherry-pick, keep upstream's `browseClient.PAYLOAD_TMP_DIR` path, export blockRemoteImages, gate in preview() unless `allowNetwork === true`, add an orchestrator-level assertion. No egress receipt needed: it removes a fetch. Effort: human 2 hours, CC+gstack 10 min. Refuter's counter: "the user's browser fetching the user's own images is not gstack egress" is answered by the documented promise and the dead flag. + +**7. browse click on a missing selector waits twice (s8-codediff-browse-click-missing-selector-no-second-wait), P1, val 7 / fit 9.** browse/src/write-commands.ts:349-369: after `.click({ timeout: 5000 })` throws, the catch calls `locator.evaluate(el => el.tagName === 'OPTION')` on the same locator; Playwright auto-waits, browse sets no default timeout (grep setDefaultTimeout in browse/src = 0), so the 30s library default collides with the CLI's 30s abort at browse/src/cli.ts:828 and the user sees "Command timed out" instead of the precise selector error at ~5s. Fork ref: a84a6e23 (~12 lines: `optionLocator.count().catch(() => 0)`, evaluate only when count === 1, plus a regression test). Port: cherry-pick only that hunk and test; bump the test's own timeout to 10s; reproduce pre-fix timing once with Playwright installed for the CHANGELOG line. Effort: human 2 hours, CC+gstack 10 min. Refuter found no counter beyond CI timing tightness. + +**8. ios-qa proxy hangs forever on a suspended app (s8-codediff-iosqa-proxy-timeout-hardening), P1, val 7 / fit 9.** ios-qa/daemon/src/proxy.ts:57-84 passes `timeout: 30_000` to httpRequest but per Node docs the 'timeout' event only notifies; the request must be destroyed manually. No outer deadline exists (grep setTimeout|headersTimeout in ios-qa/daemon/src/index.ts = none), so shouldRefreshTunnel's 504 branch (index.ts:245-256) is unreachable and /ios-qa, /ios-fix wedge with no typed error. Upstream has zero behavioural tests of proxyToDevice. Fork ref: 2c487305. Port: adapt, do not cherry-pick: add the `settled` guard, `req.on('timeout')` to 504 upstream_timeout then `req.destroy()`, `res.on('aborted')` to 503, `opts.timeoutMs`; KEEP `reject(err)` for unmapped codes so index.ts:213-243's ECONNRESET/EPIPE to 503 device_disconnected re-bootstrap still fires; leave out the bundled `x-gstack-expected-bundle-id` header. Port the bounded-504 test. Effort: human ~2 hours, CC+gstack 15 min. Refuter's counter: a verbatim port would defeat upstream's recovery mapping; adaptation resolves it. + +**9. `transcript_ingest_mode=off` is a dead consent switch (s1-prewave-transcript-ingest-mode-off, clustered with residual (d) of s8-codediff-bin-lib-absorbed-batch), P1, val 7 / fit 9.** bin/gstack-config:181 (`transcript_ingest_mode) echo "off"`) is the only non-prose occurrence in the repo; bin/gstack-memory-ingest.ts:541-545 gates transcripts solely on `--sources` (default ALL_TYPES at :234) and bin/gstack-gbrain-sync.ts:1211-1265 never reads the key. Yet setup-gbrain/memory.md:138 advertises it as "Disable entirely" and setup-gbrain/sections/transcript-gate.md.tmpl:47-53 offers "E) Never ingest transcripts (off)" and then runs `gbrain-sync --full` unconditionally, so a user who declined gets every transcript ingested on every /sync-gbrain. Fork refs: 6249074d (PR #2232 slice, /tmp/ta-wt/memory-trust:bin/gstack-memory-ingest.ts:523-538), fork PR #7 commit 7260b8e2 (CLOSED; not in the local clone, verified via `gh pr diff 7`; exempts --probe). Port: natively, not cherry-pick: decide semantics (honour only an explicit `off`; either change the :181 default to empty and update test/gstack-config-defaults.test.ts:139, or read the raw key with a comment explaining why); pass memory-ingest's existing `--sources` minus transcript from gbrain-sync when off; exempt --probe; one stderr line when the skip fires; fix transcript-gate.md.tmpl:52 so option E does not run --full; port the "honors off mode before invoking gbrain" test. Effort: human ~1 day, CC+gstack 30-45 min. Refuter's counter: the fork regex-parses config.yaml directly, bypassing gstack-config's GSTACK_STATE_ROOT resolution, and PR #2232 is CONFLICTING; both are shape objections, and this slice is independently shippable because it only removes writes. + +**10. question-preference hook slug mismatch (s2-runtime-question-pref-hook-slug-mismatch), P1, val 8 / fit 8.** hosts/claude/hooks/question-preference-hook.ts:300-306 `slugFromCwd` returns `path.basename(cwd)`, fed at :395 to `lookupPreference` (:149-159) which reads `projects//question-preferences.json`; the only writer, bin/gstack-question-preference:36-38, buckets by `gstack-slug` which is `owner-repo` for any repo with an origin remote (bin/gstack-slug:17-20). Additionally, no writer of `global-question-preferences` exists anywhere (only the hook reads it and the test seeds it), so the Layer 2 deny is effectively inert outside the no-.git test fixture. Fork ref: b6572ebb (mechanism is GStack-2 runtime/identity.js, not portable). Port: replace slugFromCwd's body with `slugFromEnvironment(stateRoot(), stdin.cwd)` from lib/bin-context.ts:150 (git-spawn-free on cache hits, writes the same slug-cache as gstack-slug), keep `'unknown'` for missing cwd; add a fixture that pre-seeds the slug-cache and asserts a deny; measure first-miss cost inside the 3s hook budget; confirm the Windows lane. Effort: human ~4 hours, CC+gstack 20 min. Refuter's counter: Layer 1 (`gstack-question-preference --check`) already honours project prefs so users mostly see correct behaviour; does not excuse a documented enforcement layer that never fires. + +**11. Chromium dead on Ubuntu 24.04 default kernel (s8-codediff-browse-userns-sandbox-probe), P1, val 8 / fit 8.** browse/src/browser-manager.ts:88-99 `shouldEnableChromiumSandbox` checks only win32, GSTACK_CHROMIUM_NO_SANDBOX, CI, CONTAINER and root; repo-wide grep for apparmor|userns hits only xprotect-heal.ts NEGATIVE_SIGNATURES; #2157 is OPEN and describes the Ubuntu 24.04 LTS default (`apparmor_restrict_unprivileged_userns=1`). Fork ref: 4a8833cc (sysctl probe, `isNoUsableSandboxError`, relaunch-once, `warnSandboxUnavailableOnce`, 8 unit tests). Port: take the relaunch-once fallback first, composed inside the doLaunch closure at all three sites (browser-manager.ts:528, :744, :1821) so launchWithXProtectHeal stays the outer layer; emit SANDBOX_UNAVAILABLE once and surface it in `browse status`; append the GSTACK_CHROMIUM_NO_SANDBOX=1 hint to the startup-error text; treat the sysctl probe as optional (it saves one failed launch but can downgrade where a setuid helper would have worked). Port the unit tests; extend the static tripwire to all three launch sites. Cite #2157 as closing; #2101 is not fixed by this. Effort: human 1 day incl. an Ubuntu 24.04 VM check, CC+gstack 30-45 min. Refuter's counters: the fork author asked to close #2157 as resolved by the env var; upstream deliberately chose opt-in (CHANGELOG.md:3960); the fork's warning goes to the daemon log. None outweighs a dead daemon on the current LTS default, and a relaunch on the literal zygote fatal has zero false positives. + +**12. Terminal-agent WS Origin pin is never armed (s1-prewave-terminal-ws-extension-id-pin), P1, val 6 / fit 9.** browse/src/terminal-agent.ts:41 `const EXTENSION_ID = process.env.BROWSE_EXTENSION_ID || ''`, :602 gated compare; browse/src/terminal-agent-control.ts:77-79 spawns the agent with only BROWSE_STATE_FILE/BROWSE_SERVER_PORT/BROWSE_OWNER_PID, so production never sets it, while the comment at :583-584 says the Origin gate is "required, not optional". Fork refs: 7b3f391b, d947d2e1 (PR #2226). Port ONLY the pin: split GSTACK_EXTENSION_ID into a small browse/src/extension-identity.ts (new file) re-exported from server.ts, make the /ws Origin compare unconditional (403), update the fake-origin test sites (terminal-agent-integration.test.ts:120-135) and add a foreign-ID case; before merge confirm GStack Browser.app / GBrowser's baked extension carries the same manifest key or gate the strict compare behind isCustomExtensionBakedChromium(); never copy the fork constant (`hjcdll...` is NOT upstream's `dgbkdb...`, server.ts:306). Effort: human 2 hours, CC+gstack 15 min. Refuter's counter: defense in depth only, since AUTH_TOKEN is already released only to the pinned origin via POST /extension-token (server.ts:1886-1897); accepted, which is why this is last, but the code comment and the code disagree and the fix is two lines. + +**13. Cluster: ten small bin/browse/template fixes (grep-verified at HEAD, panel TAKE P1, not adversarially refuted).** Each is a few lines with the upstream defect re-confirmed by me at the cited line; none carries voice or design risk. +- `gh pr checks` field names (gap2-skillwave-land-gh-checks-targeturl-bugfix, val 6 / fit 10): land-and-deploy/sections/first-run-validation.md.tmpl:112 `--json name,targetUrl` and land-and-deploy/SKILL.md.tmpl:154 `--json name,state,status,conclusion`; the panel verified live with gh 2.83.2 that both field sets are rejected ("Unknown JSON field"). Fix: `name,link` and `name,state,bucket`, regen. Fork ref: bbad1bab. +- Codex/Factory /ship skips plan verification (gap5-hygiene-codex-ship-qa-only-path, val 6 / fit 9): scripts/resolvers/review.ts:1178 `cat ${CLAUDE_SKILL_DIR}/../qa-only/SKILL.md`; hosts/codex.ts has no qa-only pathRewrite (grep = 0). Fix: pathRewrite to `$GSTACK_ROOT/../gstack-qa-only/SKILL.md` for codex and factory, regen goldens, test. Upstream PR #1772 (@spacegeologist) OPEN since 2026-05-28. +- Detached HEAD emits `BRANCH=HEAD` (s8-codediff-slug-detached-head-branch, val 6 / fit 10): bin/gstack-slug:274-276 passes `git rev-parse --abbrev-ref HEAD` through, so CI runs and bisects pool review/evidence logs under HEAD-reviews.jsonl. One-line map to `unknown`, plus a test. Fork ref: b6572ebb. +- brain-cache hardcodes `~/.claude/skills/gstack/bin/gstack-config` (s8-codediff-brain-cache-gstack-config-hardcoded-path, val 6 / fit 10): bin/gstack-brain-cache:614-616 silently falls back to defaults on every non-Claude host. Fix: `join(import.meta.dir, 'gstack-config')`. +- gstack-config ignores inline `# comments` (gap3-config-inline-comment-stripping, val 6 / fit 9): bin/gstack-config:321-329 read_config_value has no `#` strip, so `update_check: false # temp` fails to disable; browse/src/config.ts:208-229 (the "single source of truth" TS reader) does strip. Fix: one sed clause ordered before the key strip; two tests. +- welcome.html loads Google Fonts and Fontshare (s8-codediff-browse-welcome-offline-fonts, val 6 / fit 9): browse/src/welcome.html:7-8; server auto-navigates the headed browser there on launch, unreceipted. Fix: system font stack (+9/-7) and a no-remote-asset tripwire. Fork ref: e1cd3096. +- Dangling symlink passes validateOutputPath (s8-codediff-browse-path-security-dangling-symlink, val 6 / fit 9): browse/src/path-security.ts:43-54 shares one try for lstatSync and realpathSync with `if (e.code !== 'ENOENT') throw e`, so a dangling link under /tmp pointing outside falls through to the parent-dir check. Fork ref: f14445bb (restructure + test). +- v1.27 artifacts rename uses bare repo names (gap3-migration-v127-owner-qualified-gh-rename-and-heal, val 8 / fit 8): gstack-upgrade/migrations/v1.27.0.0.sh:219-224 `gh repo rename NEW --repo OLD` with stderr discarded; gh requires OWNER/REPO so the GitHub-side rename fails every time. Take the owner-qualification hunk (fork 5c5b8c4e, Brandon Pugsley) and consider the heal migration (56425c9a); keep upstream's #1383 failure semantics. +- GSTACK_HOME not honoured as state root in four bins (s8-codediff-gstack-home-state-dir-consistency, val 5 / fit 10): bin/gstack-update-check:25 and bin/gstack-telemetry-log:27 read only GSTACK_STATE_DIR; bin/gstack-detach writes `~/.gstack/locks` and its test cleans the real home (test/gstack-detach.test.ts:141). Six one-line edits. +- buildGbrainEnv passes a project's DATABASE_URL into a PGLite gbrain (s8-codediff-gbrain-exec-strip-caller-database-url, val 7 / fit 8): lib/gbrain-exec.ts:117-125 returns the caller env untouched when config has no database_url; issue #1917 OPEN. Adapt (strip only when config exists and lacks database_url; keep GSTACK_RESPECT_ENV_DATABASE_URL), flip the three pinning tests. +Effort for the cluster: human ~2 days, CC+gstack ~1.5 hours across two or three bisected PRs. + +## 3. Contested — take only if the maintainer agrees + +- **Self-contained questions (s3-judgment-self-contained-questions).** Refuted as a universal preamble rule (doubles output tokens at every gate; hardcodes host UI collapsing behaviour); the office-hours-specific failure in issue #879 is real. Tie-break: add one sentence at office-hours/sections/design-and-handoff.md.tmpl:168 printing the saved path plus the doc body as assistant text before the Approve/Revise gate (credit @RyanAlberts, close #879/#1116), and nothing universal? +- **/review small-diff coverage (s9-skills-review-small-diff-sweep-hole).** Refuted as a Pass 3 sweep (upstream runs an adversarial subagent on every diff, review.ts:509); what remains is one inaccurate sentence at review/checklist.md:10 and no Access Control bullet in Pass 1 CRITICAL. Tie-break: is an Access Control bullet (missing auth middleware, IDOR, path traversal) worth its bytes in a forced-read file under the context-budget ratchet? +- **Reply in the user's language (s9-skills-match-user-language, #679).** Refuted as an always-on Voice-section line (community-PR guardrail, chat-only scoping leaves reports English). Tie-break: an opt-in `reply_language` config key rendered into Writing Style, with the mixed-language tests time-attack asked for on #679? +- **AskUserQuestion compact payload (gap2-skillwave-askuserquestion-compact-payload-1241).** Refuted: shrinking `question` to 80 chars strips what question-log-hook and question-preference-hook hash and classify (hosts/claude/hooks/question-log-hook.ts:288-313, question-preference-hook.ts:404-457) and re-legitimises prose-then-tool. Tie-break: reproduce #1208 on the VSCode host first; if real, decide payload-vs-prose as a deliberate AUQ resolver design with an A/B receipt. +- **Catalog trim on external hosts (gap3-catalog-trim-external-hosts, #1972).** Refuted: on Codex the description is the only routing signal and the fork's own maintainer refused this on #1972 without routing evals. Tie-break: run a Codex routing eval; if it passes, merge #1972 with @hadrien-de-march credit. +- **$B record (s6-branches-browse-record-cdp-screencast, fork PR #54).** Refuted as a drop-in: `record` in SCOPE_READ, spawnSync ffmpeg in the daemon request path, frames never cleaned, cap evaluated only on frame arrival, VP9 into .mp4, and #2497 is already queued at TODOS.md:43. Tie-break: CDP screencast (no context rebuild, works headed, no-ffmpeg fallback) versus #2497's recordVideo .webm; decide once at triage and land one. +- **Codex sandbox canary (s8-codediff-codex-sandbox-canary), panel TAKE_PARTIAL P1, val 8 / fit 7, xref PARTIAL, not among the 48 refuted.** Fork ref 8abb08e5 (bin/gstack-codex-probe:110-142 in the fork tree). Upstream has a real round-trip model probe (bin/gstack-codex-probe:39-142) and typed CODEX_MODE degradation (scripts/resolvers/constants.ts:118-160) but no sandbox/userns signature (grep bwrap|bubblewrap|landlock|user namespace across bin/ lib/ scripts/ codex/ review/ ship/ = 0), so a Codex that exits empty because bwrap cannot create user namespaces reads as a clean pass in /review, /ship and /autoplan on devcontainers, Flatpak and hardened Ubuntu. Issue #1892 is OPEN. The appended refuter for s8-codediff-bin-lib-absorbed-batch directs this to be handled under #1892 on its own merits, not as a fork port. Tie-break: does Garry want a fail-closed `sandbox_unavailable` CODEX_MODE with the mandated one-liner "codex skipped: sandbox unavailable" plus the same signature in review.ts:383-385/548-550/689-691 and codex/SKILL.md.tmpl:310, or the GSTACK_CODEX_NO_SANDBOX opt-in that #1892 asks for, or both? Effort: human ~1 day, CC+gstack ~45 min (+15 min for the env gate). +- **Privacy page (s7-docs-method-privacy-data-flow-doc).** Refuted as a fork port (Context.dev-flavoured, drifts). The real defect surfaced: `gstack-egress grants` enumerates four grants (bin/gstack-egress:155-190) while bin/gstack-config defines `update_check`, `codex_reviews`, `pair_agent`. Tie-break: fix the grants under-enumeration (upstream bug, no fork lineage) and stop there, or also write a short table in README's Privacy section? +- **Codex dispatch consent and scan-at-sink (s4-fleet-codex-consent-and-redact-at-sink).** Refuted: Codex runs locally with `-s read-only` in the repo and reads any file, so scanning a materialised diff is scan-then-re-render, and `codex_reviews` is already the master switch. Tie-break: an egress receipt (fail-open) at each codex dispatch, since test/egress-receipt-wiring.test.ts:138-143 exempts skill prose, plus one disclosure line in the preflight? +- **Untrusted-data authority rule (gap6-s15-hostadv-untrusted-data-authority-rule).** Refuted as a preamble generator (generate-evidence-directive.ts:3-12 records the decision not to import SHARED-JUDGMENT). Concrete gap: qa/qa-only use `{{BROWSE_SETUP}}` which does not carry UNTRUSTED_CONTENT_WARNING (scripts/resolvers/browse.ts:58-60). Tie-break: wire the existing warning into BROWSE_SETUP and take the two per-skill lines from PR #2186 (@danlayfield), nothing else? +- **Delete ~/.gstack/.auth.json writer (s1-prewave-auth-json-file-write-removal).** Refuted for now: browser-manager.ts:638-640 says the component-baked GBrowser extension reads it, and the token is already on disk 0600 in the browse state file. Tie-break: does Garry know whether GBrowser's baked extension bootstraps via POST /extension-token? If yes, delete plus a migration. +- **Loosened inputs and stale strings (s1-prewave-review-loosened-inputs-stale-strings).** Byte-for-byte upstream PR #2141 (@aversini, MERGEABLE), not a fork item. Tie-break: does Garry want two more anecdote-derived categories in a forced-read checklist? If yes, merge #2141 keeping the "Enum & Value Completeness" heading (test pin at test/skill-validation.test.ts:893-897). +- **Base detection for local-only repos (s9-skills-base-detection-local-only).** Refuted: the fork ladder adds `git remote set-head -a` (network) and a merge-base guess, and upstream pins "fall back to main" (test/gen-skill-docs.test.ts:649-651). Two literal-`main` sites bypassing BASE_BRANCH_DETECT are upstream bugs: scripts/resolvers/utility.ts:111-112 and design.ts:83. Tie-break: fix those two now; revisit a "no remote" terminal state only on field reports. +- **"Pre-existing" requires evidence (s9-skills-pre-existing-requires-base-run).** Refuted as an automatic base-branch run (deps, DB side effects, minutes per ship). Tie-break: rewrite T1 vocabulary to verified/unverified with base-branch CI status (`gh run list --branch `) as default evidence and a failing-files-only worktree run as an opt-in option? +- **CLAUDE.md `## Review` section (s9-skills-review-claudemd-review-section, #452).** Refuted: the host already loads CLAUDE.md, and free-form suppression rules cannot be told from tightening. Tie-break: a docs/skills.md paragraph documenting the convention (credit @evb87-tech) and close #452? +- **/scrape evidence gate (gap2-skillwave-scrape-evidence-gate-1717).** Refuted as a 40-line step (coached E2E, breaks skillify's final-attempt slice). Tie-break: two sentences in Step 4 ("if the intent names a filter, count or ordering, the rows must demonstrate it"), credit @orendi84, close #1717? +- **gen-skill-docs single-host failure exits 0 (s2-runtime-gen-skill-docs-single-host-fatal).** Refuted as P1: setup:727-731 renders codex unconditionally under set -e, so flipping :1150 would let a Codex render error abort the Claude install. Tie-break: P3, only with a setup decision to warn-and-continue for unselected hosts and a throwing-template test. + +## 4. Defer + +- **Codex prompt over stdin (s7-docs-method-regression-pr2370-stdin-prompt-dispatch; also s4-fleet-codex-mktemp-portable's residual).** Blocked on a real bug: bin/gstack-codex-probe:194 runs `"$@" &` in the no-gtimeout watchdog, and bash redirects an async command's stdin from /dev/null, so `codex exec - < file` would dispatch an empty prompt on stock macOS. Unblocks: change :194 to `"$@" <&0 &` with a PATH-masked test, then convert exactly review-mode.md.tmpl:87 and review.ts:373. +- **Time constraint as a ceiling (s3-judgment-time-constraint-caps-scale).** Contradicts autoplan/SKILL.md.tmpl:150-159 "MUST NOT compress"; docs/designs/PACING_UPDATES_V0.md:97-110 already owns chain-scoped accounting. Unblocks: fold "a time box caps question rounds, never review completeness" into PACING V1.1. +- **Transcript source-id isolation and current-repo scoping (s1-prewave-transcript-source-id-isolation, s1-prewave-transcript-current-repo-scope; PR #2232 remainder, issue #2140).** Fork passes `--source-id` to `gbrain import` with no support probe (gbrain docs spell it `--source`); two-pass split must be re-derived across resume, reconciliation (bin/gstack-memory-ingest.ts:1803-1812, :2316-2330) and BulkResult merging. Unblocks: the never-double-store review TODOS.md:3947 gates on, a flag probe in the #2662 pattern, and confirmation that gbrain can exclude a source from dream; sequence after the `off` fix. +- **Defensive framing for red-team and security specialists (gap2-skillwave-review-defensive-framing-shared-resolver-1921).** No reproducible denial trace exists for those two prompts (time-attack's own #1921 review asked for one). Unblocks: an E2E trace; then frame only the red-team prompt via a shared constant. +- **Recording caps/fallbacks, QA recording prose, shutdown flush and tunnel-denial pins (s6-branches-record-caps-and-fallbacks, s6-branches-qa-recording-specialist-judgment, s6-branches-record-shutdown-flush, s6-branches-record-tunnel-denial-and-scope).** Nothing to govern until a record primitive lands. The fourth item (panel TAKE_PARTIAL P1, val 4 / fit 9, fork ref 76c89d56) is two one-line negative pins in browse/test/dual-listener.test.ts:109-120 and browse/test/tunnel-gate-unit.test.ts:41-53 plus the scope decision; upstream's closed TUNNEL_COMMANDS allowlist (browse/src/server.ts:315-344) already denies any unlisted command, so the pins are documentation of a property, not a behaviour change, and its earlier refuter said exactly that. Unblocks: the #2497/#54 decision; then wall-clock cap, byte ceiling, zero-frame screenshot, .webm for VP9, the tunnel pins with the "a continuous capture is a larger exfil surface than a single screenshot" rationale, the scope classification (panel recommends SCOPE_WRITE over the fork's SCOPE_READ or #2497's SCOPE_CONTROL), and four neutral prose rules written against the real semantics. +- **Web-data task taxonomy (s6-branches-web-data-task-taxonomy-and-rankings).** Points at providers upstream does not ship; the authenticated-to-own-browser rule already lives in scripts/resolvers/third-party-actions.ts:35-37. Unblocks: the Aside Phase 2 product call at TODOS.md:462-486; then two sentences in scrape's "does NOT do" list. +- **ios-qa local package wording (gap5-hygiene-ios-qa-local-package-wording-1735).** Not a fork item; an upstream #1735 close-out: ios-qa/SKILL.md.tmpl:124/:255 and ios-clean/SKILL.md.tmpl:82's wrong `.package(url:)` line. Unblocks: whoever closes #1735; decide bearer persistence separately against #1796. +- **make-pdf entity-obfuscated `javascript:` href (gap3-makepdf-parser-backed-sanitizer-not-recommended, skeptic flip, never panel-judged).** Skeptic reports make-pdf/src/render.ts:243-246 rewrites only a literal `javascript:` and fork PR #6 commit f1a92355 (CLOSED; not in the local clone, verified via `gh pr diff 6`) has a test for `javascript:`. Exploitability is low (output is a PDF rendered by Chromium), so not a shortlist item. Unblocks: port that one test; if it fails, extend the existing decode step, not the parser-backed sanitizer. +- **ios-qa per-mutation bundle guard (s8-codediff-iosqa-active-bundle-assertion, skeptic flip).** Needs StateServer.swift.template changes; decide with the ios-qa maintainers alongside #1796/#1975. +- **Panel DEFERs:** parallel /batch across tab groups (s6-branches-browser-batch-multitab-plan; blocked on the global switchTab pin at browse/src/server.ts:1063-1072); fast-path gate E2E (s4-fleet-fast-path-gate-e2e; presupposes a trivial-change fast path upstream has not adopted); Windows console-flash residue (gap1-s10-browsewave-windows-console-flash-suite; #1784's claim that Bun.spawnSync ignores windowsHide is unverified, needs a Windows tester); iOS XCUITest evidence schema and flow runner (gap4-s13-ios-xcuitest-evidence-schema, gap4-s13-ios-xcuitest-flow-runner; contradict ios-qa/SKILL.md.tmpl:41 "No simulator, no XCTest", needs an architecture decision); /cso --fix (gap2-skillwave-cso-fix-mode-safe-autofixes-1053; inverts cso's report-only contract at cso/SKILL.md.tmpl:44, needs a safe redesign); Windows network lockdown CI (s6-branches-windows-network-lockdown-ci; no offline CI phase exists to use it). + +## 5. Skip + +| Item | Reason | +|---|---| +| Bound Playwright install + continue (s1-prewave-setup-install-deadline-and-continue) | Duplicate of gap3 wearing a 300s deadline that would kill legitimate slow downloads; Playwright already retries 5x. Close #2233 once gap3 lands. | +| Setup probe deadline (s1-prewave-setup-probe-deadline) | Headline absorbed (setup:417-466 `_kill_tree`/`_wait_with_deadline`, #2136 CLOSED); install bound better served by upstream PR #2359. | +| Official-API-first rule (s6-branches-web-data-official-api-first-rule) | Truism already in the tier-3 reuse ladder; motivated by one planning anecdote; targets /scrape, which is one-shot by contract. | +| PR mutations via REST (s9-skills-pr-mutations-rest) | Ship Step 19 rewrites the body with the #1079 REST fallback after document-release; #1079 CLOSED; standalone exposure is warn-and-continue. | +| Team hook as Node .cjs (s1-prewave-team-hook-cross-platform-cjs, #2229) | Claude Code on Windows requires Git Bash; the real fix is the `bash ` prefix convention (setup:2060-2071) on bin/gstack-team-init:183-189; fork re-introduces the #2500 false-block and a Node dependency. | +| Matcher `Skill\|skill` (s1-prewave-team-hook-matcher-lowercase-skill) | No-op on every supported host; Copilot CLI does not read .claude/settings.json. | +| Issue forms and labels (s7-docs-method-issue-templates-and-labels) | Fork never implemented them either; upstream triage is wave-based, not label-based. Do fix the dangling "The evidence bar" link at .github/PULL_REQUEST_TEMPLATE.md:4 (grep 'evidence bar' CONTRIBUTING.md = 0); that fix is record s7-docs-method-contributing-evidence-bar-section, see section 5a. | +| Multi-OS static tree lane (s7-docs-method-multi-os-static-tree-gate) | Static budget tests have zero platform dependence; a macOS lane is an upstream CI decision (mktemp/keychain files), not a fork import. | +| README update-check disclosure (s1-prewave-readme-update-check-disclosure) | Fork line is now inaccurate (ls-remote plus raw fetch, two cache windows); rewrite natively from bin/gstack-update-check:157-243 if wanted. | +| Hardcoded `origin/main` residue (s4-fleet-hardcoded-origin-main-residue) | Three descriptive prose sites with near-zero impact (review/checklist.md:5, review/greptile-triage.md:86, office-hours/SKILL.md.tmpl:87); fix as housekeeping when touching those files, do not adopt the fork's banned-regex tripwire. | +| /plan-pm-review (gap2-skillwave-plan-pm-review-1666) | Explicitly declined by Garry on #1412 ("The user IS the PM"). | +| Conductor implicit AUQ hook opt-in removal (s1-prewave-conductor-implicit-auq-hook-optin) | Upstream deliberately narrowed rather than removed it (setup:2294-2307) and pins the contract in test/setup-plan-tune-hooks-noninteractive.test.ts:91-95. | +| QA evidence-per-finding layout (gap5-hygiene-1484-qa-evidence-per-finding) | Panel SKIP: judgment already upstream; #1484 folder layout adds nothing. | +| patchright stealth backend (gap1-s10-browsewave-patchright-stealth-backend) | Panel SKIP: ongoing per-Playwright-bump cost for a CDP leak upstream's Layer C already addresses. | +| BACKLOG-MAP generator (s7-docs-method-backlog-map-triage-generator) | Panel SKIP: 810-line offline triage generator for a fork process. | +| browse-migrate legacy extension IDs (s1-prewave-browse-migrate-legacy-extension) | Panel SKIP: path-derived legacy IDs are not an upstream state. | +| CLAUDE.md `node -e` snippet (s1-prewave-team-claude-md-node-snippet) | Panel SKIP: same Node dependency argument as #2229. | +| /setup-search-mcp with Exa (gap2-skillwave-setup-search-mcp-exa-2168) | Panel SKIP: adds a third-party MCP endpoint to Search Before Building. | +| GStack 2 judgment contracts: verification contract, total-verification ledger, execution depth profiles, build-scale classification, zero-question budget, proportional planning, trivial-change fast path, one-inspection-command-per-call, dispatcher precedence (s9-skills-*, s3-judgment-*) | Depend on the six-dispatcher architecture and the fifteen-vector scale system; PACING_UPDATES_V0.md:97-110 already recorded what to fold in. | +| Fleet harness sub-ideas: tEnd bookend, rage taxonomy, abandonment contract, metric provenance (s4-fleet-*) | Methodology for the persona-fleet harness TODOS.md already defers. | +| Docs/method artifacts: ADR series, audit-backlog ledger, bloat ledger, host compat evidence tiers, routing scenario fixtures, install-matrix artifacts, skills.sh distribution, devcontainer, upgrade/rollback doc, egress audit doc (s7-docs-method-*) | Contributor-facing process artifacts in fork voice; upstream keeps the equivalents machine-readable (receipts, ratchet fixtures). | +| New skills: /yc-review, /diagnose, /fanout, /community-review, /land merge queue, /plan-status, /pr-prep audit (s6-branches-yc-review-skill, gap2-skillwave-*) | Each adds catalog bytes under the enforced 1,150-token discovery ceiling for a niche. #1815 is Garry's own still-OPEN /land proposal (gh: OPEN, author garrytan) and should be decided there, not via the fork; #1949 (/fanout) is a CLOSED community PR by @sohmn. | +| Host adapters: Grok Build, Pi/Antigravity/Vibe/Qoder, Codex ~/.agents migration, request_user_input rewrite, native image_gen (gap3-hosts-*) | Host-config refactor is blocked (docs/designs/SLATE_HOST.md); declarative configs alone ship untested surfaces. | + +Beyond the items above, 39 panel-P1 and roughly 130 P2/P3 take or take-partial items (170 P2/P3 in total: P2 126, P3 44 per summary.json) were neither adversarially refuted nor grep-verified for this report. The P2/P3 leftover (browse env knobs, retro/health additions, ios-qa docs, test-infra pins, Windows PowerShell bootstrap, statusline, learnings refine, design round artifacts) is individually small and can be picked up opportunistically from /tmp/ta-eval/residual-index.json. The 39 P1 items are listed next so they are not mistaken for P2/P3. + +### 5a. Unrefuted panel-P1 take / take-partial items (39) + +Scores are user_value / fit from residual-index.json. "Panel only" means no evidence beyond the panel record was gathered for this report; the panel was generous, so treat these verdicts as unproven until grep-verified or refuted. Two entries carry an earlier prior_refuter refuted=true and are marked. + +| Id | Panel | Val/Fit | Xref | Disposition | +|---|---|---|---|---| +| s8-codediff-browse-absorbed-batch | TAKE_PARTIAL | 7/9 | PARTIAL | Mis-titled batch; 12 of 13 absorbed. Panel only for the one residual. | +| s9-skills-ship-breaking-change-over-linecount | TAKE_PARTIAL | 7/9 | PARTIAL | Overlaps CLAUDE.md "Scale-aware bumps" prose; panel only. | +| s9-skills-eng-review-pre-mortem-and-data-model | TAKE_PARTIAL | 8/8 | MISSING | Adds a pre-mortem step to plan-eng-review; voice-bearing template change, needs Garry's read. Panel only. | +| gap3-codex-probe-custom-provider-env-key | TAKE | 7/9 | MISSING | Codex auth probe for config.toml env_key providers; panel only. | +| gap2-skillwave-office-hours-codebase-surface-premise-verify-1738 | TAKE_PARTIAL | 8/8 | MISSING | Verify codebase-fact premises before AUQ in office-hours; template change, panel only. | +| gap2-skillwave-retro-persist-recommendations-followthrough-1834 | TAKE | 8/8 | MISSING | /retro persists recommendations and scores follow-through; panel only. | +| gap2-skillwave-retro-language-agnostic-test-detection-2037-2013 | TAKE | 8/8 | PARTIAL | Python/Terraform/Bats test-file detection in retro metrics; consistent with platform-agnostic rule, panel only. | +| gap2-skillwave-cso-mini-shai-hulud-tier3-1523 | TAKE | 8/8 | MISSING | /cso Tier 3 known-campaign IOC rules; panel only. | +| gap6-s15-hostadv-fixture-set-and-schema | TAKE | 8/8 | MISSING | Injection-resistance fixture set; sibling of the persona-fleet harness TODOS.md defers, panel only. | +| s8-codediff-iosqa-drop-resolve6-unicast-dns | TAKE | 6/9 | MISSING | Drop dns.resolve6 fallback leaking device hostname; panel only. | +| s8-codediff-memory-ingest-default-secret-gate | TAKE | 7/8 | MISSING | Default-ON redact scan over staged ingest bytes; panel only. | +| s8-codediff-iosqa-devicectl-typed-errors-hwudid | TAKE | 7/8 | MISSING | devicectl typed errors + hardware-UDID match; same fork commit as shortlist item 8, panel only. | +| s8-codediff-codex-sandbox-canary | TAKE_PARTIAL | 8/7 | PARTIAL | Listed in section 3 (Contested); handle under OPEN issue #1892 on its own merits per the bin-lib refuter. | +| gap2-skillwave-reviewer-discipline-gate-884 | TAKE_PARTIAL | 7/8 | MISSING | Requested human review as merge gate (#884); panel only. | +| s2-runtime-workflow-hardening-tripwire-test | TAKE | 6/9 | MISSING | Free test for mutable action refs / permissions / concurrency; pairs with the sha-pins item below, panel only. | +| s4-fleet-spec-execute-host-neutral-spawn | TAKE | 6/9 | MISSING | /spec --execute spawns `claude -p` on every host; panel only. | +| s4-fleet-review-precision-answer-key-corpus | TAKE_PARTIAL | 7/8 | PARTIAL | Answer-key review corpus; TODOS.md already defers answer-key evals, panel only. | +| s7-docs-method-contributing-evidence-bar-section | TAKE | 6/9 | PARTIAL | The record behind the dangling PULL_REQUEST_TEMPLATE.md:4 link (verified: grep 'evidence bar' CONTRIBUTING.md = 0). Write the section in house voice or re-point the link; do not paste fork prose. Human 2-3 hours, CC+gstack 15 min. | +| s8-codediff-make-pdf-render-sentinel-invariant | TAKE | 6/9 | PARTIAL | Strip U+0000 at render entry, throw on surviving placeholder; panel only. | +| s9-skills-system-functional-qa | TAKE | 8/7 | MISSING | QA for APIs/CLIs/jobs/webhooks; new skill-sized surface under the catalog ceiling, needs a product call. Panel only. | +| s9-skills-debug-bisect-and-nonreproduction-taxonomy | TAKE_PARTIAL | 7/8 | MISSING | /investigate bounded bisect + non-reproduction classes; panel only. | +| s9-skills-review-finding-validation-gate | TAKE_PARTIAL | 7/8 | PARTIAL | VALIDATED/REJECTED/UNCERTAIN per finding before Fix-First; panel only. | +| gap1-s10-browsewave-chromium-path-setup-probe | TAKE | 6/9 | MISSING | setup launch probe honours GSTACK_CHROMIUM_PATH; natural rider on shortlist item 3, panel only. | +| gap1-s10-browsewave-cookie-import-cdp-pipe | TAKE | 7/8 | MISSING | Windows cookie import over --remote-debugging-pipe; panel only. | +| gap1-s10-browsewave-untrusted-content-hardening-prose | TAKE_PARTIAL | 7/8 | MISSING | Skill-prose hardening across investigate/browse/qa/canary/ship; overlaps the BROWSE_SETUP tie-break in section 3, panel only. | +| gap3-codex-jsonl-parser-bin | TAKE | 7/8 | MISSING | bin/gstack-codex-jsonl-parser replacing inline python in /codex (#1329); panel only. | +| gap2-skillwave-land-and-deploy-configurable-merge-method-726 | TAKE | 7/8 | MISSING | Configurable merge method via CLAUDE.md Deploy Configuration; fits utility.ts:65 idiom, panel only. | +| s9-skills-ask-only-uninferable | TAKE_PARTIAL | 7/7 | MISSING | SHARED-JUDGMENT clause 10; preamble growth under the ratchet, panel only. | +| s3-judgment-session-start-code-intelligence-offer | TAKE_PARTIAL | 6/8 | PARTIAL | Session-start indexing offer; panel only. | +| s7-docs-method-workflow-sha-pins-and-permissions | TAKE | 5/9 | PARTIAL | SHA-pin actions + top-level permissions; CI hygiene, panel only. | +| gap5-hygiene-chromium-path-status-correction | TAKE | 5/9 | PARTIAL | GSTACK_CHROMIUM_PATH for headless launch; panel only. | +| s9-skills-external-effects-no-blind-retry | TAKE_PARTIAL | 7/7 | PARTIAL | EXTERNAL-EFFECTS semantics; the PR-mutations sibling was refuted (section 5), panel only. | +| s1-prewave-uninstall-preserve-unowned-codex-dirs | TAKE_PARTIAL | 6/8 | PARTIAL | Prior refuter refuted=true: bug is real but is upstream's own TODOS.md:258-262 backlog item; do it there, not as a port. | +| s5-beta-human-era-anchors | TAKE_PARTIAL | 7/7 | MISSING | Retire file-count/human-day proxies; touches ship/review heuristics Garry wrote, panel only. | +| s9-skills-ship-smallest-release-convention | TAKE_PARTIAL | 7/7 | MISSING | Honour the repo's release convention at its smallest; panel only. | +| gap3-codex-global-discover-ssh-url-form | TAKE | 5/9 | MISSING | normalizeRemoteUrl ssh:// slash form; small, panel only. | +| s6-branches-record-tunnel-denial-and-scope | TAKE_PARTIAL | 4/9 | MISSING | Prior refuter refuted=true (already guaranteed by the closed TUNNEL_COMMANDS allowlist); folded into the record cluster in section 4. | +| s5-beta-hung-child-stall-guards | TAKE_PARTIAL | 5/8 | PARTIAL | E2E idle-stall/wall-clock guards; overlaps v1.77 test-infra wave, panel only. | +| gap2-skillwave-office-hours-artifact-outcome-1049 | TAKE | 6/7 | MISSING | Verify design doc exists before outcome:success (#1049); panel only. | + +## 6. Already absorbed / superseded / not applicable + +From records-other.json (100 ABSORBED, 25 NOT_APPLICABLE, 3 SUPERSEDED), grouped: + +| Cluster | Status | Evidence | +|---|---|---| +| Apple App Store release journey (10 items: core roll-up plus adapter before branch gate, classify errors before credentials, browser credential ban + escalation ladder, deck editor, macOS CI runner split, pricing in authorization, session-minted upload key, sticky assets, two permitted interactions) | ABSORBED | ship/sections/apple-release.md.tmpl (header credits time-attack; wave 2, then hardened with `allAppsVisible:false`) | +| Judgment rules: design docs repo-local (#703), decision-record concision (#2000), evidence-before-claimed-limitations, third-party web actions contract, founder-resources opt-out (#538), prose fallback when no question tool | ABSORBED | office-hours/sections/design-and-handoff.md.tmpl:20-42, :343-366; scripts/resolvers/preamble/generate-evidence-directive.ts:15-17; scripts/resolvers/third-party-actions.ts:29-45; generate-ask-user-format.ts:13-37 | +| Egress receipts, gstack-egress CLI, context-bill, discovery-surface ceiling, distill/eval model host-neutral | ABSORBED (wave 1) | bin/gstack-egress, lib/egress-receipt.ts, test/egress-receipt-wiring.test.ts; test/catalog-budget.test.ts; lib/eval-model.ts | +| Browse daemon: bun-polyfill spawn parity, disconnect-cause null browser, numbered-profile cookies (#2139), Playwright ^1.62 pin, Windows ACL SID (#2170), closeTab race, handoff SingletonLock, stop-ack before shutdown (#2020), live-but-busy daemon preservation (#2219/#2231), lock errno honesty (#1084), extension getPort withhold (#1822), CJK IME (#1272), WS subprotocol echo, /health token removal, stealth tests, telemetry config tier | ABSORBED | browse/src/browser-manager.ts:194,986-1020,1796-1798; cli.ts:608-641,684-745; meta-commands.ts:424-436; file-permissions.ts:45-87; server.ts:1870-1915; extension/sender-auth.js; browse/src/telemetry.ts:47-91 | +| Setup/install: --help side-effect free, gstack-paths shell quoting, settings-hook fail-closed, mirror support files (#1502), absolute bin path (#1820), investigate hook paths (#1873), freeze symlink resolution (#781), careful compound-command guard (#2039), Codex/setup wave set, codex web-search flag | ABSORBED | setup:16-49, :871, :964-967; bin/gstack-paths:16-22; bin/gstack-settings-hook:70-223; freeze/bin/check-freeze.sh:100-116; careful/bin/check-careful.sh:186-189; scripts/resolvers/constants.ts:50-65 | +| CI / supply chain: bun-version pin, dependency-review + osv-scanner, OSV config, fork-PR secret guards, gate secret scan, workflow concurrency, basic-ftp pin, diff ^9 bump | ABSORBED | 9 workflows pin bun 1.3.13; .github/workflows/{dependency-review,osv-scanner}.yml; .osv-scanner.toml; evals.yml:78-107; .github/scripts/gate-secret-scan.mjs; test/workflow-concurrency.test.ts; package.json:59 | +| gbrain / memory: policy tier chokepoint, ingest staging, brain-context-load cold probe, brain-sync tilde paths (#1656), decision-log --supersede, setup-gbrain provider Step 0, codex-session-import xargs -r, deprecated brain-consumer scripts deleted | ABSORBED | bin/gstack-gbrain-sync.ts:45,839-880; bin/gstack-brain-context-load.ts:72-200; bin/gstack-decision-log:57,97; setup-gbrain/SKILL.md.tmpl:107-142; bin/gstack-codex-session-import:67-71 | +| ios-qa: boot token out of os_log, loopback bind, hashed session tokens, touch selectors out of Release | ABSORBED | ios-qa/templates/StateServer.swift.template:100-105,154-167; ios-qa/daemon/src/index.ts:365-397; DebugBridgeTouch.m.template:23 | +| make-pdf: invisible preamble, sibling browse before PATH, absorbed fix batch (render/smartypants) | ABSORBED | make-pdf/src/render.ts:69-70,213-265,487-515; browseClient.ts:10-20 | +| Test infra: hermetic skills config dir, no-suicide-exit guard, handoff test isolation, sharded paid tier, CI gate binaries + poppler, codex E2E MCP isolation, multi-ecosystem ship test markers, Xvfb argv0 match, design timeout/count validation, benchmark timeout validation, catalog lead periods, artifacts-init push protocol (#2225), telemetry error mapping (#769), context-recovery slug (#1851), autoplan jq bind (#2021), land-and-deploy squash readback (#2011), one-way-door reset/revoke (#2024), document-generate (#1477), Opus 4.7 migration, codex openai.yaml, Windows spawn/DACL, question-format hook outputs | ABSORBED | test/helpers/hermetic-env.ts:278; test/no-suicide-exit.test.ts; scripts/test-paid-shards.ts; free-tests.yml:92-128; codex-session-runner.ts:211-214; scripts/resolvers/testing.ts:8-50; browse/src/xvfb.ts:144-170; bin/gstack-artifacts-init:67-82,218-256; scripts/one-way-doors.ts:66-72 | +| Nine further ABSORBED items not named in a row above: s1-prewave-sidebar-chat-era-test-deletion (#1984), gap3-test-infra-wave-fully-absorbed (14 community PRs), gap2-skillwave-env-var-resolver-paths-1704, gap1-s10-browsewave-merged-pair-blocklist-and-brain-trust-ids (#2228), gap1-s10-browsewave-update-check-hardening (#1982), gap1-s10-browsewave-spawned-session-helper-and-telemetry-state (#1188), s6-branches-investigate-brokenness-v035, gap5-hygiene-bac5d9a5-unreachable-gate-tests-closed, s5-beta-body-floor-guard | ABSORBED | One-line evidence per id in records-other.json; with these the 100 ABSORBED reconcile against the rows above | +| GStack 2 runtime internals: execution-result envelope, release pipeline/attestation, shard heuristics, dispatcher headers and alias tables, runtime/cli main guard, deepeval/platform bakeoff, PostHog, demos fixture, Windows isolated gates harness, web-data egress rails, browser-provider eval, telemetry domain hashing, ios executor planner | NOT_APPLICABLE | No runtime/, evals/, demos/ or executor/ directories upstream; TODOS.md:460-505 and PACING_UPDATES_V0.md:97-110 record what was folded in instead | +| Deliberate upstream decisions: generic gpt-5.6 overlay (#2245; Sol is exact-only, scripts/models.ts:26-41), Slate first-class arm (docs/designs/SLATE_HOST.md), Hermes install arm/frontmatter (hosts/hermes.ts:32-37), careful text-segment filter (#1110, a destructive-command bypass), gstack-paths --get, extension storage.session auth (POST /extension-token chosen), Apple intermediate browser-drive states (s3-judgment-apple-intermediate-browser-drive-and-asc-cli), beta-first eval recalibration, slop-diff generated exclusion, windows-setup-e2e runtime lane | NOT_APPLICABLE | As cited per row in records-other.json | +| design daemon reset timers; Claude Desktop AUQ pre-empt (#2146); memory-ingest total-files tripwire (#2144) | SUPERSEDED | design/src/daemon.ts:206,489 (different shape); question-preference-hook.ts:21-23 never emits 'defer'; bin/gstack-memory-ingest.ts:2303-2346 reconciliation check | + +## 7. Open time-attack PRs on upstream + +All eight upstream PRs are OPEN and CONFLICTING as of 2026-09-02 (gh); none can merge as-is. + +| PR | Title | Disposition | Grounding | +|---|---|---|---| +| #2235 | /bug-report skill | Port-the-residual, then close | Panel P2 val 7 / fit 5: a new tier-2 skill overlapping /investigate under the 1,150-token catalog cap. The independently useful piece is the redact-doc `retain` argument: scripts/resolvers/redact-doc.ts:85-88 deletes $REDACT_FILE right after the scan while the prose says "pass the SAME file downstream" (s1-prewave-bug-report-skill). Take `retain`, decline the skill. | +| #2233 | Bound Playwright bootstrap, preserve skill registration | Close-superseded | Probe deadline absorbed (setup:417-466, #2136 CLOSED); install bound better served by PR #2359 (30 min, GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT); the continue-after-failure half lands via the gap3 re-implementation crediting #1900. Update TODOS.md:3951-3953. | +| #2232 | Transcript trust, scope, source isolation | Port-the-residual | Take the `transcript_ingest_mode=off` slice natively now (shortlist item 9); defer scope and `--source-id` behind the never-double-store review (TODOS.md:3947) and a gbrain flag probe. Close the PR after the off fix with a pointer to the deferred design. | +| #2231 | Preserve live daemon during busy loads | Close-superseded | browse/src/cli.ts:684-686 daemonPidAlive + probeHealthWithBackoff, :733-745 "#2219 IRON RULE"; issue #2219 CLOSED 2026-08-17 (records-other: s1-prewave-live-daemon-preservation ABSORBED). | +| #2230 | Test fixes for 182 failures unmasked by #2172 | Close-superseded | 5 of 6 fixes absorbed or superseded plus the bunfig PATH-restore preload (s1-prewave-hermetic-test-fixes). Optional 15-minute residual: one shared `codex --version` availability helper replacing seven `which codex` gates. | +| #2229 | Team-init cross-platform hook + Copilot lowercase skill | Close-not-wanted | Fork re-introduces the #2500 false-block (checks only ~/.claude/skills/gstack), adds a Node dependency, Copilot does not read .claude/settings.json, README hunk is a voice rewrite. Native fix instead: `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/check-gstack.sh"` on Windows at bin/gstack-team-init:183-189, or route through gstack-settings-hook. | +| #2226 | Browse local auth leak + legacy profile migration | Port-the-residual | Take only the terminal-agent WS Origin pin (shortlist item 12). auth.json deletion gated on a GBrowser source check; bin/gstack-browse-migrate, bin/gstack-browse-repair and the whole-surface Host check (#1324) stay separate decisions. Never copy the fork's extension ID constant. | +| #2225 | artifacts-init HTTPS push remotes | Close-superseded | bin/gstack-artifacts-init:11, :67-82, :218-256 `--push-protocol` incl. `preserve` and `gh config get git_protocol`; test/gstack-artifacts-init.test.ts:277-380 carries the same test titles. | + +Fork's own open PRs: + +| PR | Title | Disposition | +|---|---|---| +| time-attack #54 | $B record (CDP screencast) + iOS session poller | Do not merge as-is. Treat as the design alternative when triaging #2497 (TODOS.md:43): fix SCOPE_READ to a stricter scope, no spawnSync ffmpeg in the daemon, wall-clock cap, frame cleanup, .webm for VP9, tunnel-denial pins (s6-branches-record-tunnel-denial-and-scope), then land one implementation with @Topherhindman and @itstimwhite credit. The iOS poller (s6-branches-ios-record-session-poller) follows the same decision. | +| time-attack #40 | Web-data provider contract (Firecrawl/Exa/Context.dev/Aside) | Not wanted now (CONFLICTING on the fork too). Revisit only at the Aside Phase 2 product call (TODOS.md:462-486); upstream deliberately ships no off-machine scraping providers (s6-branches-web-data-provider-cli, s6-branches-web-data-task-taxonomy-and-rankings). | +| time-attack #55 | AGENTS.md Cursor Cloud notes | Do not put in AGENTS.md (upstream's is the generated catalog). The useful residual is routing raw `git commit` calls in test/diff-scope.test.ts:21-31, gstack-version-bump, team-mode, worktree and gstack-repo-mode tests through test/helpers/scratch-repo.ts (gpgsign/fsmonitor neutralised) plus one CONTRIBUTING.md:175-177 sandbox sentence (s7-docs-method-agents-md-cursor-cloud-gotcha). | + +## 8. Suggested port order + +1. **Wave A: setup and hook correctness (three bisected commits, one PR).** Playwright best-effort block (credit #1900), check-freeze STATE_DIR line plus test pins (credit #1509), gstack-relink ownership gate plus tests (credit #2119). Effort: human ~1.5 days, CC+gstack ~1 hour plus one free-suite run. +2. **Wave B: stale community PR merges and contributor-doc fixes.** #1777 (taste confidence), #1096 (Render key), optionally #2141 if the checklist growth is accepted, the #1772-equivalent Codex/Factory qa-only pathRewrite with golden regen, and the CONTRIBUTING "The evidence bar" section or link re-point (s7-docs-method-contributing-evidence-bar-section). Effort: human ~half a day, CC+gstack ~45 min. +3. **Wave C: browse, make-pdf, ios small fixes.** make-pdf preview offline gate, click no-second-wait, ios proxy timeout (adapted), terminal WS Origin pin, welcome.html system fonts, dangling-symlink validateOutputPath, Ubuntu userns relaunch-once (closes #2157). One commit each. Effort: human ~2.5 days incl. an Ubuntu 24.04 check, CC+gstack ~2 hours. +4. **Wave D: /ship platform-agnostic purge.** tests.md.tmpl Steps 5/6 rewrite, ship/SKILL.md.tmpl:381 evidence pairs, readiness-gate default, `gh pr checks` field fix, goldens regen, marker test extension (credit #1102, close #1069). Effort: human ~2.5 days, CC+gstack ~2 hours. +5. **Wave E: consent and state hygiene.** `transcript_ingest_mode=off` honoured plus transcript-gate prose; GSTACK_HOME in update-check/telemetry-log/detach; brain-cache config path; gstack-config inline comments; detached-HEAD slug; v1.27 migration owner qualification; gbrain-exec DATABASE_URL strip (adapted); `gstack-egress grants` under-enumeration (update_check, codex_reviews, pair_agent). Effort: human ~2.5 days, CC+gstack ~2 hours. +6. **Wave F: contested items, only after Garry's calls in section 3.** Office-hours #879 one-sentence fix, Access Control bullet, opt-in reply_language, UNTRUSTED_CONTENT_WARNING into BROWSE_SETUP, egress receipt at codex dispatch, Codex sandbox canary under #1892, literal-`main` fixes at utility.ts:111-112 and design.ts:83, codex stdin dispatch after the gstack-codex-probe:194 wrapper fix. Effort: human ~3 days, CC+gstack ~2.5 hours, plus one paid AUQ/ship eval run for anything touching the preamble. The 39 unrefuted P1 items in section 5a are not scheduled; each needs the same grep-verify-or-refute pass the 48 received before it earns a wave. \ No newline at end of file diff --git a/docs/designs/fork-port-residual-2026-09/SHAS.md b/docs/designs/fork-port-residual-2026-09/SHAS.md new file mode 100644 index 000000000..b8293ccb3 --- /dev/null +++ b/docs/designs/fork-port-residual-2026-09/SHAS.md @@ -0,0 +1,21 @@ +# Fork-port residual evaluation: provenance + +Evaluated 2026-09-02. Read-only; see REPORT.md for the maintainer-facing result. + +| Ref | SHA | +|---|---| +| time-attack/gstack main (fork tip) | `0aca1f77dec3766c1627e219653ab181d380b542` | +| garrytan/gstack main (upstream HEAD at evaluation) | `0d1bd5616c0ef096bb7ccee336f63c60ee408618` (v1.79.0.0) | +| merge-base | `7c9df1c568a9ea745508f679a329332b2c338063` | + +## Files + +| File | sha256 | What | +|---|---|---| +| REPORT.md | `087a8476c3bb8d081626ee55eff00845a18ef2e8cbec049ce1c63ec860a33bb4` | eight-section report (shortlist, contested, defer, skip, absorbed, PR dispositions, port order) | +| residual-index-lite.json | `a342e6cd7b57e08b05ad2355e04765e995496dbf7bbb65399ea38c8a4b344644` | 287 residual items: cross-reference status, panel verdict, refuter pointer | +| records-other.json | `b13e64596bee93ceffb4161d30dc30f9cc6f5fd2db3dd0184bc31187b328a19f` | 128 items already absorbed / superseded / not applicable, one-line evidence each | +| refuters.json | `4c7740a2f5bd0978f564468cab18d9a5945cb2a38108759a23d74900f2af2b0f` | 48 adversarial refuter verdicts on the top-ranked candidates | +| summary.json | `53b3c9c45f62d49166c5cd36a1ef383da60cd899b7e2110851b13f94731db1dc` | counts | + +Method: two research workflows (sweep + cross-reference + panel; refute + synthesize + fact-check), then a CEO review (HOLD SCOPE), an eng review, and two Codex outside-voice passes. The full-text residual index (2.9MB) was not committed; the lite index carries every id, status, verdict, and truncated residual text. diff --git a/docs/designs/fork-port-residual-2026-09/records-other.json b/docs/designs/fork-port-residual-2026-09/records-other.json new file mode 100644 index 000000000..cfa06ebb7 --- /dev/null +++ b/docs/designs/fork-port-residual-2026-09/records-other.json @@ -0,0 +1,2104 @@ +[ + { + "id": "s3-judgment-evidence-before-claimed-limitations", + "title": "SHARED-JUDGMENT clause 13: evidence before claimed limitations (absorbed)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #47 (merge 21a4fdfc, commit 38f0c0ef, branch time-attack/appleship-evidence-clause, v1.64.17.0)", + "38f0c0ef", + "5757efc6" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s3-judgment-evidence-before-claimed-limitations] ABSORBED as a preamble directive: scripts/resolvers/preamble/generate-evidence-directive.ts:15-17 ('## Claimed Limitations Need Evidence \u2014 A claimed limitation or requirement... is a material claim. State one only with the verbatim error, the documen" + ], + "notes": "" + }, + { + "id": "s3-judgment-third-party-web-actions-contract", + "title": "THIRD-PARTY-ACTIONS.md contract (agentic browser first, per-task consent) (absorbed)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #24 (merge 18e0c840, branch time-attack/third-party-actions, v1.63.0.0)", + "c8c0c259", + "71b54789" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s3-judgment-third-party-web-actions-contract] ABSORBED and extended: scripts/resolvers/third-party-actions.ts:29-45 carries all five rules ('Adapted from time-attack/gstack's THIRD-PARTY-ACTIONS.md'), now recommending Aside by name with detect-and-defer (2026-08-27) and adding CAPTCHA to user-perfo" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-release-journey-core", + "title": "Apple App Store release adapter roll-ups (APPLE-RELEASE.md) (absorbed; s3 sub-rules stay separate)", + "kind": "feature", + "fork_refs": [ + "fork PR #29 (merge 729134b3, branch time-attack/appleship, v1.64.0.0)", + "fork PRs #31-#50 (merges efa24a1e..a6bc2de3)", + "df7f296d", + "547afe49", + "2bbd8a06", + "d71162b3", + "6452b82e", + "8dee3c2b", + "0095fec2", + "ed070a75", + "f20e6102", + "b506b4ca" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s3-judgment-apple-release-journey-core] ABSORBED (wave 2, then hardened): ship/sections/apple-release.md.tmpl (58 lines, header credits 'Ported from time-attack/gstack (GStack 2) APPLE-RELEASE.md, refined across 21 live App Store releases'), covering membership gate + free-account ceiling (para 'No" + ], + "notes": "" + }, + { + "id": "s4-fleet-egress-receipts", + "title": "Egress receipts / gstack-egress CLI / verify Stop hook / context-bill calibration (absorbed wave 1)", + "kind": "security", + "fork_refs": [ + "f29966d9", + "a78c9a0f", + "docs/gstack-2/EGRESS-RECEIPTS.md", + "fcd2e444", + "24899e7c", + "08459f9f", + "e933731c", + "9606ea63", + "7b4fb1a7" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s4-fleet-egress-receipts] ABSORBED: bin/gstack-egress + lib/egress-receipt.ts + bin/gstack-egress-lib.sh per CLAUDE.md; test/egress-receipt-wiring.test.ts:9 ('Every enumerated off-machine sink must route its send through the receipt'), :115 SCANNER_EXEMPT. The per-surface call-site table doc has no" + ], + "notes": "" + }, + { + "id": "s5-beta-make-pdf-sibling-browse-before-path", + "title": "make-pdf resolves sibling browse binary before PATH (6b00ca6b)", + "kind": "fix", + "fork_refs": [ + "6b00ca6b" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s5-beta-make-pdf-sibling-browse-before-path] ABSORBED / pre-existing (00f966b3 v1.30.0.0): make-pdf/src/browseClient.ts:10-20 order GSTACK_BROWSE_BIN -> BROWSE_BIN -> sibling via execPath (#2156) -> ~/.claude/skills/gstack -> Bun.which -> error with hint; :153-162 candidate listing.\n[s4-fleet-make-" + ], + "notes": "" + }, + { + "id": "s5-beta-ship-multi-ecosystem-markers", + "title": "/ship test detection: Django/JVM/Elixir/Rust markers as evidence for the ask (e3259078)", + "kind": "fix", + "fork_refs": [ + "e3259078" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s5-beta-ship-multi-ecosystem-markers] ABSORBED: scripts/resolvers/testing.ts:8-50 contains the identical marker block (manage.py :13, mix.exs :20, pom.xml :21, gradle :22, TESTFILES :32, rust in-source :34) and offer table (:43-50).\n[s4-fleet-django-test-detection] ABSORBED: scripts/resolvers/testi" + ], + "notes": "" + }, + { + "id": "s5-beta-decision-log-supersede-replacement", + "title": "gstack-decision-log --supersede writes the replacement (b4d7abd1)", + "kind": "fix", + "fork_refs": [ + "b4d7abd1" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s5-beta-decision-log-supersede-replacement] ABSORBED (2be6c06b wave 2): bin/gstack-decision-log:57 jsonArg found by leading `{`; :97 `{ ...validPayload(jsonArg), supersedes: targetId }`.\n[s4-fleet-decision-log-supersede] ABSORBED: bin/gstack-decision-log:78-97 (`const replacement = jsonArg ? { ...v" + ], + "notes": "" + }, + { + "id": "s7-docs-method-telemetry-domain-hashing-policy", + "title": "Telemetry privacy: salted-hash security_url_domain + anonymous tier local-only (b79f41ee)", + "kind": "security", + "fork_refs": [ + "evals/privacy/egress-audit-2026-07-28.md residual risk #1", + "b79f41ee", + "e1cd3096", + "94e46742" + ], + "final": "NOT_APPLICABLE", + "basis": "cross-reference", + "evidence": [ + "browse/src/security.ts:311-316 \u2014 NOTE: logAttempt + salted payload hashing + telemetry spawn plumbing were ripped with sidebar-agent.ts (#2557, CHANGELOG.md:902 v1.67.0.0 / :990-995); grep hashPayload|gstack-telemetry-log|buildTelemetrySpawnCommand in browse/src \u2192 0 hits", + "bin/gstack-telemetry-log:46,65,239,253,261 \u2014 `--url-domain` flag still parsed and emitted raw via json_safe only; repo-wide grep url-domain|urlDomain|url_domain|attack_attempt|--event-type outside this file \u2192 0 producers (only supabase/migrations/004_attack_telemetry.sql:9,29,38 and supabase/functions/community-pulse/index.ts:135,156-159 consume it)", + "supabase/functions/community-pulse/index.ts:156-159 \u2014 aggregates domainCounts keyed by raw security_url_domain for the 'top domains last 7 days' index (migration 004 line 18) \u2014 upstream's dashboard design intends raw hostnames" + ], + "notes": "Fork b79f41ee changed 3 things: (1) hash security_url_domain in browse/src/security.ts reportAttemptTelemetry \u2014 upstream deleted that entire producer in v1.67.0.0 (#2557), so no raw domain leaves the " + }, + { + "id": "s5-beta-brain-context-load-cold-probe", + "title": "brain-context-load cold-probe latency fix (2fdf6d02, absorbed)", + "kind": "fix", + "fork_refs": [ + "2fdf6d02" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s5-beta-brain-context-load-cold-probe] ABSORBED (2be6c06b wave 2): bin/gstack-brain-context-load.ts:72-73 GSTACK_BRAIN_TIMEOUT_MS override; :199-200 same '500ms budget misreported gbrain as missing whenever a cold process spawn exceeded the timeout' comment.\n[s4-fleet-brain-context-load-cold-probe]" + ], + "notes": "" + }, + { + "id": "s5-beta-gbrain-policy-ingest-staging-pdf-sanitizer", + "title": "gbrain-sync per-repo policy + memory-ingest gbrain 0.42 staging + make-pdf offline sanitizer (absorbed wave 2)", + "kind": "security", + "fork_refs": [ + "b41b4a78", + "cca23b2d", + "3a082841" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s5-beta-gbrain-policy-ingest-staging-pdf-sanitizer] ABSORBED (wave 2): bin/gstack-gbrain-sync.ts:45,839,870,880 repoPolicyTier chokepoint + skipped-policy-read-only; bin/gstack-memory-ingest.ts:1408-1416 policy counters and #2392 policyError abort; make-pdf/src/render.ts:214,265-281 @import/url()/s" + ], + "notes": "" + }, + { + "id": "s4-fleet-browse-lock-error-honesty", + "title": "browse acquireServerLock reports real errno, only EEXIST is contention (#1084/#1725, absorbed)", + "kind": "fix", + "fork_refs": [ + "4a8833cc", + "d947d2e1", + "113717b0", + "upstream PR #1725 @jbetala7 (OPEN)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s4-fleet-browse-lock-error-honesty] ABSORBED: browse/src/cli.ts:608-641 (ServerLockError, '#1084' comment, ENOENT retry), :263-281 Chromium profile lock helpers (#1781). Busy-daemon preservation (#2231/#2219) belongs to another slice.\n[gap1-s10-browsewave-lock-acquisition-errors] ABSORBED: upstream" + ], + "notes": "" + }, + { + "id": "s1-prewave-no-suicide-exit-guard", + "title": "Remove delayed process.exit test teardowns + no-suicide-exit static guard (#2172/#2252/#2230, absorbed)", + "kind": "test-infra", + "fork_refs": [ + "e7c37d76", + "upstream PR #2230", + "a26de527", + "d947d2e1", + "4dced295", + "269acaf6", + "7741b754", + "f0beb1a7", + "af10edb1", + "2eefdea4", + "045bdaa5", + "628c76b6", + "upstream PR #2172 @sneakygriff (OPEN)", + "upstream PR #2252 @whd4 (OPEN)", + "upstream PR #2230 @time-attack (OPEN)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s1-prewave-no-suicide-exit-guard] ABSORBED: /home/vercel-sandbox/gstack/test/no-suicide-exit.test.ts exists; v1.77 strict shard classifier also fails a shard without bun's summary line (CLAUDE.md).\n[s4-fleet-no-suicide-exit-test] ABSORBED: test/no-suicide-exit.test.ts exists; no `setTimeout(() => p" + ], + "notes": "" + }, + { + "id": "s1-prewave-closetab-last-tab-race", + "title": "closeTab captures wasActive before page.close() (absorbed 3aab7654)", + "kind": "fix", + "fork_refs": [ + "38381436", + "pr-2172-test-env", + "upstream PR #2230", + "b6572ebb" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s1-prewave-closetab-last-tab-race] ABSORBED: upstream commit 3aab7654 'capture active-tab state before close()'; browse/src/browser-manager.ts:986-1020 (`const wasActive`, 'only reassign when activeTabId no longer points at a live tab').\n[s2-runtime-closetab-wasactive] ABSORBED: upstream browse/src" + ], + "notes": "" + }, + { + "id": "gap2-skillwave-investigate-hook-paths-claude-skill-dir-1873", + "title": "investigate hook paths drop Claude-only CLAUDE_SKILL_DIR (#1873, absorbed)", + "kind": "fix", + "fork_refs": [ + "cdf3531a (maxpetrusenkoagent)", + "c896016b (t)", + "upstream PR #1873 OPEN (maxpetrusenkoagent); issue #2469 CLOSED 2026-08-15", + "8bc0a04f" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[gap2-skillwave-investigate-hook-paths-claude-skill-dir-1873] ABSORBED: investigate/SKILL.md.tmpl:33-38 hooks use `$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh`; :121-124 comment 'frontmatter hooks ... run before CLAUDE_SKILL_DIR exists ... (#2469)'.\n[s4-fleet-investigate-claude-skill-dir]" + ], + "notes": "" + }, + { + "id": "s5-beta-windows-spawn-dacl-hardening", + "title": "Windows windowsHide spawns (#1835) + .gstack ACL inheritance repair (#1605) (2aa255b7, absorbed)", + "kind": "security", + "fork_refs": [ + "2aa255b7" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s5-beta-windows-spawn-dacl-hardening] ABSORBED (2be6c06b wave 2 and later): browse/src/file-permissions.ts:15-32 icacls inheritance break + explicit grant; windowsHide across browse/src spawns (config.ts:35,90,165; browse-client.ts:106; meta-commands.ts:780,844).\n[s4-fleet-windows-hardening] ABSORB" + ], + "notes": "" + }, + { + "id": "gap1-s10-browsewave-playwright-bump-headed-executable-pin", + "title": "Playwright ^1.60 bump (#1565/#1703, absorbed)", + "kind": "fix", + "fork_refs": [ + "d947d2e1", + "de47bf4f", + "upstream PR #1565 @stevenbarragan (OPEN)", + "6a614f8a" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[gap1-s10-browsewave-playwright-bump-headed-executable-pin] ABSORBED: /home/vercel-sandbox/gstack/package.json:62 `\"playwright\": \"^1.62.1\"` (with patchedDependencies at :90); browse/src/browser-manager.ts:717 `const chromePath = executablePath || chromium.executablePath()` and :757 explicit executab" + ], + "notes": "" + }, + { + "id": "s7-docs-method-platform-bakeoff", + "title": "Model-benchmark lib / Braintrust rewrite / eval-platform bakeoff (NOT_APPLICABLE direction decision)", + "kind": "tooling", + "fork_refs": [ + "evals/platform-bakeoff/README.md", + "evals/platform-bakeoff/platform_bakeoff.py", + "evals/platform-bakeoff/results/REAL-BAKEOFF.md", + "ce8088d5", + "36f972f2", + "7f4574e1", + "0645f52f", + "b6572ebb" + ], + "final": "NOT_APPLICABLE", + "basis": "cross-reference", + "evidence": [ + "grep -rli 'braintrust|langfuse|deepeval|autoevals|bake-off|bakeoff' across upstream .ts/.md/.json/.py (excluding node_modules/dist) \u2192 only scripts/brain-cache-spec.ts (BrainTrustPolicy = gbrain trust policy, unrelated)", + "ls lib/model-benchmark evals \u2192 both missing; bin/gstack-model-benchmark:1-40 imports test/helpers/benchmark-runner.ts + providers/{claude,gpt,gemini}; `--judge` = Anthropic SDK judge (test/helpers/benchmark-judge.ts)", + "grep -i 'rubric|required.term|requiredTerm|upload|writeReceipt|fetch(' bin/gstack-model-benchmark test/helpers/benchmark-runner.ts test/helpers/benchmark-judge.ts \u2192 0 (no deterministic rubric scorer, no upload sink)" + ], + "notes": "The fork's arc (b6572ebb in-house runner/judge/pricing \u2192 36f972f2/7f4574e1 Braintrust owns scoring \u2192 0645f52f consent gate \u2192 ce8088d5 Python bake-off) ends in a recommendation to stay provider-neutral" + }, + { + "id": "s4-fleet-runtime-cli-main-guard", + "title": "runtime/cli.js direct-execution main() guard (fork runtime only)", + "kind": "fix", + "fork_refs": [ + "020b84a2", + "5cc8ba1c" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "[s4-fleet-runtime-cli-main-guard] NOT_APPLICABLE: no runtime/ directory upstream; gen-skill-docs main() guard noted in CLAUDE.md.\n[s6-branches-runtime-cli-direct-entry-fix] ls /home/vercel-sandbox/gstack \u2192 no runtime/ directory." + ], + "notes": "" + }, + { + "id": "s5-beta-ios-stateserver-loopback-bind", + "title": "ios-qa StateServer binds loopback only (d22c034e, absorbed)", + "kind": "security", + "fork_refs": [ + "d22c034e" + ], + "final": "ABSORBED", + "basis": "cross-reference (skeptic confirmed)", + "evidence": [ + "ios-qa/templates/StateServer.swift.template:154-167 \u2014 IPv4 listener uses params.requiredLocalEndpoint = 127.0.0.1 (`NWListener(using: params)`), IPv6 keeps the wildcard port bind; landed in 2be6c06b v1.65.0.0 (fork port wave 2), diff shows the identical switch/case as fork d22c034e.", + "ios-qa/templates/StateServer.swift.template:176-184 \u2014 newConnectionHandler gates every connection through isLoopbackPeer(); :198-215 accepts loopback or RFC 4193 ULA fc00::/7 (CoreDevice tunnel) and cancels everything else.", + "ios-qa/templates/StateServer.swift.template:4 ('Loopback-only'), :32-33 (dual-stack listeners; 'The fork's single-listener IPv6-only binding was caught in eng + outside-voice review as incomplete')." + ], + "notes": "The fork's bug (wildcard IPv4 bind with dead loopback config) does not exist at upstream HEAD; the fix landed with wave 2 (v1.65.0.0). Only a comment/dead-code tidy remains \u2014 fold into any future ios-" + }, + { + "id": "gap5-hygiene-beta-first-measurement-recalibration", + "title": "2026-08-09 eval timeout recalibration / auq-format-gate demotion (dispatcher-surface specific)", + "kind": "test-infra", + "fork_refs": [ + "35a39e58", + "21bb1dd7" + ], + "final": "NOT_APPLICABLE", + "basis": "cross-reference", + "evidence": [ + "test/helpers/eval-budgets.ts:2-15 \u2014 'Timeout policy for paid tests \u2014 five tiers instead of hand-tuned sprawl\u2026 46\u00d7300s, 46\u00d7120s, 44\u00d7360s\u2026 hand-ratcheted per test'; :19 JUDGE_MS=120_000, :22 CAPTURE_MS=300_000, :25 CAPTURE_LONG_MS=600_000, :28 PTY_MS=900_000, :35 PTY_LONG_MS=1_200_000", + "test/eval-budgets-policy.test.ts:1-14 \u2014 FIT invariant (every tier fits the sharded runner wall minus 120s overhead) + RATCHET invariant ('raw numeric timeout literals in paid test files only shrink\u2026 a literal is legal only with justification, and the count is pinned')", + "CHANGELOG.md:311 (v1.74.0.0, 2026-08-29) '| Hand-tuned paid timeout literals | 395 | 97 (46 justified) | 5 tiers |', :332 (seven 28-min timeouts inside 25-min jobs trimmed; eval-budgets fit test), :339 ('298 paid timeout literals swept onto five named tiers\u2026 round-up only')" + ], + "notes": "Resolves the earlier UNKNOWN: the fork's premises (dispatcher-tree turn counts; 'measured the REAL 2.0 surface for the first time'; no green baseline for auq-format-gate) are all specific to the GStac" + }, + { + "id": "gap5-hygiene-windows-setup-e2e-runtime-lane", + "title": "windows-setup-e2e.yml rewrite around GStack 2 runtime installer", + "kind": "ci", + "fork_refs": [ + "git diff upstream/main origin/main -- .github/workflows/windows-setup-e2e.yml (origin/main 0aca1f77)", + "f14445bb" + ], + "final": "NOT_APPLICABLE", + "basis": "cross-reference", + "evidence": [ + "/home/vercel-sandbox/gstack/.github/workflows/windows-setup-e2e.yml:72-113 \u2014 lane is `bun run build` (GSTACK_SKIP_PLAYWRIGHT=1) + .exe presence + find-browse + gstack-paths checks; it never runs ./setup, so there is no state root to isolate", + "/home/vercel-sandbox/gstack/.github/workflows/windows-setup-e2e.yml:28-36 \u2014 PR-number concurrency key with the explicit comment that head_ref (the fork's shape) collides across forks; `permissions: contents: read` (secretless lane)", + "/home/vercel-sandbox/gstack/setup:18-42 \u2014 usage block lists --host/--model/--prefix/--no-prefix/--team/--no-team/-q/-h only; `grep -nE 'dry-run|DRY_RUN|dry_run|--preview' setup` \u2192 0 hits (no preview mode to assert non-mutation against)" + ], + "notes": "Fork commit f14445bb (2026-07-20, on fork main) is the 'componentize GStack 2 runtime' wave; the workflow rewrite is a consequence of that architecture. Upstream is not adopting the runtime, so the la" + }, + { + "id": "gap2-skillwave-autoplan-tasks-aggregator-jq-bind-2021", + "title": "autoplan tasks aggregator jq .commit bind (#2021/#2018, absorbed)", + "kind": "fix", + "fork_refs": [ + "703e2027, a1997cbc (0xDevNinja)", + "upstream PR #2021 OPEN (0xDevNinja); issue #2018 CLOSED 2026-08-15", + "fork PR #8 (377b0747)", + "upstream issue #2021 (OPEN)", + "upstream issue #2018" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[gap2-skillwave-autoplan-tasks-aggregator-jq-bind-2021] ABSORBED: scripts/resolvers/tasks-section.ts:123-128 ('.commit must be bound BEFORE piping to the split commit array' + `.commit as $c | select(...)`); test/tasks-section-jq.test.ts:1-18 regression pin for #2018; CHANGELOG.md:1233,1306.\n[s1-pre" + ], + "notes": "" + }, + { + "id": "gap2-skillwave-diff-dep-bump-1599", + "title": "diff ^7 \u2192 ^9 GHSA bump (#1599, absorbed)", + "kind": "security", + "fork_refs": [ + "ac2c88aa (genisis0x)", + "upstream PR #1599 OPEN (genisis0x); issue #1588 CLOSED 2026-08-15", + "d0fd2c0f" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[gap2-skillwave-diff-dep-bump-1599] ABSORBED: package.json:59 `\"diff\": \"^9.0.0\"`; CHANGELOG.md:1613 'diff 9.0.0 (GHSA-73rr-hh4g-fpgx, @genisis0x)'.\n[s1-prewave-diff-pkg-ghsa-bump] ABSORBED: package.json:59 `\"diff\": \"^9.0.0\"`; bun.lock resolves `diff@9.0.0`." + ], + "notes": "" + }, + { + "id": "gap2-skillwave-extension-getport-token-withhold-1822", + "title": "extension getPort withholds token from non-extension senders (#1822, absorbed via sender-auth.js)", + "kind": "security", + "fork_refs": [ + "2e7fcd73 (Mike Ilog / Mike-E-Log)", + "c6b466e1 (t, test)", + "upstream PR #1822 OPEN (Mike-E-Log)", + "0af2add8", + "upstream PR #1822 (OPEN)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[gap2-skillwave-extension-getport-token-withhold-1822] ABSORBED: extension/sender-auth.js exists; extension/background.js:317-328 'Privileged types ... are for this extension's own pages only ... gstackSenderAuth.denialFor(msg.type, sender, chrome.runtime.id)' before the getPort handler at :330.\n[s1" + ], + "notes": "" + }, + { + "id": "s2-runtime-dependency-review-osv-workflows", + "title": "dependency-review.yml + osv-scanner.yml CI gates (#2038, absorbed)", + "kind": "ci", + "fork_refs": [ + "09492d34", + "6fe51219 (Jayesh Betala / jbetala7)", + "964602a3 (Sinabina, fork-only removal)", + "upstream PR #2038 OPEN (jbetala7)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s2-runtime-dependency-review-osv-workflows] ABSORBED in wave 2 (v1.65.0.0 'supply-chain CI'): upstream .github/workflows/dependency-review.yml and osv-scanner.yml exist (both listed in .github/workflows; osv-scanner.yml has 1 SHA-pinned ref, dependency-review.yml 2), plus test/osv-config-wiring.tes" + ], + "notes": "" + }, + { + "id": "s2-runtime-release-pipeline-windows-archive", + "title": "GStack 2 runtime packaging, attestation, release lane and npm/CI plumbing (NOT_APPLICABLE cluster)", + "kind": "ci", + "fork_refs": [ + "dbf94804", + "b0047cc5", + "75b35766", + "f7d44a4e", + "bec9b9be", + ".github/workflows/release-artifacts.yml", + "docs/gstack-2/RELEASE-INTEGRITY.md", + ".github/scripts/create-runtime-release-manifest.mjs", + ".github/scripts/stage-runtime-components.mjs", + "f9afa73d", + "c1e45019", + "2f19f0b5", + "f14445bb", + "d6ef673e", + "b6572ebb", + "31ae801c" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "[s2-runtime-release-pipeline-windows-archive] Upstream has no release-artifacts workflow, cosign, or runtime bootstrap; windows-free-tests.yml:135 uses actions/upload-artifact rather than tar.\n[s7-docs-method-release-artifacts-attestation] NOT CHECKED beyond confirming absence: grep -i 'cosign|sigst" + ], + "notes": "" + }, + { + "id": "s2-runtime-execution-result-contract", + "title": "GStack 2 runtime internals: execution-result envelope, locks, deadlines, identity plumbing (NOT_APPLICABLE cluster)", + "kind": "methodology", + "fork_refs": [ + "9b5ae407", + "9b3188e7", + "9bb382f2", + "b6e4ad5a", + "b6572ebb", + "f14445bb", + "d6ef673e", + "a84a6e23", + "e1cd3096", + "b79f41ee", + "231fb9d7", + "f19d6cda", + "a9399ad3", + "682f6d03", + "d7908b25", + "b85497d7" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "[s2-runtime-execution-result-contract] grep schemaVersion/EXECUTION_RESULT/'execution result' across lib/*.ts bin/*.ts scripts/resolvers/*.ts returns nothing; upstream has no shared CLI result envelope to attach this to.\n[s2-runtime-runcommand-deadline-kill-grace] Upstream has no runtime/; grep Abor" + ], + "notes": "" + }, + { + "id": "s9-skills-dispatcher-header-and-alias-tables", + "title": "GStack 2 six-dispatcher architecture scaffolding: headers, alias/compat tables, migration map, runtime-absent probe, parity fixtures (NOT_APPLICABLE cluster)", + "kind": "judgment-rule", + "fork_refs": [ + "b6572ebb", + "682f6d03", + "a9399ad3", + "docs/gstack-2/SKILL-MIGRATION.md", + "1b38be6a", + "44221ac7", + "ac06a37d", + "3ebde802", + "e6f602bc", + "8502961b", + "fork PRs #18, #19, #21, #25, #26, #30 (regression fixtures)" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "[s9-skills-dispatcher-header-and-alias-tables] Upstream architecture unchanged (44 skill dirs, scripts/gen-skill-docs.ts, _gstack-command router); TODOS.md:460-505 and PACING_UPDATES_V0.md:97-110 record what upstream chose to fold in from the fork instead. grep 'Skipped modules|Context\\.dev|Global C" + ], + "notes": "" + }, + { + "id": "s2-runtime-shard-isolation-heuristics", + "title": "GStack 2 dispatcher-surface test-runner and tier adjustments (NOT_APPLICABLE cluster)", + "kind": "test-infra", + "fork_refs": [ + "d7357c28", + "37144e8b", + "8fd8bf43", + "37042b68", + "9504b703", + "a3ed7a68", + "44221ac7" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "[s2-runtime-shard-isolation-heuristics] Upstream scripts/test-free-shards.ts has none of containsScheduledProcessExitZero/planBoundedFreeTestShards (grep empty), scripts/test-free-strict.ts does not exist; test/no-suicide-exit.test.ts fails the suite on any scheduled process.exit in a test file, and" + ], + "notes": "" + }, + { + "id": "s6-branches-windows-isolated-gates-harness", + "title": "Windows isolated cloud gates harness for specific upstream PR heads (NOT_APPLICABLE cluster)", + "kind": "ci", + "fork_refs": [ + "fe868994", + "9c1ddd79", + "60863a71", + "f915a246", + "808bb546", + "codex/windows-gates-20260715-central" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "[s6-branches-windows-isolated-gates-harness] Upstream .github/workflows has windows-free-tests.yml and windows-setup-e2e.yml (curated subset + fresh-install gate), no per-PR isolated harness. `gh pr view` 1743/1981/2260/2243/2245/2246/2247 \u2192 all OPEN, unmerged. `gh run list -R time-attack/gstack --b" + ], + "notes": "" + }, + { + "id": "gap3-design-daemon-reset-cancels-shutdown-timers", + "title": "design daemon resetForTest clears shutdown/exit timers", + "kind": "test-infra", + "fork_refs": [ + "467161bd", + "b6572ebb" + ], + "final": "SUPERSEDED", + "basis": "cross-reference", + "evidence": [ + "design/src/daemon.ts:206 `setTimeout(() => process.exit(exitCode), 50);` \u2014 untracked handle (fork stores it in exitTimer)", + "design/src/daemon.ts:489 `setTimeout(() => gracefulShutdown(0), 50);` \u2014 untracked handle (fork stores it in shutdownTimer)", + "design/src/daemon.ts:570-581 __testInternals__.resetForTest clears boards/boardMutex/lastMeaningfulActivity/idleExtensions/shuttingDown only; grep 'clearTimeout|shutdownTimer|exitTimer|shutdownRequestTimer' design/src/daemon.ts -> 0" + ], + "notes": "The fork's stated failure (an in-process /shutdown test process.exit(0)s the bun runner mid-suite, later files silently skipped, CI green) can no longer occur upstream: the one such test stubs exit, t" + }, + { + "id": "s2-runtime-handoff-singleton-lock-cleanup", + "title": "Handoff uses resolveChromiumProfile() + SingletonLock cleanup (absorbed)", + "kind": "fix", + "fork_refs": [ + "b6572ebb", + "7b3f391b" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s2-runtime-handoff-singleton-lock-cleanup] ABSORBED: upstream browse/src/browser-manager.ts:1796 `const userDataDir = resolveChromiumProfile();` and :1798 `cleanSingletonLocks(userDataDir);` inside handoff().\n[s1-prewave-handoff-resolve-chromium-profile] ABSORBED: browse/src/browser-manager.ts:663 " + ], + "notes": "" + }, + { + "id": "s2-runtime-stop-ack-before-shutdown", + "title": "browse stop/restart acknowledge before shutdown (#2020, absorbed)", + "kind": "fix", + "fork_refs": [ + "b6572ebb", + "9919c4cd", + "d947d2e1", + "d8c64ee2", + "upstream PR #2020 @devkd111 (OPEN)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s2-runtime-stop-ack-before-shutdown] ABSORBED (wave 2 'browse stop fix'): upstream browse/src/meta-commands.ts:424-436 contains the identical comment and deferred shutdown; browse/test/stop-ack-before-shutdown.test.ts is byte-identical to the fork's.\n[gap1-s10-browsewave-daemon-stop-restart-defer] " + ], + "notes": "" + }, + { + "id": "s1-prewave-live-daemon-preservation", + "title": "Never kill a live-but-busy browse daemon / loaded-machine lifecycle (#2219/#1732/#1847, absorbed)", + "kind": "fix", + "fork_refs": [ + "d8a7d014", + "time-attack/2219", + "upstream PR #2231", + "upstream issue #2219", + "d947d2e1", + "94deed4b", + "efebbff9", + "83f872cc", + "upstream PR #1732 @mplatts (OPEN)", + "upstream PR #1847 @harjothkhara (CLOSED)", + "upstream PR #2231 @time-attack (OPEN)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s1-prewave-live-daemon-preservation] ABSORBED: browse/src/cli.ts:684-686 daemonPidAlive + probeHealthWithBackoff, :733-745 '#2219 IRON RULE' + forceRestart, :871-895 decideDaemonRestart/report-busy; issue #2219 CLOSED 2026-08-17.\n[gap1-s10-browsewave-loaded-machine-daemon-lifecycle] ABSORBED: brows" + ], + "notes": "" + }, + { + "id": "gap4-s13-ios-xcuitest-executor-planner", + "title": "ios-qa/executor: IOSQAFlow \u2192 XCUITest xcodebuild argv planner (466b1ec7)", + "kind": "tooling", + "fork_refs": [ + "466b1ec7", + "time-attack/gstack PR #14" + ], + "final": "NOT_APPLICABLE", + "basis": "cross-reference", + "evidence": [ + "ls /home/vercel-sandbox/gstack/ios-qa/ \u2192 SKILL.md, SKILL.md.tmpl, daemon, docs, scripts, templates (no executor/)", + "grep -rnE 'IOSQAFlow|selectorCandidates|xcuitest-plan|GSTACK_IOS_QA_FLOW|buildXCUITestPlan|XCUITestRunnerConfig' across upstream \u2192 0", + "ls test/fixtures/ios-qa/FixtureApp/Tests \u2192 DebugBridgeCoreTests only (no AdaptiveFixtureUITests / GStackFlowRunnerUITests.swift)" + ], + "notes": "Upstream still has no flow\u2192xcodebuild planner, but the planner only makes sense as the argv side of an XCUITest runner target, and upstream's ios-qa architecture (SKILL.md.tmpl:41; CHANGELOG 'no XCTes" + }, + { + "id": "s1-prewave-sidebar-chat-era-test-deletion", + "title": "Delete chat-queue-era sidebar tests, re-pin surviving invariants (#1984, absorbed)", + "kind": "test-infra", + "fork_refs": [ + "514e5294", + "upstream PR #2230", + "d947d2e1", + "045bdaa5", + "upstream PR #1984 @maxpetrusenkoagent (OPEN)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "[s1-prewave-sidebar-chat-era-test-deletion] ABSORBED: `ls browse/test/security-sidepanel-dom.test.ts browse/test/sidebar-integration.test.ts` \u2192 No such file; browse/test/sidebar-ux.test.ts present.\n[gap1-s10-browsewave-sidebar-ux-test-refresh] NOT_APPLICABLE: upstream browse/test/sidebar-ux.test.ts " + ], + "notes": "" + }, + { + "id": "s3-judgment-design-docs-repo-local", + "title": "Design docs written to docs/designs/.md in the repo; reviews prefer the repo-local copy (#703)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #18 (merge c3dbec14, v1.61.0.0)", + "upstream issue #703 (CLOSED 2026-08-15)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED (wave 2): office-hours/sections/design-and-handoff.md.tmpl:20-35 'Repo copy (dual-write, #703 + #2000)... ALSO write the doc to docs/designs/{topic-slug}.md' with a redaction scan-at-sink the fork did not have; scripts/resolvers/design-doc-discovery.ts:6-7,32-39 'Repo-local docs win when at" + ], + "notes": "" + }, + { + "id": "s3-judgment-design-doc-concision", + "title": "Design doc is a decision record: bullets, one line per ruled-out approach, omit settled/empty sections (#2000)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #19 (merge 7d5c110b, branch time-attack/designdocs, v1.61.1.0)", + "upstream issue #2000 (CLOSED 2026-08-15)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED: office-hours/sections/design-and-handoff.md.tmpl:37-42 'Decision-record concision (#2000). The doc is a decision record, not a transcript: one bullet per decision with its why; an approach the user ruled out DURING the session gets one line (name + rejection reason)... No page cap'; CHANGE" + ], + "notes": "" + }, + { + "id": "s3-judgment-founder-resources-optout", + "title": "Founder resources honor a persistent 'Never show me these again' opt-out (#538)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #26 (merge a1afacea, branch time-attack/motiviation, v1.61.2.0)", + "upstream issue #538 (CLOSED 2026-08-15)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED with an upstream improvement: office-hours/sections/design-and-handoff.md.tmpl:343-366 ('Standing opt-out check (#538) \u2014 run FIRST... skip this entire section silently... VERIFY the write... before promising anything'); bin/gstack-config:164 default true, :400-401 validation; test/founder-r" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-adapter-before-branch-gate", + "title": "Store distribution is not repository landing: Apple adapter loads before the branch gate; never abort over branch topology (#41)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #41 (merge a28f0b59, branch time-attack/appleship-mode-routing, v1.64.11.0)", + "fork PR #29 step 10" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED: ship/SKILL.md.tmpl:80-90 'Step 0.9: Apple target detection \u2014 Shipping to the App Store is not landing a PR... STOP and Read ship/sections/apple-release.md FIRST \u2014 before the branch gate and any preflight... The branch gate and repository-landing pipeline below apply ONLY to repository-land" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-two-permitted-interactions", + "title": "The Apple journey permits exactly two interactions: the authorization/sign-in moment and the missing-assets question (#35, #36, #37)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #35 (merge 65b65013, v1.64.6.0)", + "fork PR #36 (merge fdb68f88, v1.64.7.0)", + "fork PR #37 (merge c6f8f1f1, v1.64.8.0)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED verbatim: ship/sections/apple-release.md.tmpl 'The one authorization moment' para ('The whole journey permits exactly two interactions, and no others... Auth menus, tool-choice questions, plan confirmations, and step-by-step narration requests are contract violations.') and para 2 ('One too" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-sticky-assets-decision-store", + "title": "Store-assets question asked once per app, ever \u2014 decision store checked before asking, persisted after (#38)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #38 (merge 7d4529d3, branch time-attack/appleship-sticky-assets, v1.64.9.0)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED verbatim: ship/sections/apple-release.md.tmpl 'Store assets' para ('Once per app, EVER: before asking, check the decision store (`bin/gstack-decision-search --scope repo --query \"store assets\"`)... persist it (`~/.claude/skills/gstack/bin/gstack-decision-log` with scope `repo`)'). Header co" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-deck-editor-no-key-live-check", + "title": "Marketing screenshots never require an API key; the free deck editor leads; options built from a LIVE installed-skill check (#39, #42)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #39 (merge bdcd6e13, v1.64.10.0)", + "fork PR #42 (merge 9c75ec20, v1.64.12.0)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED verbatim: ship/sections/apple-release.md.tmpl 'Store assets' section \u2014 bullet 'Marketing screenshots, free and local, no API key... never claim screenshots need an API key while this skill is installed' and closing para 'Build this question's options from a LIVE check of installed skills at" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-session-mints-upload-key", + "title": "Session-minted App Store Connect upload key; nobody types an app-specific password (#43, #48)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #43 (merge 2de5c36f, v1.64.13.0)", + "fork PR #48 (merge ba82dc01, branch time-attack/upload-key-mint, v1.64.18.0)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED and hardened: ship/sections/apple-release.md.tmpl step 4 carries the full mint procedure verbatim plus upstream's least-privilege change \u2014 `allAppsVisible:false` with an explicit app-scoped `apps` relationship instead of the fork's `allAppsVisible:true`, and PATCH re-association for a secon" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-credential-browser-ban-and-escalation-ladder", + "title": "Browser-driven credential creation banned; auth failures escalate mint \u2192 re-sign-in \u2192 self-service ASP only on permissions refusal (#44, #45)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #44 (merge 9b5ad8a2, v1.64.14.0)", + "fork PR #45 (merge fe194ef0, v1.64.15.0)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED and triple-pinned: ship/sections/apple-release.md.tmpl step 5 verbatim ('NEVER offer or recommend a browser drive to create credentials \u2014 no agentic browser of any kind, for any password, key, or token, under any framing'); scripts/resolvers/third-party-actions.ts:39 'Creating Apple credent" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-classify-errors-before-credentials", + "title": "Classify errors before touching credentials (metadata vs auth); adapter overrides the third-party browser offer for the whole Apple journey (#46)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #46 (merge 05ef17b8, branch time-attack/appleship-cli-only, v1.64.16.0)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED verbatim: ship/sections/apple-release.md.tmpl step 4 ('CLASSIFY the error before touching credentials... lootBox, ageAssurance, parentalControls, messagingAndChat') and step 5 ('this adapter OVERRIDES the Third-Party Web Actions contract (earlier in this skill)... the ONLY browser use this " + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-pricing-in-authorization", + "title": "Pricing (free/paid + price) folded into the authorization moment, once per app, decision-store persisted; storefront pricing via appPriceSchedules because fastlane price_tier is broken (#49)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #49 (merge 138e7138, branch time-attack/pricing-question, v1.64.19.0)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED verbatim: ship/sections/apple-release.md.tmpl 'The one authorization moment' ('Pricing belongs to this same breath, once per app EVER... a free launch cannot be un-launched') and 'Storefront completion' ('`POST /v1/appPriceSchedules`... fastlane's `price_tier` option is broken against the c" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-non-mac-macos-ci-runner", + "title": "Non-macOS hosts: honest Mac-required split \u2014 build/sign/upload legs route through a macOS CI runner with the minted key as a secret; API legs stay local (#50)", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #50 (merge a6bc2de3, commit df7f296d, branch time-attack/windows-ci-lane, v1.64.20.0)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED verbatim: ship/sections/apple-release.md.tmpl para 3 ('A Mac is required only for the build legs... route exactly those legs through a macOS CI runner (a GitHub Actions `macos` runner...)... Never claim the whole release is impossible off a Mac, and never pretend the build leg is possible t" + ], + "notes": "" + }, + { + "id": "s3-judgment-apple-intermediate-browser-drive-and-asc-cli", + "title": "Intermediate Apple-adapter states: agentic-browser drive of App Store Connect forms (#31), asc CLI app-record creation (#32), .p8-first auth (#29/#33) \u2014 superseded within the fork", + "kind": "judgment-rule", + "fork_refs": [ + "fork PR #31 (v1.64.2.0)", + "fork PR #32 (v1.64.3.0)", + "fork PR #33 (merge efa24a1e, v1.64.4.0)", + "fork PR #34 (merge 1a67db98, v1.64.5.0)" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "NOT_APPLICABLE by design: upstream ship/sections/apple-release.md.tmpl encodes only the final state \u2014 fastlane `produce` for the app record ('never call the app record a manual gate'), no `asc`, browser only for 'the paid Apple Developer Program membership purchase itself... and, for PAID apps only," + ], + "notes": "" + }, + { + "id": "s1-prewave-health-token-removal", + "title": "/health status-only, token via pinned-origin bootstrap", + "kind": "security", + "fork_refs": [ + "7b3f391b", + "upstream PR #2226" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED: browse/src/server.ts:1870-1915 (`POST /extension-token \u2014 pinned-origin token bootstrap`, 'Health check ... NEVER carries a token in any mode'); extension/manifest.json:6 `key`; extension/background.js:41-56." + ], + "notes": "" + }, + { + "id": "s1-prewave-extension-storage-session-auth", + "title": "Provision extension auth via chrome.storage.session (TRUSTED_CONTEXTS) instead of HTTP", + "kind": "security", + "fork_refs": [ + "7b3f391b", + "upstream PR #2226" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "NOT_APPLICABLE (alternative design shipped): browse/src/server.ts:1885-1908; extension/sender-auth.js:29-60 (PRIVILEGED_TYPES incl. getPort/getToken, denialFor); browse/test/extension-token.test.ts, server-auth.test.ts, dual-listener.test.ts reference /extension-token; grep storage.session/TRUSTED_C" + ], + "notes": "" + }, + { + "id": "s1-prewave-ws-subprotocol-duplicate-header", + "title": "Fix WS upgrade 1006 from duplicated Sec-WebSocket-Protocol echo", + "kind": "fix", + "fork_refs": [ + "7b3f391b", + "upstream PR #2226" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED: browse/src/terminal-agent.ts:640 'No explicit Sec-WebSocket-Protocol echo: Bun >= 1.3 auto-echoes'; extension/sidepanel-terminal.js:308,585,807 still use `gstack-pty.${token}` (upstream's chosen format)." + ], + "notes": "" + }, + { + "id": "s1-prewave-artifacts-init-push-protocol", + "title": "gstack-artifacts-init honors configured push protocol / preserves explicit URLs (--push-protocol)", + "kind": "fix", + "fork_refs": [ + "d6b300cf", + "test-issue-1348", + "upstream PR #2225", + "fork PR #7 commit f036446f" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED: bin/gstack-artifacts-init:11 usage, :67-82 PUSH_PROTOCOL/REMOTE_SOURCE, :218-256 resolution incl. `preserve` and `gh config get git_protocol`; test/gstack-artifacts-init.test.ts:277-380 (same test titles as fork incl. '--push-protocol overrides the inferred protocol')." + ], + "notes": "" + }, + { + "id": "s1-prewave-basic-ftp-pin", + "title": "Pin basic-ftp to 5.3.1 (four HIGH advisories)", + "kind": "security", + "fork_refs": [ + "fork PR #2 (5ca87456)", + "upstream PR #2227" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED: `git log -S basic-ftp -- package.json` shows f55e2709 'override basic-ftp to 5.3.1' then ae8914af (v1.67.0.0); grep basic-ftp in package.json/bun.lock now empty (dependency gone)." + ], + "notes": "" + }, + { + "id": "s1-prewave-catalog-lead-embedded-periods", + "title": "gen-skill-docs catalog lead: don't split on embedded periods (filenames, URLs, versions)", + "kind": "fix", + "fork_refs": [ + "fork PR #3 (2f6c3e96)", + "upstream issue #2171 (OPEN)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED: scripts/gen-skill-docs.ts:344-353 regex `^((?:[^.!?]|[.!?](?!\\s|$))*[.!?])(?:\\s|$)` with comment naming 'TODOS.md', URLs, 'v1.45.0.0'; test/catalog-trim.test.ts:132-145 regression tests for DESIGN.md/v1.45.0.0/'TODOS.md backlog'." + ], + "notes": "" + }, + { + "id": "s1-prewave-hermes-namespaced-frontmatter", + "title": "Hermes-generated frontmatter `name:` matches the namespaced directory", + "kind": "fix", + "fork_refs": [ + "fork PR #4 (bb68b1c2)" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "NOT_APPLICABLE: hosts/hermes.ts:32-37 'No full install arm \u2014 users can hand-copy the instruction-only digest', instructionTier rulesFile; grep nameTransform|emittedName|external-skill-name in scripts/gen-skill-docs.ts and scripts/host-config.ts empty; .gitignore:23 `.hermes/`." + ], + "notes": "" + }, + { + "id": "s1-prewave-codex-web-search-flag", + "title": "Drop deprecated `--enable web_search_cached` from every codex exec/review invocation", + "kind": "fix", + "fork_refs": [ + "fork PR #5 (5f9d132e)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED (different judgment): scripts/resolvers/constants.ts:50-65 CODEX_WEB_SEARCH_FLAG with comment 'codex >=0.144 deprecated the legacy --enable-based web_search_cached'; codex/SKILL.md.tmpl:245 documents the -c override." + ], + "notes": "" + }, + { + "id": "s1-prewave-gstack-paths-get", + "title": "`gstack-paths --get ` scalar output; codex skill avoids eval", + "kind": "tooling", + "fork_refs": [ + "fork PR #5 (deed6e85)" + ], + "final": "NOT_APPLICABLE", + "basis": "pre-classified from sweep (fork-runtime-only)", + "evidence": [ + "NOT_APPLICABLE: bin/gstack-paths:16 'values are emitted shell-quoted (printf %q) so eval round-trips them'; codex/SKILL.md.tmpl:130 still `eval \"$(~/.claude/skills/gstack/bin/gstack-paths)\"`; no `--get` (grep empty)." + ], + "notes": "" + }, + { + "id": "s1-prewave-design-variant-count-validation", + "title": "design `--count` rejects non-integer/zero/negative instead of silently generating nothing", + "kind": "fix", + "fork_refs": [ + "fork PR #6 (03c0d24d, 9a04c05f)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED: design/src/flag-utils.ts:1-45 (`--count abc \u2192 for (i < NaN) never runs \u2192 ZERO variants, exit 0`, parseIntFlag; landed 94993f74 v1.61.0.0)." + ], + "notes": "" + }, + { + "id": "s1-prewave-design-timeout-message", + "title": "Design image timeout message matches the real 240s timer", + "kind": "fix", + "fork_refs": [ + "fork PR #6 (8ae097d9, 6bdfe770)" + ], + "final": "ABSORBED", + "basis": "absorbed per sweep evidence (not re-verified)", + "evidence": [ + "ABSORBED: design/src/variants.ts:68 `240_000` and :135 `\"Timeout (240s)\"`." + ], + "notes": "" + }, + { + "id": "s1-prewave-make-pdf-invisible-preamble", + "title": "make-pdf: a leading