Repository navigation
Conversation
…files
The auto-updater fast-forwards with `merge --ff-only --autostash`. When
the carried-over local edits conflict with the incoming revision, git
2.56 still exits 0 from that merge, pops the edits with conflict markers
into the live checkout, and keeps the stash entry. The recovery only ran
on a non-zero exit with "autostash" in stderr, so on git 2.56 it never
ran: the updater logged UPDATED and ran setup over a half-merged tree,
in the directory Claude Code runs hooks from. When it did run, it had two
more problems:
- `git checkout -- .` refuses unmerged paths ("error: path 'f' is
unmerged", exit 1), so it left the markers in place anyway.
- `git stash drop` dropped stash@{0}, whatever that was. refs/stash is
shared with every worktree of the repository, and when
refs/stash.lock is held git cannot store the autostash at all, so the
top entry can be someone else's.
Now the merge is judged by `git ls-files -u`, whatever its exit code.
On unmerged files after the fast-forward, autostash_recover finds the
autostash commit by the id git prints ("cannot store <id>" first, then
"Created autostash: <id>", with the merge run under LC_ALL=C and
core.abbrev=40). A moved refs/stash is only the fallback. When that id
is not a stash entry, it is anchored under refs/gstack-autostash/<UTC>.
The tree is reset to the fast-forwarded revision, which the hook check
already passed, and the stash is kept. The log line is
AUTOSTASH_CONFLICT_RECOVERED tree_reset=1 kept_stash=<id> kept_ref=<ref>.
The update then continues, so the regenerable patches are re-rendered
and setup runs as for any other move. If the commit cannot be found,
nothing is reset: the markers stay for a person, the run logs
PULL_FAILED, and setup does not run.
Files that are already unmerged before the update, from someone's own
merge, rebase or stash pop, now hold the update with PULL_FAILED
"unmerged files ...". Without that check, the recovery would take them
for its own and reset them away.
core.abbrev=40, not "no": "no" is accepted from git 2.31.0 (its release
notes), and `merge --autostash` exists from 2.27.0, so "no" would fail
every update on git 2.27-2.30.
Files:
- bin/gstack-session-update: autostash_recover, the unmerged-files check
before the merge, the C-locale merge with its stdout kept for the id,
and removal of the old grep-and-drop block.
- test/session-update-autostash.test.ts: 7 cases in a new describe block.
A conflicting pop leaves no markers, keeps the edits and still runs
setup. A user's own stash entry survives. A held refs/stash.lock gets
the edits anchored. With a git whose merge prints nothing, a moved
refs/stash still finds the edits; with the lock held as well, nothing
is reset. Pre-existing unmerged files hold the checkout. A
non-conflicting edit rides over the update (control).
Tests, on upstream 15ebd85:
- 5 of the 7 new cases fail against the previous session-update; the
unmerged-files guard and the control pass on both.
- Two mutants are each caught by one new case: dropping the refs/stash
fallback, and resetting when the edits cannot be found.
- session-update-autostash, session-update-stages,
session-update-hook-hold and team-mode pass 63/0 with
`--timeout 60000`. At bun's 5 s default under heavy load (one
reading: load average 169), the three shared files timed out 8 tests
on this branch and 17 with the previous script.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An independent review of the autostash recovery (Codex gpt-6-astra, high effort, verdict BLOCK high=1 med=2) found three defects on its rare paths. All three are fixed here, each with a case that failed first. - [high] The refs/stash fallback could erase local edits. When git's messages were unavailable, recovery took any change to refs/stash as its own autostash. refs/stash is shared with every worktree: if a sibling pushed a stash while refs/stash.lock kept git from storing this one, recovery took the sibling's entry and reset the checkout, erasing edits nothing held. Now only git's own printed id counts. A missing id resets nothing: the markers stay, the run logs PULL_FAILED. Recovery needs git's messages, so when the temp file for them cannot be created the update is held before the merge (HELD temp-file-unavailable), not merged blind. - [med] A stop after the checkout moved lost the setup it owed. The failure recorded only the pull reason. Once the person resolved the conflict, the next check saw an unchanged HEAD and no pending stage, logged UP_TO_DATE, and never set up the new revision. Now a failure after HEAD moved records `setup` as a pending stage, retried at once. The unmerged-files check holds it until the conflict is resolved, and the pull reason is the stage's reason, so the session-start line says what to resolve. - [med] An unreadable index read as "no conflicts". Both `git ls-files -u` checks discarded the exit status, so a failed read after a conflicting pop (which git exits 0 for) let setup run over the markers. Now a failed read stops the update with PULL_FAILED "cannot read the index", before the merge or after it. Files: - bin/gstack-session-update: autostash_recover drops the refs/stash fallback (and STASH_BEFORE). The temp-file hold. The ls-files exit status checked at both points. The pending setup stage on a failure after HEAD moved. - test/session-update-autostash.test.ts: a shim(fx, name, tool, body) helper; the fallback case now expects a refusal; the refusal case runs on through resolution to setup; three new cases (unreadable index before and after the merge, no temp file for git's messages). Tests, on upstream 15ebd85: - The 5 changed or new cases failed on the previous commit, each on its defect: refs/stash trusted and recovered; UP_TO_DATE with no setup after resolution; UPDATED over an unreadable index, before and after the merge; a blind merge with no temp file. - Against upstream's own session-update, 8 of the block's 10 cases fail. - session-update-autostash, session-update-stages, session-update-hook-hold and team-mode pass 66/0 with `--timeout 90000`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lock heartbeat subshell left its sleep 300 child orphaned when the updater exited, so heartbeats accumulated across sessions. Track and reap the heartbeat job (with a job-table fallback for signals between fork and $!), give the subshell its own exit trap that kills and waits its sleeper, and map HUP/INT/TERM to exits so the EXIT trap always runs. Adds a fixture-driven regression test.
…eanup shape Intentional pin change: test/session-update-autostash.test.ts 'reclaim is TOCTOU-safe' and 'EXIT trap is ownership-checked' pinned the one-line EXIT trap and the 'while :; do sleep 300; ...' loop that #3191 (Jess Sullivan) replaces with cleanup_session_update and a 'sleep 300 & wait' heartbeat. The pins still assert the ownership-checked rm -rf, the EXIT trap, MYPID, and that the heartbeat is killed. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…ract hooks without tar (#3184) Item 2 (#3189, #3191 adopted in the previous commits) and item 3 of the fix-wave plan. bin/gstack-ff-update <dir> <rev> is now the only code that moves an install's tree. Inside an install-scoped lock (absolute git common dir, the updater's TTL and mv-aside reap, a heartbeat reaped on exit) it captures the working tree and the staged state through private indexes, carries edits that merge cleanly (as an autostash pop would), lets the regenerable SKILL.md/sections footprint take the target when it conflicts, and shelves any other conflicting edit, staged change or unmerged file into refs/gstack/rescue/<oid> (first parent the pre-update HEAD, second the staged state; compare-and-create; gstack rescue identity). The move is a two-way read-tree on the private index, so a late edit or an untracked file in the way refuses with autostash-unrecoverable and changes nothing. It sources nothing so the held-install recovery can fetch it from the new revision. gstack-session-update calls it instead of merge --autostash, holds a checkout with unmerged files (unmerged-files:), and names shelved edits at session start at most once a day with inspect, export-the-delta and delete commands. The incoming revision is extracted for the hook check through a private index and checkout-index with core.symlinks=false, never tar (MSYS tar cannot create connect-chrome before its target), and the held reason carries the failing command's flattened, control-free stderr. Held lines print before the stage line, one case per reason with its anchor, every path shell-quoted, no setup fix while unmerged files exist. docs/troubleshooting.md gains the held-install recovery forms (Git Bash, PowerShell 5.1, trial and go-back) and entries for each new line. design: kept_ref is refs/gstack/rescue/<oid>, refs/stash is never touched, the quiet-merge cases become a pre-existing-ref refusal and its recovery, pre-existing unmerged files are a HELD unmerged-files, and the git shim pins the real binary through git --exec-path. Intentional pin changes in existing tests: - test/session-update-stages.test.ts: printed paths are shell-quoted; the bun-too-old fix runs gstack-ff-update instead of git pull. - test/session-update-hook-hold.test.ts: the order pin looks for 'bash "$FF_UPDATE"' instead of 'merge --ff-only'; the hook-check-unavailable fix no longer recommends git pull --autostash; installedUpdater copies gstack-ff-update beside the updater. Reported by wengweiling (#3184). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…3189) Item 2 of the fix-wave plan, kept in its own commit so PR 1b can be rebased onto PR 1a (which edits only Step 4's Bun floor-check line). Step 4's `git pull --ff-only --autostash` had the same hole the auto-updater had: a conflicting autostash exits 0 and leaves conflict markers in the install, and the skill printed FF_OK. It now runs bin/gstack-ff-update . origin/main after the fetch and the floor check: exit 0 runs setup (a rescued line is relayed with its inspect, export and delete commands), exit 4 (not a fast-forward) is the only way into the gated reset fallback, and any other refusal stops with nothing changed. The footprint discard moves to the reset fallback, ahead of its stash, where the warning text already promised it. Intentional pin changes: - test/upgrade-template-pins.test.ts: the ff-before-reset pin looks for 'bash bin/gstack-ff-update . origin/main'. - test/gstack-upgrade-fresh-shell.test.ts: the C9 fence marker is 'bash bin/gstack-ff-update'; the gstack-like fixture ships the bin. - test/upgrade-setup-recovery.test.ts: the git-path fixtures ship gstack-ff-update (a stub where git itself is a stub). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…ssing (#3130) Item 4 of the fix-wave plan. Windows PowerShell 5.1 strips the double quotes inside native arguments, so the bash -c script setup.ps1 handed Git Bash arrived broken ("unexpected EOF while looking for matching"). The script now holds no double quote: `set -f; IFS=; mapfile -d '' -t argv < $1; rm -f -- $1; exec ./setup ${argv[@]}` (empty IFS and no globbing keep $1 and each argv entry whole). That form drops an empty argument, so setup.ps1 refuses one before anything runs (SETUP_PS1_EMPTY_ARGUMENT, with its troubleshooting entry). test/setup-ps1.test.ts runs the adversarial arguments under legacy passing (pwsh with $PSNativeCommandArgumentPassing = 'Legacy', and powershell.exe itself on Windows) with a space in TMPDIR, plus the empty-argument refusal. Intentional pin change: the pinned bash -c line in "passes arguments NUL-separated through a private file to a constant bash script" now names the quote-free script and asserts it holds no double quote. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…inks back to it A per-session CLAUDE_CONFIG_DIR whose skills symlink to ~/.claude/skills passed the split-brain guard, so its temporary path was written into the global settings.json and the hooks broke once the session directory was removed. Fall back to $HOME/.claude/skills/gstack whenever both spellings resolve to the same tree. Fixes #3180
…ck-memorable (#3180) bin/gstack-memorable carried a verbatim copy of setup's old canonical-root block, so `gstack-memorable enable` under a per-session CLAUDE_CONFIG_DIR still baked the session path into settings.json after #3181 fixed setup. The choice now lives in bin/gstack-claude-root.sh (gstack_canonical_claude_root), sourced by both. Intentional test changes: - test/setup-hook-canonical-paths.test.ts: #3181's source-slicing block test is rewritten to source the helper (with a spawnSync timeout, which the sliced version lacked and the tripwire flags); the "honors CLAUDE_CONFIG_DIR" pin moves from setup to the helper, and a new pin requires both callers to source it. Adds a no-$HOME-install case. - test/setup-hook-syntax-gate.test.ts: the hook-section slice starts at the new assignment and the harness sources the helper. - test/setup-claude-code-migration.test.ts, test/team-mode.test.ts: the partial-checkout fixtures copy the new helper setup sources. Reviewer test: gstack-memorable enable under a per-session config dir that links back to ~/.claude registers the $HOME path (fails on main). Builds on #3181 by NetrioGit. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
A 1.31 setup run from a checkout under ~/.cursor/skills linked unprefixed <skill>/ folders there, each holding a SKILL.md symlink into the checkout. Current setup links gstack-<skill>/ folders, and the retired-skill prune only looks at gstack-* names, so the old folders stayed: Cursor loaded autoplan, review and the rest twice, or kept dangling links once the checkout moved. Run the Claude flat-name cleanup on ~/.cursor/skills as well. Cursor installs only gstack-* names, so no prefix gate is needed, and the helper removes only entries proven to link into gstack (#2119), so user-owned folders stay. It runs after linking, so a run that fails before the new links exist keeps the old folders. Reported by @andrewkjankowski in #3084. Fixes #3133 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`./setup --host cursor` now runs the flat-name cleanup on ~/.cursor/skills as well as ~/.claude/skills, so the "cleaned up old entries" line says which directory the folders were removed from. Reviewer test for #3152 (szsunyuan): a real dir whose SKILL.md links into gstack loses only that link and keeps the user's file; SKILL.md links into a sibling repo named gstack-fork, a relative link, and a dangling link outside any gstack checkout are all kept; the printed line names the removed folders and the Cursor skills dir. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
… it is (#3131) A Bun installed with winget ships no `bunx`, so setup's `bunx playwright install chromium` exited 127 and was reported as "offline, proxy, or blocked download?". setup now runs `bun_cmd x playwright install chromium` through its existing bun resolver ($BUN_CMD), and bin/gstack-browser-ensure runs the bun it found on PATH with `x`. Exit 127 gets its own text naming the resolved bun, Bun from bun.sh, and the skip commands for Git Bash and PowerShell; other codes keep the network hint. New troubleshooting entry: #chromium-install-exit-127. Intentional test changes: - test/setup-playwright-best-effort.test.ts: the installer stub is now a `bun_cmd()` stub (was `bunx()`), its mark renamed bun-x-called and now records the args; the block under test calls bun_cmd. New winget-shaped cases (bun on PATH, no bunx anywhere) run setup's real bun_cmd. - test/gstack-browser-ensure.test.ts: the fixture is winget-shaped (the `bun` stub handles `x`, no `bunx` on PATH); the exit-7 pin now reads `<stub>/bun x playwright install chromium exited 7 (...)`. --help in gstack-browser-ensure prints the comment header only (3,24p). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…ecovery natively (#3130, #3184) windows-setup job: - setup.ps1 joins the path filter (with bin/gstack-ff-update, bin/gstack-claude-root.sh, bin/gstack-browser-ensure, docs/troubleshooting.md and test/setup-ps1.test.ts). - powershell.exe must be Windows PowerShell 5; test/setup-ps1.test.ts runs with a space in TEMP and must report exactly 4 pass, 0 fail, 1 skip (the Linux-only missing-prerequisite case). - A copy of the built checkout under a path with a space installs through `powershell.exe -File setup.ps1 --host codex`. held-install-journey job (new, parallel): - origin's main is this PR's tree on top of the v1.91.101.0 tree (47d639a); fix-branch adds one file. - A prefix-mode v1.91.101.0 install at the runner's ~/.claude path in three states (clean, dirty-conflicting, already-unmerged), with auto_upgrade=true and the throttle cleared: the old updater must log `HELD hook-check-unavailable`; the PowerShell recovery form, read verbatim from docs/troubleshooting.md and run under powershell.exe, must reach main with every local edit (only those) in one rescue ref. The dirty state runs it with its .\setup.ps1; the others replace that step with a marker. - The trial and go-back forms move the install to fix-branch (detached) and back to main. - Only then a later release whose symlink sorts before its target is pushed, and the new updater must log UPDATED and reach it. - Both jobs assert the exact list of cases that ran. The journey steps ran locally on Linux with pwsh in Legacy argument mode as powershell.exe, a git exec-path whose ../../../bin/bash.exe is bash, and a failing tar standing in for MSYS tar (all 9 cases passed); the windows-latest run itself happens on the PR. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…f the type check Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
The cache key line is a secret-scanner false positive on added lines (env.kv); the fixture's own setup installs its packages either way. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
test/free-fixtures.test.ts failed on the frozen-branch Ubicloud run: test/fixtures/session-update-heartbeat.sh (adopted with #3191) was in no paid touchfile and not in FREE_FIXTURES. Its only consumer is test/session-update-heartbeat.test.ts. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…item 11 (PR 1a) PR 1a owns the bun-version-unreadable hold (BUN_VERSION_UNREADABLE), its banner case and its troubleshooting entry; this branch is rebased onto 1a. Drops the case, the docs entry and the banner test this branch had added. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…H as an alias and a BASH_UNAVAILABLE refusal Follows lib/script-invocation.ts (Lane C, PR 2): GSTACK_GIT_BASH is the override and GSTACK_BASH its deprecated alias at the same precedence; when both are set and differ GSTACK_GIT_BASH wins and one warning prints. An override that does not exist is refused, never skipped. Without one: the install git --exec-path belongs to (behind a scoop shim, or a portable Git), the folders around git.exe, %LOCALAPPDATA%\Programs\Git, %ProgramFiles%\Git, and last a bash on PATH whose uname -s reports MSYS; WSL's bash (System32, WindowsApps) is rejected. A miss lists every candidate with its rejection, then the PowerShell user-variable and ~/.claude/settings.json env recipes (BASH_UNAVAILABLE, #bash-unavailable). test/setup-ps1.test.ts gains a 14-row pwsh-on-Linux table with every location injected (override, alias, both set, missing override and alias, scoop shim, per-user, portable and machine-wide Git, MSYS PATH bash, WSL and non-MSYS rejections, missing); 11 rows fail on main. The rows mirror Lane C's table in test/script-invocation.test.ts; they are not wired to it, since that branch is not available here. The Windows job's expected skip count for this file moves from 1 to 15 (the table is Linux-only). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…P_PS1_EMPTY_ARGUMENT) Adds the row to lib/result-codes.ts (anchor #setup-ps1-empty-argument, which setup.ps1 already prints). bin/gstack-doctor-components.sh is regenerated from the table (generated; resolved by regeneration at merge). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
… PowerShell's, not bash's) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
MSYS tar reads `D:\a\_temp/...` as host:path (rsh syntax) and fails with 'Cannot open: No such file or directory' before setup.ps1 runs. The copy target now goes through cygpath -u; setup.ps1 still gets the Windows form. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
MSYS tar cannot extract connect-chrome, a symlink that precedes its target (the #3184 failure mode), so the copy died before setup.ps1 ran. tar -h stores the target instead. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix wave 2026-10-15, PR 1b: recovery, install and Windows
Layer 2 of the fix-wave stack: base
capy/fixwave-1a(PR 1a). Adopts #3189 (@BenjaminDSmithy), #3191 (@Jesssullivan), #3181 (@Brit-juho, commit author NetrioGit) and #3152 (@szsunyuan). Fixes #3184, #3130, #3180, #3133, #3131.No VERSION, CHANGELOG, package.json version or agents-digest stamp in this layer; the CHANGELOG draft is in
.context/fixwave-changelog-1b.mdfor the integrator.Behavior changes
/gstack-upgradeStep 4 move the install only throughbin/gstack-ff-update. Local edits that conflict with an update (staged, unstaged, and files that were already unmerged) are saved inrefs/gstack/rescue/<id>before the install moves; at most once a day the session-start line shows how to inspect the rescue, export only your change (git -C '<install>' diff '<ref>^' '<ref>'), and delete the ref. Already-unmerged captures are labelled "needs manual resolution".tar(MSYS tar could not create theconnect-chromesymlink), so installs are no longer held on every release (Windows + Git Bash: auto-update held on every new release (tar can't create the connect-chrome symlink, fail-closed since 1.91.89.0); banner shows a stale "bun not found" instead #3184). The banner shows the hold reason instead of a stale "bun not found". Held installs get published recovery, trial and go-back commands for Git Bash and for Windows PowerShell (docs/troubleshooting.md#auto-update-held-install-recovery)..\setup.ps1works under Windows PowerShell 5.1 (Windows: setup.ps1 fails under Windows PowerShell 5.1 — embedded double quotes in the bash -c string are not escaped #3130). An empty argument is refused before anything runs (exit 2,SETUP_PS1_EMPTY_ARGUMENT). Git Bash is resolved in the shared order:GSTACK_GIT_BASH, orGSTACK_BASHas a deprecated alias (when both are set and differ,GSTACK_GIT_BASHwins with one warning); an override that does not exist is refused; then the Git for Windows install found viagit --exec-path(scoop shim, portable Git), the folders around git.exe,%LOCALAPPDATA%\Programs\Git,%ProgramFiles%\Git, and last an MSYS-verified bash on PATH. WSL's bash is rejected. A miss prints every candidate with its rejection reason, the PowerShell user-variable and~/.claude/settings.jsonrecipes, andBASH_UNAVAILABLE.CLAUDE_CONFIG_DIRlinks back to~/.claude, setup andgstack-memorable enableregister hooks at~/.claude/skills/gstack, the path that outlives the session (setup bakes ephemeral CLAUDE_CONFIG_DIR path into global hooks (dead Stop/session hooks after the session exits) #3180). Both use one resolver,bin/gstack-claude-root.sh../setup --host cursorremoves 1.31-era unprefixed skill folders proven to be gstack's and printscleaned up old entries: <names> (removed from <dir>)(setup --host cursor leaves 1.31-era unprefixed Cursor skill folders, so Cursor loads duplicate skills #3133). User-owned folders stay.gstack-browser-ensureinstall Chromium withbun xinstead ofbunx, which a winget Bun lacks (Windows: Chromium bootstrap fails with exit 127 when bunx is missing, reported as a network problem #3131). Exit 127 names the resolved Bun and how to skip the browser (docs/troubleshooting.md#chromium-install-exit-127); other exit codes keep the network hint.windows-setup-e2e.ymlruns setup.ps1 under PowerShell 5.1 from a path with a space, and a new held-install job runs the PowerShell recovery, trial and go-back forms verbatim on a prefix-mode v1.91.101.0 install in three states. Both jobs assert the exact list of cases that ran.Coordinator exception:
lib/result-codes.ts,docs/troubleshooting.md(append-only set), andbin/gstack-doctor-components.sh(generated fromlib/result-codes.ts; resolved by regeneration at merge).setup.ps1prints a#bash-unavailablelink whose entry PR 2 creates: merge PR 2 first, or the link dangles briefly.Merge order: the
bun-version-unreadablehold, its banner case and its#bun-version-unreadableentry belong to PR 1a; this branch keeps 1a's banner case verbatim and addsunmerged-files,autostash-unrecoverableandhook-check-unavailablecases beside it, so no hold reason falls through to the generic line. The/gstack-upgradeStep 4 pull throughgstack-ff-updateis its own commit and keeps 1a's Bun floor check ahead of it.Checks on the rebased head
Focused: session-update-* (held-banner, hook-hold, stages, autostash, heartbeat), gstack-ff-update, held-install-recovery, setup-ps1 (pwsh), upgrade-template-pins, gstack-upgrade-fresh-shell, upgrade-setup-recovery, parity-suite, doctor-components, troubleshooting-anchors, spawnsync-timeout-tripwire, module-size-ratchet, bun-version-shells, sanitize-echo, gen-skill-docs: 798 pass, 0 fail. Setup/install tests (hook canonical paths, memorable, cursor legacy skills, playwright best-effort, browser-ensure, free-fixtures, hook syntax gate): 137 pass, 0 fail.
bun run typecheckclean;bun run typecheck:test865 known diagnostics, none new.gen:skill-docsandgen-doctor-componentsregenerate with no diff. Pregate preflight: warn only on PII-shaped test fixture strings, no credentials. Local emulation of the held-install Windows journey: all steps passed. Fullbun run testran green (17/17 shards on Ubicloud) before the rebase and was not rerun.