Skip to content

Commit b54a00d

Browse files
Merge remote-tracking branch 'origin/main' into stevenbarragan/pin-chromium-launch-headed
* origin/main: v1.42.1.0 feat: gate terminal-agent teardown on ServerConfig.ownsTerminalAgent (unblocks gbrowser embedder) (garrytan#1615) v1.42.0.0 Daegu wave: 23 community-filed bugs + PTY classifier enforcement (24 bisect commits) (garrytan#1594) v1.41.1.0 fix wave: 7 HIGH bugs from external audit + regression tests (PR garrytan#1169 follow-up) (garrytan#1592)
2 parents ecf7efb + b03cd1a commit b54a00d

78 files changed

Lines changed: 3634 additions & 223 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
name: Windows Setup E2E
2+
3+
# End-to-end fresh-install gate for Windows. Runs `./setup` on a clean
4+
# windows-latest checkout and asserts the build completes, binaries
5+
# resolve via find-browse, and the gstack-paths state root resolves
6+
# cleanly. Catches Bun shell-parser regressions in package.json's build
7+
# chain (#1538, #1537, #1530, #1457, #1561) before they reach users.
8+
#
9+
# Separate from windows-free-tests.yml because that one runs a curated
10+
# unit-test subset; this one exercises the install path itself.
11+
#
12+
# Runner: GitHub-hosted free windows-latest. ~3-5 min total.
13+
14+
on:
15+
pull_request:
16+
branches: [main]
17+
paths:
18+
- 'package.json'
19+
- 'scripts/build.sh'
20+
- 'scripts/write-version-files.sh'
21+
- 'setup'
22+
- 'browse/src/cli.ts'
23+
- 'browse/src/find-browse.ts'
24+
- 'bin/gstack-paths'
25+
- '.github/workflows/windows-setup-e2e.yml'
26+
workflow_dispatch:
27+
28+
concurrency:
29+
group: windows-setup-e2e-${{ github.head_ref }}
30+
cancel-in-progress: true
31+
32+
jobs:
33+
windows-setup:
34+
runs-on: windows-latest
35+
timeout-minutes: 15
36+
37+
steps:
38+
- uses: actions/checkout@v4
39+
40+
- uses: oven-sh/setup-bun@v1
41+
with:
42+
bun-version: latest
43+
44+
- name: Configure git identity
45+
run: |
46+
git config --global user.email "windows-setup-e2e@gstack.test"
47+
git config --global user.name "Windows Setup E2E"
48+
git config --global init.defaultBranch main
49+
shell: bash
50+
51+
- name: Install dependencies
52+
run: bun install --frozen-lockfile
53+
shell: bash
54+
55+
- name: Run bun run build (the previously-broken path)
56+
# This is the regression gate. Bun's Windows shell parser rejected
57+
# multiple constructs the old inline build chain used; the wave
58+
# moved the build to scripts/build.sh. If this step fails on
59+
# Windows, the build chain regressed.
60+
run: bun run build
61+
shell: bash
62+
env:
63+
GSTACK_SKIP_PLAYWRIGHT: '1'
64+
65+
- name: Verify binaries exist (with .exe extension on Windows)
66+
run: |
67+
set -e
68+
test -f browse/dist/browse.exe || test -f browse/dist/browse || (echo "MISSING: browse" && exit 1)
69+
test -f browse/dist/find-browse.exe || test -f browse/dist/find-browse || (echo "MISSING: find-browse" && exit 1)
70+
test -f design/dist/design.exe || test -f design/dist/design || (echo "MISSING: design" && exit 1)
71+
test -f bin/gstack-global-discover.exe || test -f bin/gstack-global-discover || (echo "MISSING: gstack-global-discover" && exit 1)
72+
echo "All binaries present"
73+
shell: bash
74+
75+
- name: Verify find-browse resolves to the .exe variant
76+
run: |
77+
set -e
78+
OUT=$(bun browse/src/find-browse.ts 2>&1) || true
79+
echo "find-browse output: $OUT"
80+
# On Windows, find-browse should successfully resolve to a binary,
81+
# whether or not it has the .exe extension on disk. Empty output
82+
# or "not found" means the .exe extension resolver regressed.
83+
echo "$OUT" | grep -qE '(browse\.exe|browse)$' || (echo "find-browse failed to resolve binary on Windows" && exit 1)
84+
shell: bash
85+
86+
- name: Verify gstack-paths state root resolves
87+
run: |
88+
set -e
89+
eval "$(bash bin/gstack-paths)"
90+
test -n "$GSTACK_STATE_ROOT" || (echo "GSTACK_STATE_ROOT empty" && exit 1)
91+
test -n "$PLAN_ROOT" || (echo "PLAN_ROOT empty" && exit 1)
92+
test -n "$TMP_ROOT" || (echo "TMP_ROOT empty" && exit 1)
93+
echo "GSTACK_STATE_ROOT=$GSTACK_STATE_ROOT"
94+
echo "PLAN_ROOT=$PLAN_ROOT"
95+
echo "TMP_ROOT=$TMP_ROOT"
96+
shell: bash

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ dist/
44
browse/dist/
55
design/dist/
66
make-pdf/dist/
7-
bin/gstack-global-discover
7+
bin/gstack-global-discover*
88
.gstack/
99
.claude/skills/
1010
.claude/scheduled_tasks.lock

CHANGELOG.md

Lines changed: 173 additions & 0 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,20 @@ Activity / Refs / Inspector as debug overlays behind the footer's
236236
flow, dual-token model, and threat-model boundary — silent failures
237237
here usually trace to not understanding the cross-component flow.
238238

239+
**Embedder terminal-agent ownership** (v1.42.1.0+). `buildFetchHandler`
240+
in `browse/src/server.ts` accepts `ServerConfig.ownsTerminalAgent?:
241+
boolean` (default `true`). When `true`, factory shutdown runs the full
242+
teardown: `pkill -f terminal-agent\.ts` plus `safeUnlinkQuiet` on
243+
`<stateDir>/terminal-port` and `<stateDir>/terminal-internal-token`.
244+
Embedders (e.g. the gbrowser phoenix overlay) that pre-launch their
245+
own PTY server must pass `false` so their discovery files survive
246+
gstack teardown cycles. The flag is the third caller-owned teardown
247+
gate in `ServerConfig` (alongside `xvfb?` and `proxyBridge?`); polarity
248+
is inverted (explicit bool vs presence) and documented in the field's
249+
JSDoc. CLI `start()` always passes `true` explicitly — the static-grep
250+
test in `browse/test/server-embedder-terminal-port.test.ts` fails CI
251+
if a refactor drops it.
252+
239253
**WebSocket auth uses Sec-WebSocket-Protocol, not cookies.** Browsers
240254
can't set `Authorization` on a WebSocket upgrade, but they CAN set
241255
`Sec-WebSocket-Protocol` via `new WebSocket(url, [token])`. The agent

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ Four paths, pick one:
395395
- **PGLite local** — zero accounts, zero network, ~30 seconds. Isolated brain on this Mac only. Great for try-first; migrate to Supabase later with `/setup-gbrain --switch`.
396396
- **Remote gbrain MCP** — your brain runs on another machine (Tailscale, ngrok, internal LAN) or a teammate's server; paste an MCP URL and bearer token. Optionally pair with a local PGLite for symbol-aware code search in split-engine mode. Best for cross-machine memory without standing up a local DB.
397397

398-
After init, the skill offers to register gbrain as an MCP server for Claude Code (`claude mcp add gbrain -- gbrain serve`) so `gbrain search`, `gbrain put_page`, etc. show up as first-class typed tools — not bash shell-outs.
398+
After init, the skill offers to register gbrain as an MCP server for Claude Code (`claude mcp add gbrain -- gbrain serve`) so `gbrain search`, `gbrain put`, etc. show up as first-class typed tools — not bash shell-outs.
399399

400400
**Keeping the brain current.** Run `/sync-gbrain` from any repo to re-index its code into gbrain (incremental by default, `--full` for a full reindex, `--dry-run` to preview). The skill registers the cwd as a federated source via `gbrain sources add`, runs `gbrain sync --strategy code`, and writes a `## GBrain Search Guidance` block to your project's CLAUDE.md so the agent prefers `gbrain search`/`code-def`/`code-refs` over Grep. The block is removed automatically if the capability check fails — no stale guidance pointing at tools that aren't installed.
401401

TODOS.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,99 @@
11
# TODOS
22

3+
## browse server: terminal-agent teardown follow-ups (filed v1.41 via /plan-eng-review)
4+
5+
### P3: Identity-based terminal-agent kill (replace pkill regex with PID)
6+
7+
**What:** Record the spawned terminal-agent PID at `browse/src/cli.ts:1057` and
8+
replace `pkill -f terminal-agent\.ts` at both `cli.ts:1047` and
9+
`server.ts:1281` (now inside the `if (ownsTerminalAgent)` gate) with
10+
`process.kill(pid, signal)` against the recorded PID.
11+
12+
**Why:** `pkill -f terminal-agent\.ts` matches by command-line regex, so today
13+
it can kill ANY process whose argv contains `terminal-agent.ts` — sibling
14+
gstack sessions, editor processes that have the file open, a second gstack
15+
run on the same host. Latent footgun for the CLI path, not just embedders.
16+
17+
**Pros:** Removes a real cross-session foot-cannon. PID-based kill is the
18+
correct identity primitive. Lets us tighten `pkill -f`'s broad-match warning
19+
in the new `ownsTerminalAgent` JSDoc to "historical" rather than "current".
20+
21+
**Cons:** Requires threading the PID through the CLI-to-server state path
22+
(currently the parent server reads `terminal-port` to discover the agent; it
23+
would also need `terminal-agent-pid`). Touches `cli.ts`, `server.ts`, and
24+
`terminal-agent.ts` together — bigger surface than the v1.41 fix.
25+
26+
**Context:** Surfaced by both Codex and Claude subagent during /autoplan
27+
review of the `ownsTerminalAgent` gate. Currently documented as out-of-scope
28+
in `browse/src/server.ts` JSDoc for `ServerConfig.ownsTerminalAgent`. The
29+
embedder fix (ownsTerminalAgent: false) means embedders don't hit this; CLI
30+
users still do.
31+
32+
**Depends on:** None.
33+
34+
---
35+
36+
### P3: shutdown() reads module-level `config`, not `cfg.config` (composition gap)
37+
38+
**What:** `browse/src/server.ts:shutdown()` reads `path.dirname(config.stateFile)`
39+
where `config` is the module-level value resolved at import time, not the
40+
`cfg.config` passed into `buildFetchHandler`. Same gap applies to
41+
`cleanSingletonLocks(resolveChromiumProfile())` at server.ts:1298 — should
42+
read `cfg.chromiumProfile`.
43+
44+
**Why:** Embedders today happen to share state-dir resolution with the CLI
45+
(both go through `resolveConfig()` against the same env), so this doesn't
46+
bite. But if an embedder ever passes a divergent `cfg.config` (e.g., a test
47+
harness pointing at a temp dir), shutdown will operate on the wrong paths.
48+
The `ownsTerminalAgent` flag exposes the problem without fixing it.
49+
50+
**Pros:** Closes the embedder-composition story properly. Pairs with
51+
`cfg.chromiumProfile` to give a single coherent "this factory teardown
52+
respects cfg" contract.
53+
54+
**Cons:** Pre-existing — not a regression. Two call sites today (1285 for
55+
terminal files, 1298 for chromium locks). Threading `cfg.config` and
56+
`cfg.chromiumProfile` into the right closures is straightforward but
57+
broader than the v1.41 fix.
58+
59+
**Context:** Flagged by both Codex and Claude subagent in the /plan-eng-review
60+
dual voices. Documented as out-of-scope in the v1.41 plan; same shape as the
61+
`chromiumProfile` PR-body note to the gbrowser team.
62+
63+
**Depends on:** None.
64+
65+
---
66+
67+
### P3: Ownership-object refactor if a 4th caller-owned teardown gate appears
68+
69+
**What:** Today `ServerConfig` has three caller-owned teardown gates:
70+
`xvfb?` (presence ⇒ don't close), `proxyBridge?` (same), and now
71+
`ownsTerminalAgent` (explicit boolean). If a 4th gate appears, collapse to
72+
`cfg.callerOwns?: Set<'terminalAgent' | 'xvfb' | 'proxyBridge' | ...>` or
73+
similar.
74+
75+
**Why:** Three independent flags is below the refactor threshold — each
76+
field has clear, distinct semantics and the JSDoc voice is consistent. A
77+
fourth tips the cost balance: the per-field surface gets noisy, and
78+
"what does this factory own?" becomes a question you have to ask of three
79+
or four scattered fields instead of one explicit set.
80+
81+
**Pros:** Single source of truth for "what gstack tears down". Trivial
82+
extension surface for future caller-owned resources. Easier to assert in
83+
tests ("the set should contain X, not Y").
84+
85+
**Cons:** Premature today. The polarity-inversion note in the
86+
`ownsTerminalAgent` JSDoc only hurts a little — it's one anomaly, not a
87+
pattern. Refactoring now to an ownership object would touch every embedder.
88+
89+
**Context:** Recommended by Claude subagent during /plan-ceo-review dual
90+
voice (autoplan). Trigger: a 4th caller-owned teardown gate in this same
91+
`ServerConfig` shape.
92+
93+
**Depends on:** A 4th gate to motivate the refactor.
94+
95+
---
96+
397
## /sync-gbrain memory stage perf follow-up
498

599
### P2: Investigate `gbrain import` perf on large staging dirs

USING_GBRAIN_WITH_GSTACK.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ By default the skill asks "Give Claude Code a typed tool surface for gbrain?" If
8282
claude mcp add gbrain -- gbrain serve
8383
```
8484

85-
That registers gbrain's stdio MCP server with Claude Code. Now `gbrain search`, `gbrain put_page`, `gbrain get_page`, etc. show up as first-class tools in every session, not bash shell-outs.
85+
That registers gbrain's stdio MCP server with Claude Code. Now `gbrain search`, `gbrain put`, `gbrain get`, etc. show up as first-class tools in every session, not bash shell-outs.
8686

8787
**If `claude` is not on PATH**, the skill skips MCP registration gracefully with a manual-register hint. The CLI resolver still works from any skill that shells out to `gbrain` — MCP is an upgrade, not a prerequisite.
8888

@@ -224,8 +224,8 @@ Gbrain itself ships with these that gstack wraps:
224224
| `gbrain migrate --to supabase --url ...` | Move a PGLite brain to Supabase (lossless, preserves source as backup) |
225225
| `gbrain migrate --to pglite` | Reverse migration |
226226
| `gbrain search "query"` | Search the brain |
227-
| `gbrain put_page --title "..." --tags "a,b" <<<"content"` | Write a page |
228-
| `gbrain get_page "<slug>"` | Fetch a page |
227+
| `gbrain put "<slug>" --content "<markdown-with-frontmatter>"` | Write a page (title/tags go in YAML frontmatter inside `--content`) |
228+
| `gbrain get "<slug>"` | Fetch a page |
229229
| `gbrain serve` | Start the MCP stdio server (used by `claude mcp add`) |
230230

231231
### Config files + state

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.40.0.0
1+
1.42.1.0

bin/gstack-brain-context-load.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,10 @@ function resolveSkillFile(args: CliArgs): string | null {
192192

193193
function gbrainAvailable(): boolean {
194194
try {
195-
execFileSync("command", ["-v", "gbrain"], { stdio: "ignore" });
195+
execFileSync("gbrain", ["--version"], {
196+
stdio: "ignore",
197+
timeout: MCP_TIMEOUT_MS,
198+
});
196199
return true;
197200
} catch {
198201
return false;

bin/gstack-gbrain-sync.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,13 +287,20 @@ function gbrainSupportsSourcesRename(env?: NodeJS.ProcessEnv): boolean {
287287
* `env` is the environment passed to the spawned `gbrain` process; defaults
288288
* to `process.env`. Tests inject a PATH that points at a gbrain shim so the
289289
* helper can be exercised without a real gbrain CLI.
290+
*
291+
* Shape note: `gbrain sources list --json` returns `{sources: [...]}` (v0.20+);
292+
* older versions returned a flat array. Accept both for forward/backward compat
293+
* (mirrors `probeSource`/`sourcePageCount` in lib/gbrain-sources.ts).
290294
*/
291295
export function sourceLocalPath(sourceId: string, env?: NodeJS.ProcessEnv): string | null {
292-
const list = execGbrainJson<Array<{ id: string; local_path?: string }>>(
296+
const raw = execGbrainJson<unknown>(
293297
["sources", "list", "--json"],
294298
{ baseEnv: env },
295299
);
296-
if (!list) return null;
300+
if (!raw) return null;
301+
const list: Array<{ id?: string; local_path?: string }> = Array.isArray(raw)
302+
? (raw as Array<{ id?: string; local_path?: string }>)
303+
: ((raw as { sources?: Array<{ id?: string; local_path?: string }> }).sources ?? []);
297304
const found = list.find((s) => s.id === sourceId);
298305
return found?.local_path ?? null;
299306
}

0 commit comments

Comments
 (0)