Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1773330
fix(freeze): hook reads the same state root /freeze writes — fails cl…
garrytan Sep 3, 2026
584c2a4
fix(relink): never delete or link over a skill gstack does not own (#…
garrytan Sep 3, 2026
550352c
fix(setup): Chromium bootstrap is best-effort and bounded — skills al…
garrytan Sep 3, 2026
18d829f
docs(designs): preserve the time-attack fork-port residual evaluation
garrytan Sep 3, 2026
e5e01d1
docs: file the fork-port residual deferrals and document the Chromium…
garrytan Sep 3, 2026
b23b784
fix(relink): canonicalize link targets before the ownership check
garrytan Sep 4, 2026
29237ff
fix(telemetry): one-shot setup events never sweep other sessions' pen…
garrytan Sep 4, 2026
b8d347d
fix(hooks): partial upgrades fail closed for freeze and fall back for…
garrytan Sep 4, 2026
2548634
fix(setup): never link over, copy over, or reap a skill gstack does n…
garrytan Sep 4, 2026
529ee9a
docs: README Chromium note outside the CLAUDE.md fence; report banner…
garrytan Sep 4, 2026
953ae67
docs(todos): the bootstrap block coverage gap is pinned except the qu…
garrytan Sep 4, 2026
73506b5
fix(setup,relink): ownership proof has two strengths; weak proof neve…
garrytan Sep 4, 2026
f1a580e
fix(setup): Chromium-install lock reclaim is atomic and pid-validated…
garrytan Sep 4, 2026
55322de
fix(freeze): an unexpected non-zero death denies via an EXIT backstop…
garrytan Sep 4, 2026
3f1a5e6
refactor(telemetry): guard the stale-marker sweep with an if, not a b…
garrytan Sep 4, 2026
3bd7e6d
docs(todos): the ownership gate lives in six sites, and the cleanup a…
garrytan Sep 4, 2026
35805ad
test: the two remaining linker harnesses extract the ownership helper…
garrytan Sep 4, 2026
a8bc93e
fix(setup,relink): weak proof never costs the user a file — assets, f…
garrytan Sep 4, 2026
8e1e520
fix(setup): lock reclaim hands a fresh lock back; a live holder past …
garrytan Sep 4, 2026
85aaefb
fix(freeze): mark the decision written after the helper prints, not b…
garrytan Sep 4, 2026
25fe33d
chore: bump version and changelog (v1.80.0.0)
garrytan Sep 4, 2026
23fccb9
docs: update project documentation for v1.80.0.0
garrytan Sep 4, 2026
e64bab7
fix(setup): the final summary reports customized SKILL.md files moved…
garrytan Sep 4, 2026
1204828
test: run assembled setup harness scripts from a temp file, not `bash…
garrytan Sep 4, 2026
10100d6
docs(test-free-shards): the rerun-refresh harness spawns bash <tempfi…
garrytan Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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/<timestamp>/` 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/<ts>/` |
| 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=<seconds>` 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/<timestamp>/`.
- `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.**
Expand Down
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<ts>/<skill>/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.

Expand Down
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<ts>/` 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=<seconds>` (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

Expand Down Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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=<seconds>` 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 `<!-- AUTO-GENERATED from ... -->` 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/<timestamp>/<skill>/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:

```
Expand Down
71 changes: 63 additions & 8 deletions TODOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <base>`) 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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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).
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.79.0.0
1.80.0.0
2 changes: 1 addition & 1 deletion agents-digest/gstack-AGENTS.md
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading