From cdfe2d907475f30762a0576812f1b32482949a68 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 25 Aug 2026 17:03:12 +0000 Subject: [PATCH] feat(setup-gbrain): carve the branch-exclusive install paths into sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brain-init (Paths 1/2/3/4 bodies), engine remediation, transcript gate, and CLAUDE.md persist load on demand — at most one install route ever runs. Skeleton 75.3KB -> 57.0KB; the Step 1 detect and Step 2 path dispatch stay always-loaded. New buildSetupGbrainFixture helper gives the periodic E2Es extract-don't-copy fixtures with a non-empty guard; the voyage-code-3 gate counts scan the tmpl union (the third init site lives in engine-remediation). Co-Authored-By: Claude Fable 5 --- setup-gbrain/SKILL.md | 511 ++---------------- setup-gbrain/SKILL.md.tmpl | 497 +---------------- setup-gbrain/sections/brain-init.md | 270 +++++++++ setup-gbrain/sections/brain-init.md.tmpl | 268 +++++++++ setup-gbrain/sections/claude-md-persist.md | 79 +++ .../sections/claude-md-persist.md.tmpl | 77 +++ setup-gbrain/sections/engine-remediation.md | 71 +++ .../sections/engine-remediation.md.tmpl | 69 +++ setup-gbrain/sections/manifest.json | 32 ++ setup-gbrain/sections/transcript-gate.md | 66 +++ setup-gbrain/sections/transcript-gate.md.tmpl | 64 +++ test/gbrain-init-voyage-code-3.test.ts | 17 +- test/helpers/setup-gbrain-fixture.ts | 107 ++++ .../setup-gbrain-bin-invocation-paths.test.ts | 49 +- test/setup-gbrain-path4-structure.test.ts | 144 +++-- test/skill-e2e-setup-gbrain-bad-token.test.ts | 11 +- ...2e-setup-gbrain-path4-local-pglite.test.ts | 14 +- test/skill-e2e-setup-gbrain-remote.test.ts | 11 +- 18 files changed, 1336 insertions(+), 1021 deletions(-) create mode 100644 setup-gbrain/sections/brain-init.md create mode 100644 setup-gbrain/sections/brain-init.md.tmpl create mode 100644 setup-gbrain/sections/claude-md-persist.md create mode 100644 setup-gbrain/sections/claude-md-persist.md.tmpl create mode 100644 setup-gbrain/sections/engine-remediation.md create mode 100644 setup-gbrain/sections/engine-remediation.md.tmpl create mode 100644 setup-gbrain/sections/manifest.json create mode 100644 setup-gbrain/sections/transcript-gate.md create mode 100644 setup-gbrain/sections/transcript-gate.md.tmpl create mode 100644 test/helpers/setup-gbrain-fixture.ts diff --git a/setup-gbrain/SKILL.md b/setup-gbrain/SKILL.md index 557f381e2..4c8d1d50d 100644 --- a/setup-gbrain/SKILL.md +++ b/setup-gbrain/SKILL.md @@ -420,6 +420,20 @@ implemented as a dispatcher binary. --- +## Section index — Read each section when its situation applies + +This skill is a decision-tree skeleton. The steps below point to on-demand +sections. Read a section in full before doing its step; do not work from memory. + +| When | Read this section | +|------|-------------------| +| running the Step 1.5 broken-engine remediation — Step 1's detect returned `gbrain_local_status` of `broken-db` or `broken-config` and no shortcut flag was passed | `sections/engine-remediation.md` | +| initializing the brain in Step 4 — run ONLY the procedure for the path picked in Step 2 (Paths 1/2a/2b/3/4 or Switch; also holds the PAT scope disclosure that `--cleanup-orphans` re-uses) | `sections/brain-init.md` | +| running the Step 7.5 transcript & memory ingest gate on Paths 1, 2a, 2b, or 3 (Path 4 skips this section entirely — see the skeleton's skip note) | `sections/transcript-gate.md` | +| persisting the Step 8 `## GBrain Configuration` block to CLAUDE.md (and the Search Guidance block after Step 9 passes) | `sections/claude-md-persist.md` | + +--- + ## Step 1: Detect current state ```bash @@ -453,77 +467,15 @@ invocation flags here and skip to the matching step. Read `gbrain_local_status` from the Step 1 detect output. **If it's `broken-db` or `broken-config` AND no shortcut flag was passed**, the user has a -non-working local engine (Garry's repro: `~/.gbrain/config.json` points at a -dead Postgres URL). Fire a targeted AskUserQuestion BEFORE Step 2: - -> D# — Your local gbrain engine isn't responding. How do you want to fix it? -> Project/branch/task: -> ELI10: gbrain has a config at `~/.gbrain/config.json` but the engine it points -> at isn't reachable. That could be a transient outage (Postgres container -> stopped, Tailscale down) OR a stale config you want to abandon. Different -> remediation for each case. -> Stakes if we pick wrong: "Switch to PGLite" overwrites your existing config -> (one-way door if the user actually wanted the broken engine). "Retry" preserves -> existing state for transient cases. -> Recommendation: A (Retry) — always try the cheap option first; if engine is -> just temporarily down it'll come back without any destructive change. -> Note: options differ in kind, not coverage — no completeness score. -> A) Retry — re-probe the engine (recommended; ~80ms) -> ✅ Cheapest test: re-runs `gbrain sources list` to see if engine is back -> ✅ Zero side effects; existing config preserved -> ❌ If engine is permanently dead, retries forever; user must choose another option -> B) Switch to local PGLite (one-way — moves existing config to .bak) -> ✅ Fastest path to a working local engine if user has abandoned the old one -> ✅ ~30s; no accounts; private to this machine -> ❌ Destructive — existing config moved to ~/.gbrain/config.json.gstack-bak-{ts} -> C) Switch brain mode (continue to Step 2 path picker) -> ✅ Lets user pick Path 1/2/3/4 to re-init from scratch -> ✅ Preserves existing config until they explicitly init the new one -> ❌ Longer flow if user just wants to repair to PGLite -> D) Quit (do nothing) -> ✅ No cons — this is a hard-stop choice -> ❌ N/A -> Net: A is the right starting move; B/C are explicit destructive paths; D bails. - -**If A (Retry)**: re-run `~/.claude/skills/gstack/bin/gstack-gbrain-detect` -with `GSTACK_DETECT_NO_CACHE=1` (busts the 60s cache). If the new -`gbrain_local_status` is `ok`, continue to Step 2. If still `broken-db` or -`broken-config`, fire the same AskUserQuestion again (the user picks again). - -**If B (Switch to PGLite)** — execute the rollback-safe init sequence (plan D7): - -```bash -BACKUP="$HOME/.gbrain/config.json.gstack-bak-$(date +%s)" -mv "$HOME/.gbrain/config.json" "$BACKUP" -# gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — best for -# code retrieval. Without the key, fall back to gbrain's own auto-selected -# embedding provider chain (OpenAI 1536d when OPENAI_API_KEY is present, etc.). -# Never select gbrain's legacy zeroentropyai recipe for a new brain: the hosted -# API sunsets September 4, 2026 (#2365); the wireup helper warns existing installs. -set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) -if [ -n "${VOYAGE_API_KEY:-}" ]; then - set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 -fi -if ! gbrain init --pglite --json "$@"; then - # Restore on failure - mv "$BACKUP" "$HOME/.gbrain/config.json" - echo "gbrain init failed. Your previous config was restored at $HOME/.gbrain/config.json." >&2 - echo "PGLite directory at ~/.gbrain/pglite/ may be in a partial state — \`rm -rf ~/.gbrain/pglite\` if needed before retrying." >&2 - exit 1 -fi -echo "Switched to local PGLite. Previous config saved at $BACKUP — review before deleting." -``` - -Then jump to Step 5a (MCP registration; the new PGLite engine is registered as -local-stdio). - -**If C (Switch brain mode)**: continue to Step 2's normal path picker. - -**If D (Quit)**: STOP the skill cleanly. +non-working local engine — run the remediation below BEFORE Step 2. For `gbrain_local_status` values of `no-cli` or `missing-config`, do NOT fire Step 1.5 — fall through to Step 2 (where `no-cli` triggers Step 3 install and -`missing-config` triggers Step 4 init). +`missing-config` triggers Step 4 init). Do not read the remediation section in +that case. + +> **STOP.** Before running the Step 1.5 broken-engine remediation — Step 1's detect returned `gbrain_local_status` of `broken-db` or `broken-config` and no shortcut flag was passed, Read `~/.claude/skills/gstack/setup-gbrain/sections/engine-remediation.md` and execute it +> in full. Do not work from memory — that section is the source of truth for this step. --- @@ -632,273 +584,12 @@ continue the skill — the environment is broken until the user fixes PATH. ## Step 4: Initialize the brain -Path-specific. +Path-specific. The init procedure for the path picked in Step 2 — Paths 1, 2a, +2b, 3, 4 (4a-4e), and the Switch migration flow — lives in the brain-init +section. Run ONLY the sub-section for the picked path. -### Path 1 (Supabase, existing URL) - -Source the secret-read helper, collect URL with `read -s` + redacted preview: - -```bash -. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh -read_secret_to_env GBRAIN_POOLER_URL "Paste Session Pooler URL: " \ - --echo-redacted 's#://[^@]*@#://***@#' -``` - -Then validate structurally: - -```bash -printf '%s' "$GBRAIN_POOLER_URL" | ~/.claude/skills/gstack/bin/gstack-gbrain-supabase-verify - -``` - -If the verify exit code is 3 (direct-connection URL), the verifier's own -message explains the fix; surface it and re-prompt for a Session Pooler URL. - -On success, hand off to gbrain via env var (D10, never argv): - -```bash -GBRAIN_DATABASE_URL="$GBRAIN_POOLER_URL" gbrain init --non-interactive --json -``` - -Then `unset GBRAIN_POOLER_URL GBRAIN_DATABASE_URL` immediately. The URL is -now persisted in `~/.gbrain/config.json` at mode 0600 by gbrain itself. - -### Path 2a (Supabase, auto-provision — D7) - -Show the D11 PAT scope disclosure verbatim BEFORE collecting the token: - -> *This Supabase Personal Access Token grants full read/write/delete access -> to every project in your Supabase account, not just the `gbrain` one we're -> about to create. Supabase doesn't currently support scoped tokens. We use -> this PAT only to: create one project, poll it until healthy, read the -> Session Pooler URL — then discard it from process memory. The token -> remains valid on Supabase's side until you manually revoke it at -> https://supabase.com/dashboard/account/tokens — we recommend revoking -> immediately after setup completes.* - -Then: - -```bash -. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh -read_secret_to_env SUPABASE_ACCESS_TOKEN "Paste PAT: " -``` - -Ask the D17 tier prompt via AskUserQuestion: "Which Supabase tier?" Present -Free (2-project limit, pauses after 7d inactivity) vs Pro ($25/mo, no -pauses, recommended for real use). Explain that tier is **org-level** (per -the Management API contract) — user picks their org based on its current -tier. Pro may require them to upgrade the org first at supabase.com. - -List orgs, pick one (AskUserQuestion if multiple): - -```bash -orgs=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision list-orgs --json) -``` - -If the `.orgs` array is empty, surface: "Your Supabase account has no -organizations. Create one at https://supabase.com/dashboard, then re-run -`/setup-gbrain`." STOP. - -Ask the user for a region (default `us-east-1`; valid values are the 18 -enum values in the Supabase Management API — list a few common ones, let -them pick "Other" for a full list). - -Generate the DB password (never shown to the user): - -```bash -export DB_PASS=$(openssl rand -base64 24) -``` - -Set up a SIGINT trap (D12 basic recovery): - -```bash -trap 'echo ""; echo "gstack-gbrain: interrupted. In-flight ref: $INFLIGHT_REF"; \ - echo "Resume: /setup-gbrain --resume-provision $INFLIGHT_REF"; \ - echo "Delete: https://supabase.com/dashboard/project/$INFLIGHT_REF"; \ - unset SUPABASE_ACCESS_TOKEN DB_PASS; exit 130' INT TERM -``` - -Create + wait + fetch: - -```bash -result=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision \ - create gbrain "$REGION" "$ORG_SLUG" --json) -INFLIGHT_REF=$(echo "$result" | jq -r .ref) -~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision wait "$INFLIGHT_REF" --json -pooler=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision \ - pooler-url "$INFLIGHT_REF" --json) -GBRAIN_DATABASE_URL=$(echo "$pooler" | jq -r .pooler_url) -export GBRAIN_DATABASE_URL -gbrain init --non-interactive --json -unset SUPABASE_ACCESS_TOKEN DB_PASS GBRAIN_DATABASE_URL INFLIGHT_REF -trap - INT TERM -``` - -After success, emit the PAT revocation reminder: - -> "Setup complete. Revoke the PAT you pasted at -> https://supabase.com/dashboard/account/tokens — we've already discarded -> it from memory and don't need it again. The gbrain project will continue -> working because it uses its own embedded database password." - -### Path 2b (Supabase, manual) - -Walk the user through the supabase.com steps: -1. Login at https://supabase.com/dashboard -2. Click "New Project," name it `gbrain`, pick a region, copy the generated - database password (you'll need it for paste-back? no — it's embedded in - the pooler URL we collect next) -3. Wait ~2 min for the project to initialize -4. Settings → Database → Connection Pooler → Session → copy the URL (port - 6543) - -Then follow the same secret-read + verify + init flow as Path 1. - -### Path 3 (PGLite local) - -```bash -# gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — code -# retrieval beats general-purpose embeddings on real code queries (validated -# A/B). Without the key, gbrain auto-selects (OpenAI 1536d when available). -# Never select gbrain's legacy zeroentropyai recipe for a new brain: the hosted -# API sunsets September 4, 2026 (#2365); the wireup helper warns existing installs. -set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) -if [ -n "${VOYAGE_API_KEY:-}" ]; then - set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 -fi -gbrain init --pglite --json "$@" -``` - -Done. No network, no secrets (beyond Voyage embedding API calls during sync, if -`VOYAGE_API_KEY` is set — ~$0.18 per 1M tokens, pennies per repo). - -### Path 4 (Remote gbrain MCP — HTTP transport with bearer token) - -For users whose brain runs on another machine (Tailscale, ngrok, internal -LAN, or a teammate's server). No local gbrain CLI install, no local DB. -This skill registers the remote MCP and stops; ingestion + indexing happens -on the brain host. - -**4a. Collect MCP URL.** Prompt the user: - -``` -Paste your gbrain MCP URL (e.g. https://wintermute.tail554574.ts.net:3131/mcp): -``` - -Read with plain `read -r` (no secret hygiene needed — the URL alone isn't -a credential). Validate it starts with `https://` (require TLS for any -non-loopback host); refuse `http://` for non-localhost. - -**4b. Collect bearer token via the secret-read helper (D10, never argv).** - -```bash -. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh -read_secret_to_env GBRAIN_MCP_TOKEN "Paste bearer token: " \ - --echo-redacted 's/.\{6\}$/***REDACTED***/' -``` - -**4c. Verify via gstack-gbrain-mcp-verify.** Run the helper; capture the -classified JSON output: - -```bash -verify_json=$(GBRAIN_MCP_TOKEN="$GBRAIN_MCP_TOKEN" \ - ~/.claude/skills/gstack/bin/gstack-gbrain-mcp-verify "$MCP_URL") -status=$(echo "$verify_json" | jq -r .status) -``` - -If `status != "success"`, the helper has already classified the failure -into NETWORK / AUTH / MALFORMED and emitted a one-line remediation hint. -Surface the hint above the raw error from `error_text` and **STOP** with -a clear "fix and re-run /setup-gbrain" message. Do NOT continue to Step 5a -on a failed verify — partial registration would leave the user with a -half-broken state. - -Capture two values from the verify output for downstream steps: -- `SERVER_VERSION` (e.g., `0.27.1`) — written to the CLAUDE.md block in Step 8. -- `URL_FORM_SUPPORTED` (`true|false`) — passed to `gstack-artifacts-init` in - Step 7 to control which form of the brain-admin hookup command is printed. - -**4d. (Path 4) Offer local PGLite for code search.** Per plan D10/D11, ask: - -> D# — Want symbol-aware code search on this machine? -> Project/branch/task: -> ELI10: The remote brain at `` is great for cross-machine knowledge, -> but symbol queries like `gbrain code-def` / `code-refs` / `code-callers` need -> a local index of THIS machine's code. We can spin up a tiny isolated PGLite -> database (~30 seconds, no accounts, ~120 MB disk) just for code, separate -> from your remote brain. Transcripts and artifacts continue routing through -> the artifacts repo to the remote brain — local PGLite stays code-only. -> Stakes: without it, semantic code search in this repo's worktrees falls -> back to Grep. -> Recommendation: A — 30 seconds, no ongoing cost, unlocks the symbol tools. -> Completeness: A=10/10 (full split-engine), B=7/10 (remote-only). -> A) Yes, set up local PGLite for code (recommended) -> ✅ Unlocks `gbrain code-def`, `code-refs`, `code-callers` per worktree -> ✅ Independent engine — won't disturb remote brain or share transcripts -> B) No, remote MCP only -> ✅ Zero local state — only `~/.claude.json` MCP registration -> ❌ Symbol code queries fall back to Grep in this repo's worktrees -> Net: A = full split-engine; B = remote-only. - -**If A (Yes)**: install + init local PGLite with rollback-safe semantics (D7): - -```bash -~/.claude/skills/gstack/bin/gstack-gbrain-install || exit $? -# At this point the local gbrain CLI is on PATH. Init PGLite, but back up any -# existing ~/.gbrain/config.json first (rollback if init fails). -if [ -f "$HOME/.gbrain/config.json" ]; then - BACKUP="$HOME/.gbrain/config.json.gstack-bak-$(date +%s)" - mv "$HOME/.gbrain/config.json" "$BACKUP" -fi -# gstack default for local code-search PGLite: voyage-code-3 (1024d) when -# VOYAGE_API_KEY is set. It wins the A/B over voyage-4-large and OpenAI -# text-embedding-3-large on this codebase's symbol queries. Falls back to -# gbrain's auto-selected provider when the key isn't present. -set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) -if [ -n "${VOYAGE_API_KEY:-}" ]; then - set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 -fi -if ! gbrain init --pglite --json "$@"; then - if [ -n "${BACKUP:-}" ] && [ -f "$BACKUP" ]; then mv "$BACKUP" "$HOME/.gbrain/config.json"; fi - echo "gbrain init failed. Existing config (if any) was restored. PGLite at ~/.gbrain/pglite/ may be in a partial state — \`rm -rf ~/.gbrain/pglite\` to reset." >&2 - echo "Continuing setup without local code search; you can re-run /setup-gbrain to retry." >&2 -fi -``` - -Then continue to Step 5a. The remote-http MCP registration in 5a runs as -today; the local PGLite is independent of MCP registration (Claude Code talks -to the remote brain via MCP for queries; `gbrain` CLI talks to local PGLite -for code-def/refs/callers). - -**If B (No)**: skip the install + init. The local engine stays absent. -`gbrain_local_status` will be `missing-config` (or `no-cli` if gbrain isn't -installed). `/sync-gbrain` will SKIP the code stage cleanly per plan D12. - -**4e. Skip Steps 3, 4 (other paths) and 5 (local doctor) when B was picked.** -When A was picked, Step 3 already ran (via gstack-gbrain-install) and Step 4 -already ran (via `gbrain init --pglite`); jump straight to Step 5a. When B -was picked, Steps 3/4/5 are no-ops; also skip Step 7.5 (transcript ingest) -since memory-stage routes through the artifacts pipeline in remote-http mode -per plan D11. - -The bearer token (`GBRAIN_MCP_TOKEN`) stays in process env until Step 5a's -`claude mcp add --header` consumes it; then `unset GBRAIN_MCP_TOKEN` -immediately. Token security trade-off documented in -`setup-gbrain/memory.md`: brief argv exposure during `claude mcp add`, -resting state in `~/.claude.json` mode 0600. - -### Switch (from detect's existing-engine state) - -```bash -# Going PGLite → Supabase, collect URL first (Path 1 flow), then: -timeout 180s gbrain migrate --to supabase --url "$URL" --json -# Going Supabase → PGLite: -timeout 180s gbrain migrate --to pglite --json -``` - -If `timeout` returns 124 (exit code for timeout): surface D9 message -("Migration didn't complete in 3 minutes — another gstack session may be -holding a lock on the source brain. Close other workspaces and re-run -`/setup-gbrain --switch`. Your original brain is untouched."). STOP. +> **STOP.** Before initializing the brain in Step 4 — run ONLY the procedure for the path picked in Step 2 (Paths 1/2a/2b/3/4 or Switch; also holds the PAT scope disclosure that `--cleanup-orphans` re-uses), Read `~/.claude/skills/gstack/setup-gbrain/sections/brain-init.md` and execute it +> in full. Do not work from memory — that section is the source of truth for this step. --- @@ -1095,154 +786,21 @@ this machine's transcripts indexed, they pull from your `gstack-artifacts-$USER` repo (set up in Step 7) on whatever schedule they prefer. Set `gstack-config set transcript_ingest_mode off` and continue to Step 8. -For Paths 1, 2a, 2b, 3: +For Paths 1, 2a, 2b, 3, run the ingest gate: -After memory sync is wired (Step 7) but before persisting the CLAUDE.md -config (Step 8), offer to bring this Mac's coding-agent transcripts + -curated `~/.gstack/` artifacts into gbrain so the retrieval surface -(per-skill manifests, salience block) has data to surface. - -Run the probe to size the operation: -```bash -bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe -``` - -Read the output. If `Total files in window: 0`, skip — there's nothing -to ingest. Set `gstack-config set transcript_ingest_mode incremental` -silently and continue to Step 8. - -If `New (never ingested)` is < 200 AND total bytes are < 100MB: silent -bulk via `bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --bulk --quiet`. Set -`transcript_ingest_mode=incremental` and continue. - -Otherwise (the "many transcripts on disk" path): AskUserQuestion with -the exact counts AND the value promise. Default scope is **current repo -only, last 90 days**: - -> "Found transcripts in THIS repo () over the last -> 90 days, plus across other repos on this machine ( -> total if all ingested). Ingest THIS repo's transcripts into gbrain? -> -> What you get after this: every gstack skill auto-loads recent salience -> from your past sessions in this repo, so the agent finds your prior -> work without you describing it. You can query 'what was I doing on -> day X' and get a real answer. Per-session pages are searchable, -> taggable, and deletable. Secret scanning runs before any push. -> -> What stays the same: nothing leaves your machine unless gbrain sync -> is enabled (Step 7). Per-repo trust policies still apply. -> -> Multi-Mac note: if you HAVE enabled brain sync (Step 7), these -> transcript pages will sync across your Macs. Caveat: deleting a -> transcript page later removes it from gbrain but git history retains -> it in prior commits. Use `gstack-transcript-prune` to delete in bulk; -> use `git filter-repo` on the brain remote for hard-delete from -> history." - -Options: -- A) Yes — this repo, last 90 days (recommended; ~est min) -- B) Yes — this repo, ALL history -- C) Yes — this repo + other repos on this machine -- D) Skip historical, track new from now (`transcript_ingest_mode=incremental`) -- E) Never ingest transcripts (`transcript_ingest_mode=off`) - -After answer: -```bash -~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode -bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --full --no-brain-sync -``` -(`--no-brain-sync` because Step 7 already wired that path; this just -runs the code import + memory ingest stages. Brain-sync will run on the -next preamble hook.) - -If A/D/E, ingest is incremental from this point on; preamble-boundary -hook runs `bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --incremental --quiet` on every skill -start (cheap mtime fast-path). - -Reference doc for users: `setup-gbrain/memory.md` (linked from CLAUDE.md -Step 8). +> **STOP.** Before running the Step 7.5 transcript & memory ingest gate on Paths 1, 2a, 2b, or 3 (Path 4 skips this section entirely — see the skeleton's skip note), Read `~/.claude/skills/gstack/setup-gbrain/sections/transcript-gate.md` and execute it +> in full. Do not work from memory — that section is the source of truth for this step. --- ## Step 8: Persist `## GBrain Configuration` in CLAUDE.md -Find-and-replace (or append) the section. Block format depends on mode: +CLAUDE.md is the audit trail: after a successful setup, persist the +configuration block. The exact block formats (remote-http vs local-stdio) and +the post-Step-9 Search Guidance write live in the claude-md-persist section. -### Path 4 (Remote MCP) - -```markdown -## GBrain Configuration (configured by /setup-gbrain) -- Mode: remote-http -- MCP URL: {MCP_URL} -- Server version: gbrain v{SERVER_VERSION} (from Step 4c verify) -- Setup date: {today} -- MCP registered: yes (user scope) -- Token: stored in ~/.claude.json (do not commit; never written to CLAUDE.md) -- Artifacts repo: {gstack_artifacts_remote URL or "none"} -- Artifacts sync: {off|artifacts-only|full} -- Current repo policy: {read-write|read-only|deny|unset} -``` - -The bearer token is **never** written to CLAUDE.md (CLAUDE.md is checked -in to git in many projects). It lives only in `~/.claude.json` where -`claude mcp add` placed it. - -### Paths 1, 2a, 2b, 3 (Local stdio) - -```markdown -## GBrain Configuration (configured by /setup-gbrain) -- Mode: local-stdio -- Engine: {pglite|postgres} -- Config file: ~/.gbrain/config.json (mode 0600) -- Setup date: {today} -- MCP registered: {yes/no} -- Artifacts sync: {off|artifacts-only|full} -- Current repo policy: {read-write|read-only|deny|unset} -``` - -**After Step 9 (smoke test) passes, also write the `## GBrain Search Guidance` -block** so the coding agent learns when to prefer `gbrain` over Grep. This -block is gated on the smoke test passing — write the Configuration block -first (so the user knows what state they're in even if the smoke test fails), -then return here after Step 9 and write the guidance block only if smoke -test succeeded. - -When Step 9 passes, find-and-replace (or append) this block. Use HTML-comment -delimiters so removal regex is unambiguous and never eats user content. The -block content is machine-AGNOSTIC — no engine type, no page counts, no -last-sync time. Machine state stays in the Configuration block above. - -```markdown -## GBrain Search Guidance (configured by /sync-gbrain) - - -GBrain is set up and synced on this machine. The agent should prefer gbrain -over Grep when the question is semantic or when you don't know the exact -identifier yet. Two indexed corpora available via the `gbrain` CLI: -- This repo's code (registered as `gstack-code-` source). -- `~/.gstack/` curated memory (registered as `gstack-brain-` source via - the existing federation pipeline). - -Prefer gbrain when: -- "Where is X handled?" / semantic intent, no exact string yet: - `gbrain search ""` or `gbrain query ""` -- "Where is symbol Y defined?" / symbol-based code questions: - `gbrain code-def ` or `gbrain code-refs ` -- "What calls Y?" / "What does Y depend on?": - `gbrain code-callers ` / `gbrain code-callees ` -- "What did we decide last time?" / past plans, retros, learnings: - `gbrain search "" --source gstack-brain-` - -Grep is still right for known exact strings, regex, multiline patterns, and -file globs. The brain auto-syncs incrementally on every gstack skill start. -Run `/sync-gbrain` to force-refresh, `/sync-gbrain --full` for full reindex. - - -``` - -If Step 9 smoke test fails, skip the guidance block write entirely. The user's -next `/sync-gbrain` run will re-evaluate capability and write the block when -the round-trip works. +> **STOP.** Before persisting the Step 8 `## GBrain Configuration` block to CLAUDE.md (and the Search Guidance block after Step 9 passes), Read `~/.claude/skills/gstack/setup-gbrain/sections/claude-md-persist.md` and execute it +> in full. Do not work from memory — that section is the source of truth for this step. --- @@ -1433,7 +991,8 @@ as markdown + git, recoverable manually via `gbrain import` from a clone. ## `/setup-gbrain --cleanup-orphans` (D20) -Re-collect a PAT (Step 4 path-2a scope disclosure), then: +Re-collect a PAT (show the Path 2a PAT scope disclosure — it lives in the +brain-init section; read that section if it isn't already loaded), then: ```bash # List user's Supabase projects (user has to pipe this through their own diff --git a/setup-gbrain/SKILL.md.tmpl b/setup-gbrain/SKILL.md.tmpl index 8115f9d87..b24fa776c 100644 --- a/setup-gbrain/SKILL.md.tmpl +++ b/setup-gbrain/SKILL.md.tmpl @@ -56,6 +56,10 @@ implemented as a dispatcher binary. --- +{{SECTION_INDEX:setup-gbrain}} + +--- + ## Step 1: Detect current state ```bash @@ -89,77 +93,14 @@ invocation flags here and skip to the matching step. Read `gbrain_local_status` from the Step 1 detect output. **If it's `broken-db` or `broken-config` AND no shortcut flag was passed**, the user has a -non-working local engine (Garry's repro: `~/.gbrain/config.json` points at a -dead Postgres URL). Fire a targeted AskUserQuestion BEFORE Step 2: - -> D# — Your local gbrain engine isn't responding. How do you want to fix it? -> Project/branch/task: -> ELI10: gbrain has a config at `~/.gbrain/config.json` but the engine it points -> at isn't reachable. That could be a transient outage (Postgres container -> stopped, Tailscale down) OR a stale config you want to abandon. Different -> remediation for each case. -> Stakes if we pick wrong: "Switch to PGLite" overwrites your existing config -> (one-way door if the user actually wanted the broken engine). "Retry" preserves -> existing state for transient cases. -> Recommendation: A (Retry) — always try the cheap option first; if engine is -> just temporarily down it'll come back without any destructive change. -> Note: options differ in kind, not coverage — no completeness score. -> A) Retry — re-probe the engine (recommended; ~80ms) -> ✅ Cheapest test: re-runs `gbrain sources list` to see if engine is back -> ✅ Zero side effects; existing config preserved -> ❌ If engine is permanently dead, retries forever; user must choose another option -> B) Switch to local PGLite (one-way — moves existing config to .bak) -> ✅ Fastest path to a working local engine if user has abandoned the old one -> ✅ ~30s; no accounts; private to this machine -> ❌ Destructive — existing config moved to ~/.gbrain/config.json.gstack-bak-{ts} -> C) Switch brain mode (continue to Step 2 path picker) -> ✅ Lets user pick Path 1/2/3/4 to re-init from scratch -> ✅ Preserves existing config until they explicitly init the new one -> ❌ Longer flow if user just wants to repair to PGLite -> D) Quit (do nothing) -> ✅ No cons — this is a hard-stop choice -> ❌ N/A -> Net: A is the right starting move; B/C are explicit destructive paths; D bails. - -**If A (Retry)**: re-run `~/.claude/skills/gstack/bin/gstack-gbrain-detect` -with `GSTACK_DETECT_NO_CACHE=1` (busts the 60s cache). If the new -`gbrain_local_status` is `ok`, continue to Step 2. If still `broken-db` or -`broken-config`, fire the same AskUserQuestion again (the user picks again). - -**If B (Switch to PGLite)** — execute the rollback-safe init sequence (plan D7): - -```bash -BACKUP="$HOME/.gbrain/config.json.gstack-bak-$(date +%s)" -mv "$HOME/.gbrain/config.json" "$BACKUP" -# gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — best for -# code retrieval. Without the key, fall back to gbrain's own auto-selected -# embedding provider chain (OpenAI 1536d when OPENAI_API_KEY is present, etc.). -# Never select gbrain's legacy zeroentropyai recipe for a new brain: the hosted -# API sunsets September 4, 2026 (#2365); the wireup helper warns existing installs. -set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) -if [ -n "${VOYAGE_API_KEY:-}" ]; then - set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 -fi -if ! gbrain init --pglite --json "$@"; then - # Restore on failure - mv "$BACKUP" "$HOME/.gbrain/config.json" - echo "gbrain init failed. Your previous config was restored at $HOME/.gbrain/config.json." >&2 - echo "PGLite directory at ~/.gbrain/pglite/ may be in a partial state — \`rm -rf ~/.gbrain/pglite\` if needed before retrying." >&2 - exit 1 -fi -echo "Switched to local PGLite. Previous config saved at $BACKUP — review before deleting." -``` - -Then jump to Step 5a (MCP registration; the new PGLite engine is registered as -local-stdio). - -**If C (Switch brain mode)**: continue to Step 2's normal path picker. - -**If D (Quit)**: STOP the skill cleanly. +non-working local engine — run the remediation below BEFORE Step 2. For `gbrain_local_status` values of `no-cli` or `missing-config`, do NOT fire Step 1.5 — fall through to Step 2 (where `no-cli` triggers Step 3 install and -`missing-config` triggers Step 4 init). +`missing-config` triggers Step 4 init). Do not read the remediation section in +that case. + +{{SECTION:engine-remediation}} --- @@ -268,273 +209,11 @@ continue the skill — the environment is broken until the user fixes PATH. ## Step 4: Initialize the brain -Path-specific. +Path-specific. The init procedure for the path picked in Step 2 — Paths 1, 2a, +2b, 3, 4 (4a-4e), and the Switch migration flow — lives in the brain-init +section. Run ONLY the sub-section for the picked path. -### Path 1 (Supabase, existing URL) - -Source the secret-read helper, collect URL with `read -s` + redacted preview: - -```bash -. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh -read_secret_to_env GBRAIN_POOLER_URL "Paste Session Pooler URL: " \ - --echo-redacted 's#://[^@]*@#://***@#' -``` - -Then validate structurally: - -```bash -printf '%s' "$GBRAIN_POOLER_URL" | ~/.claude/skills/gstack/bin/gstack-gbrain-supabase-verify - -``` - -If the verify exit code is 3 (direct-connection URL), the verifier's own -message explains the fix; surface it and re-prompt for a Session Pooler URL. - -On success, hand off to gbrain via env var (D10, never argv): - -```bash -GBRAIN_DATABASE_URL="$GBRAIN_POOLER_URL" gbrain init --non-interactive --json -``` - -Then `unset GBRAIN_POOLER_URL GBRAIN_DATABASE_URL` immediately. The URL is -now persisted in `~/.gbrain/config.json` at mode 0600 by gbrain itself. - -### Path 2a (Supabase, auto-provision — D7) - -Show the D11 PAT scope disclosure verbatim BEFORE collecting the token: - -> *This Supabase Personal Access Token grants full read/write/delete access -> to every project in your Supabase account, not just the `gbrain` one we're -> about to create. Supabase doesn't currently support scoped tokens. We use -> this PAT only to: create one project, poll it until healthy, read the -> Session Pooler URL — then discard it from process memory. The token -> remains valid on Supabase's side until you manually revoke it at -> https://supabase.com/dashboard/account/tokens — we recommend revoking -> immediately after setup completes.* - -Then: - -```bash -. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh -read_secret_to_env SUPABASE_ACCESS_TOKEN "Paste PAT: " -``` - -Ask the D17 tier prompt via AskUserQuestion: "Which Supabase tier?" Present -Free (2-project limit, pauses after 7d inactivity) vs Pro ($25/mo, no -pauses, recommended for real use). Explain that tier is **org-level** (per -the Management API contract) — user picks their org based on its current -tier. Pro may require them to upgrade the org first at supabase.com. - -List orgs, pick one (AskUserQuestion if multiple): - -```bash -orgs=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision list-orgs --json) -``` - -If the `.orgs` array is empty, surface: "Your Supabase account has no -organizations. Create one at https://supabase.com/dashboard, then re-run -`/setup-gbrain`." STOP. - -Ask the user for a region (default `us-east-1`; valid values are the 18 -enum values in the Supabase Management API — list a few common ones, let -them pick "Other" for a full list). - -Generate the DB password (never shown to the user): - -```bash -export DB_PASS=$(openssl rand -base64 24) -``` - -Set up a SIGINT trap (D12 basic recovery): - -```bash -trap 'echo ""; echo "gstack-gbrain: interrupted. In-flight ref: $INFLIGHT_REF"; \ - echo "Resume: /setup-gbrain --resume-provision $INFLIGHT_REF"; \ - echo "Delete: https://supabase.com/dashboard/project/$INFLIGHT_REF"; \ - unset SUPABASE_ACCESS_TOKEN DB_PASS; exit 130' INT TERM -``` - -Create + wait + fetch: - -```bash -result=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision \ - create gbrain "$REGION" "$ORG_SLUG" --json) -INFLIGHT_REF=$(echo "$result" | jq -r .ref) -~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision wait "$INFLIGHT_REF" --json -pooler=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision \ - pooler-url "$INFLIGHT_REF" --json) -GBRAIN_DATABASE_URL=$(echo "$pooler" | jq -r .pooler_url) -export GBRAIN_DATABASE_URL -gbrain init --non-interactive --json -unset SUPABASE_ACCESS_TOKEN DB_PASS GBRAIN_DATABASE_URL INFLIGHT_REF -trap - INT TERM -``` - -After success, emit the PAT revocation reminder: - -> "Setup complete. Revoke the PAT you pasted at -> https://supabase.com/dashboard/account/tokens — we've already discarded -> it from memory and don't need it again. The gbrain project will continue -> working because it uses its own embedded database password." - -### Path 2b (Supabase, manual) - -Walk the user through the supabase.com steps: -1. Login at https://supabase.com/dashboard -2. Click "New Project," name it `gbrain`, pick a region, copy the generated - database password (you'll need it for paste-back? no — it's embedded in - the pooler URL we collect next) -3. Wait ~2 min for the project to initialize -4. Settings → Database → Connection Pooler → Session → copy the URL (port - 6543) - -Then follow the same secret-read + verify + init flow as Path 1. - -### Path 3 (PGLite local) - -```bash -# gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — code -# retrieval beats general-purpose embeddings on real code queries (validated -# A/B). Without the key, gbrain auto-selects (OpenAI 1536d when available). -# Never select gbrain's legacy zeroentropyai recipe for a new brain: the hosted -# API sunsets September 4, 2026 (#2365); the wireup helper warns existing installs. -set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) -if [ -n "${VOYAGE_API_KEY:-}" ]; then - set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 -fi -gbrain init --pglite --json "$@" -``` - -Done. No network, no secrets (beyond Voyage embedding API calls during sync, if -`VOYAGE_API_KEY` is set — ~$0.18 per 1M tokens, pennies per repo). - -### Path 4 (Remote gbrain MCP — HTTP transport with bearer token) - -For users whose brain runs on another machine (Tailscale, ngrok, internal -LAN, or a teammate's server). No local gbrain CLI install, no local DB. -This skill registers the remote MCP and stops; ingestion + indexing happens -on the brain host. - -**4a. Collect MCP URL.** Prompt the user: - -``` -Paste your gbrain MCP URL (e.g. https://wintermute.tail554574.ts.net:3131/mcp): -``` - -Read with plain `read -r` (no secret hygiene needed — the URL alone isn't -a credential). Validate it starts with `https://` (require TLS for any -non-loopback host); refuse `http://` for non-localhost. - -**4b. Collect bearer token via the secret-read helper (D10, never argv).** - -```bash -. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh -read_secret_to_env GBRAIN_MCP_TOKEN "Paste bearer token: " \ - --echo-redacted 's/.\{6\}$/***REDACTED***/' -``` - -**4c. Verify via gstack-gbrain-mcp-verify.** Run the helper; capture the -classified JSON output: - -```bash -verify_json=$(GBRAIN_MCP_TOKEN="$GBRAIN_MCP_TOKEN" \ - ~/.claude/skills/gstack/bin/gstack-gbrain-mcp-verify "$MCP_URL") -status=$(echo "$verify_json" | jq -r .status) -``` - -If `status != "success"`, the helper has already classified the failure -into NETWORK / AUTH / MALFORMED and emitted a one-line remediation hint. -Surface the hint above the raw error from `error_text` and **STOP** with -a clear "fix and re-run /setup-gbrain" message. Do NOT continue to Step 5a -on a failed verify — partial registration would leave the user with a -half-broken state. - -Capture two values from the verify output for downstream steps: -- `SERVER_VERSION` (e.g., `0.27.1`) — written to the CLAUDE.md block in Step 8. -- `URL_FORM_SUPPORTED` (`true|false`) — passed to `gstack-artifacts-init` in - Step 7 to control which form of the brain-admin hookup command is printed. - -**4d. (Path 4) Offer local PGLite for code search.** Per plan D10/D11, ask: - -> D# — Want symbol-aware code search on this machine? -> Project/branch/task: -> ELI10: The remote brain at `` is great for cross-machine knowledge, -> but symbol queries like `gbrain code-def` / `code-refs` / `code-callers` need -> a local index of THIS machine's code. We can spin up a tiny isolated PGLite -> database (~30 seconds, no accounts, ~120 MB disk) just for code, separate -> from your remote brain. Transcripts and artifacts continue routing through -> the artifacts repo to the remote brain — local PGLite stays code-only. -> Stakes: without it, semantic code search in this repo's worktrees falls -> back to Grep. -> Recommendation: A — 30 seconds, no ongoing cost, unlocks the symbol tools. -> Completeness: A=10/10 (full split-engine), B=7/10 (remote-only). -> A) Yes, set up local PGLite for code (recommended) -> ✅ Unlocks `gbrain code-def`, `code-refs`, `code-callers` per worktree -> ✅ Independent engine — won't disturb remote brain or share transcripts -> B) No, remote MCP only -> ✅ Zero local state — only `~/.claude.json` MCP registration -> ❌ Symbol code queries fall back to Grep in this repo's worktrees -> Net: A = full split-engine; B = remote-only. - -**If A (Yes)**: install + init local PGLite with rollback-safe semantics (D7): - -```bash -~/.claude/skills/gstack/bin/gstack-gbrain-install || exit $? -# At this point the local gbrain CLI is on PATH. Init PGLite, but back up any -# existing ~/.gbrain/config.json first (rollback if init fails). -if [ -f "$HOME/.gbrain/config.json" ]; then - BACKUP="$HOME/.gbrain/config.json.gstack-bak-$(date +%s)" - mv "$HOME/.gbrain/config.json" "$BACKUP" -fi -# gstack default for local code-search PGLite: voyage-code-3 (1024d) when -# VOYAGE_API_KEY is set. It wins the A/B over voyage-4-large and OpenAI -# text-embedding-3-large on this codebase's symbol queries. Falls back to -# gbrain's auto-selected provider when the key isn't present. -set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) -if [ -n "${VOYAGE_API_KEY:-}" ]; then - set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 -fi -if ! gbrain init --pglite --json "$@"; then - if [ -n "${BACKUP:-}" ] && [ -f "$BACKUP" ]; then mv "$BACKUP" "$HOME/.gbrain/config.json"; fi - echo "gbrain init failed. Existing config (if any) was restored. PGLite at ~/.gbrain/pglite/ may be in a partial state — \`rm -rf ~/.gbrain/pglite\` to reset." >&2 - echo "Continuing setup without local code search; you can re-run /setup-gbrain to retry." >&2 -fi -``` - -Then continue to Step 5a. The remote-http MCP registration in 5a runs as -today; the local PGLite is independent of MCP registration (Claude Code talks -to the remote brain via MCP for queries; `gbrain` CLI talks to local PGLite -for code-def/refs/callers). - -**If B (No)**: skip the install + init. The local engine stays absent. -`gbrain_local_status` will be `missing-config` (or `no-cli` if gbrain isn't -installed). `/sync-gbrain` will SKIP the code stage cleanly per plan D12. - -**4e. Skip Steps 3, 4 (other paths) and 5 (local doctor) when B was picked.** -When A was picked, Step 3 already ran (via gstack-gbrain-install) and Step 4 -already ran (via `gbrain init --pglite`); jump straight to Step 5a. When B -was picked, Steps 3/4/5 are no-ops; also skip Step 7.5 (transcript ingest) -since memory-stage routes through the artifacts pipeline in remote-http mode -per plan D11. - -The bearer token (`GBRAIN_MCP_TOKEN`) stays in process env until Step 5a's -`claude mcp add --header` consumes it; then `unset GBRAIN_MCP_TOKEN` -immediately. Token security trade-off documented in -`setup-gbrain/memory.md`: brief argv exposure during `claude mcp add`, -resting state in `~/.claude.json` mode 0600. - -### Switch (from detect's existing-engine state) - -```bash -# Going PGLite → Supabase, collect URL first (Path 1 flow), then: -timeout 180s gbrain migrate --to supabase --url "$URL" --json -# Going Supabase → PGLite: -timeout 180s gbrain migrate --to pglite --json -``` - -If `timeout` returns 124 (exit code for timeout): surface D9 message -("Migration didn't complete in 3 minutes — another gstack session may be -holding a lock on the source brain. Close other workspaces and re-run -`/setup-gbrain --switch`. Your original brain is untouched."). STOP. +{{SECTION:brain-init}} --- @@ -731,154 +410,19 @@ this machine's transcripts indexed, they pull from your `gstack-artifacts-$USER` repo (set up in Step 7) on whatever schedule they prefer. Set `gstack-config set transcript_ingest_mode off` and continue to Step 8. -For Paths 1, 2a, 2b, 3: +For Paths 1, 2a, 2b, 3, run the ingest gate: -After memory sync is wired (Step 7) but before persisting the CLAUDE.md -config (Step 8), offer to bring this Mac's coding-agent transcripts + -curated `~/.gstack/` artifacts into gbrain so the retrieval surface -(per-skill manifests, salience block) has data to surface. - -Run the probe to size the operation: -```bash -bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe -``` - -Read the output. If `Total files in window: 0`, skip — there's nothing -to ingest. Set `gstack-config set transcript_ingest_mode incremental` -silently and continue to Step 8. - -If `New (never ingested)` is < 200 AND total bytes are < 100MB: silent -bulk via `bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --bulk --quiet`. Set -`transcript_ingest_mode=incremental` and continue. - -Otherwise (the "many transcripts on disk" path): AskUserQuestion with -the exact counts AND the value promise. Default scope is **current repo -only, last 90 days**: - -> "Found transcripts in THIS repo () over the last -> 90 days, plus across other repos on this machine ( -> total if all ingested). Ingest THIS repo's transcripts into gbrain? -> -> What you get after this: every gstack skill auto-loads recent salience -> from your past sessions in this repo, so the agent finds your prior -> work without you describing it. You can query 'what was I doing on -> day X' and get a real answer. Per-session pages are searchable, -> taggable, and deletable. Secret scanning runs before any push. -> -> What stays the same: nothing leaves your machine unless gbrain sync -> is enabled (Step 7). Per-repo trust policies still apply. -> -> Multi-Mac note: if you HAVE enabled brain sync (Step 7), these -> transcript pages will sync across your Macs. Caveat: deleting a -> transcript page later removes it from gbrain but git history retains -> it in prior commits. Use `gstack-transcript-prune` to delete in bulk; -> use `git filter-repo` on the brain remote for hard-delete from -> history." - -Options: -- A) Yes — this repo, last 90 days (recommended; ~est min) -- B) Yes — this repo, ALL history -- C) Yes — this repo + other repos on this machine -- D) Skip historical, track new from now (`transcript_ingest_mode=incremental`) -- E) Never ingest transcripts (`transcript_ingest_mode=off`) - -After answer: -```bash -~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode -bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --full --no-brain-sync -``` -(`--no-brain-sync` because Step 7 already wired that path; this just -runs the code import + memory ingest stages. Brain-sync will run on the -next preamble hook.) - -If A/D/E, ingest is incremental from this point on; preamble-boundary -hook runs `bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --incremental --quiet` on every skill -start (cheap mtime fast-path). - -Reference doc for users: `setup-gbrain/memory.md` (linked from CLAUDE.md -Step 8). +{{SECTION:transcript-gate}} --- ## Step 8: Persist `## GBrain Configuration` in CLAUDE.md -Find-and-replace (or append) the section. Block format depends on mode: +CLAUDE.md is the audit trail: after a successful setup, persist the +configuration block. The exact block formats (remote-http vs local-stdio) and +the post-Step-9 Search Guidance write live in the claude-md-persist section. -### Path 4 (Remote MCP) - -```markdown -## GBrain Configuration (configured by /setup-gbrain) -- Mode: remote-http -- MCP URL: {MCP_URL} -- Server version: gbrain v{SERVER_VERSION} (from Step 4c verify) -- Setup date: {today} -- MCP registered: yes (user scope) -- Token: stored in ~/.claude.json (do not commit; never written to CLAUDE.md) -- Artifacts repo: {gstack_artifacts_remote URL or "none"} -- Artifacts sync: {off|artifacts-only|full} -- Current repo policy: {read-write|read-only|deny|unset} -``` - -The bearer token is **never** written to CLAUDE.md (CLAUDE.md is checked -in to git in many projects). It lives only in `~/.claude.json` where -`claude mcp add` placed it. - -### Paths 1, 2a, 2b, 3 (Local stdio) - -```markdown -## GBrain Configuration (configured by /setup-gbrain) -- Mode: local-stdio -- Engine: {pglite|postgres} -- Config file: ~/.gbrain/config.json (mode 0600) -- Setup date: {today} -- MCP registered: {yes/no} -- Artifacts sync: {off|artifacts-only|full} -- Current repo policy: {read-write|read-only|deny|unset} -``` - -**After Step 9 (smoke test) passes, also write the `## GBrain Search Guidance` -block** so the coding agent learns when to prefer `gbrain` over Grep. This -block is gated on the smoke test passing — write the Configuration block -first (so the user knows what state they're in even if the smoke test fails), -then return here after Step 9 and write the guidance block only if smoke -test succeeded. - -When Step 9 passes, find-and-replace (or append) this block. Use HTML-comment -delimiters so removal regex is unambiguous and never eats user content. The -block content is machine-AGNOSTIC — no engine type, no page counts, no -last-sync time. Machine state stays in the Configuration block above. - -```markdown -## GBrain Search Guidance (configured by /sync-gbrain) - - -GBrain is set up and synced on this machine. The agent should prefer gbrain -over Grep when the question is semantic or when you don't know the exact -identifier yet. Two indexed corpora available via the `gbrain` CLI: -- This repo's code (registered as `gstack-code-` source). -- `~/.gstack/` curated memory (registered as `gstack-brain-` source via - the existing federation pipeline). - -Prefer gbrain when: -- "Where is X handled?" / semantic intent, no exact string yet: - `gbrain search ""` or `gbrain query ""` -- "Where is symbol Y defined?" / symbol-based code questions: - `gbrain code-def ` or `gbrain code-refs ` -- "What calls Y?" / "What does Y depend on?": - `gbrain code-callers ` / `gbrain code-callees ` -- "What did we decide last time?" / past plans, retros, learnings: - `gbrain search "" --source gstack-brain-` - -Grep is still right for known exact strings, regex, multiline patterns, and -file globs. The brain auto-syncs incrementally on every gstack skill start. -Run `/sync-gbrain` to force-refresh, `/sync-gbrain --full` for full reindex. - - -``` - -If Step 9 smoke test fails, skip the guidance block write entirely. The user's -next `/sync-gbrain` run will re-evaluate capability and write the block when -the round-trip works. +{{SECTION:claude-md-persist}} --- @@ -1069,7 +613,8 @@ as markdown + git, recoverable manually via `gbrain import` from a clone. ## `/setup-gbrain --cleanup-orphans` (D20) -Re-collect a PAT (Step 4 path-2a scope disclosure), then: +Re-collect a PAT (show the Path 2a PAT scope disclosure — it lives in the +brain-init section; read that section if it isn't already loaded), then: ```bash # List user's Supabase projects (user has to pipe this through their own diff --git a/setup-gbrain/sections/brain-init.md b/setup-gbrain/sections/brain-init.md new file mode 100644 index 000000000..029ae6985 --- /dev/null +++ b/setup-gbrain/sections/brain-init.md @@ -0,0 +1,270 @@ + + +Path-specific. Run ONLY the sub-section below for the path picked in Step 2 +(or the Switch flow when Step 2 chose engine migration). + +### Path 1 (Supabase, existing URL) + +Source the secret-read helper, collect URL with `read -s` + redacted preview: + +```bash +. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh +read_secret_to_env GBRAIN_POOLER_URL "Paste Session Pooler URL: " \ + --echo-redacted 's#://[^@]*@#://***@#' +``` + +Then validate structurally: + +```bash +printf '%s' "$GBRAIN_POOLER_URL" | ~/.claude/skills/gstack/bin/gstack-gbrain-supabase-verify - +``` + +If the verify exit code is 3 (direct-connection URL), the verifier's own +message explains the fix; surface it and re-prompt for a Session Pooler URL. + +On success, hand off to gbrain via env var (D10, never argv): + +```bash +GBRAIN_DATABASE_URL="$GBRAIN_POOLER_URL" gbrain init --non-interactive --json +``` + +Then `unset GBRAIN_POOLER_URL GBRAIN_DATABASE_URL` immediately. The URL is +now persisted in `~/.gbrain/config.json` at mode 0600 by gbrain itself. + +### Path 2a (Supabase, auto-provision — D7) + +Show the D11 PAT scope disclosure verbatim BEFORE collecting the token: + +> *This Supabase Personal Access Token grants full read/write/delete access +> to every project in your Supabase account, not just the `gbrain` one we're +> about to create. Supabase doesn't currently support scoped tokens. We use +> this PAT only to: create one project, poll it until healthy, read the +> Session Pooler URL — then discard it from process memory. The token +> remains valid on Supabase's side until you manually revoke it at +> https://supabase.com/dashboard/account/tokens — we recommend revoking +> immediately after setup completes.* + +Then: + +```bash +. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh +read_secret_to_env SUPABASE_ACCESS_TOKEN "Paste PAT: " +``` + +Ask the D17 tier prompt via AskUserQuestion: "Which Supabase tier?" Present +Free (2-project limit, pauses after 7d inactivity) vs Pro ($25/mo, no +pauses, recommended for real use). Explain that tier is **org-level** (per +the Management API contract) — user picks their org based on its current +tier. Pro may require them to upgrade the org first at supabase.com. + +List orgs, pick one (AskUserQuestion if multiple): + +```bash +orgs=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision list-orgs --json) +``` + +If the `.orgs` array is empty, surface: "Your Supabase account has no +organizations. Create one at https://supabase.com/dashboard, then re-run +`/setup-gbrain`." STOP. + +Ask the user for a region (default `us-east-1`; valid values are the 18 +enum values in the Supabase Management API — list a few common ones, let +them pick "Other" for a full list). + +Generate the DB password (never shown to the user): + +```bash +export DB_PASS=$(openssl rand -base64 24) +``` + +Set up a SIGINT trap (D12 basic recovery): + +```bash +trap 'echo ""; echo "gstack-gbrain: interrupted. In-flight ref: $INFLIGHT_REF"; \ + echo "Resume: /setup-gbrain --resume-provision $INFLIGHT_REF"; \ + echo "Delete: https://supabase.com/dashboard/project/$INFLIGHT_REF"; \ + unset SUPABASE_ACCESS_TOKEN DB_PASS; exit 130' INT TERM +``` + +Create + wait + fetch: + +```bash +result=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision \ + create gbrain "$REGION" "$ORG_SLUG" --json) +INFLIGHT_REF=$(echo "$result" | jq -r .ref) +~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision wait "$INFLIGHT_REF" --json +pooler=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision \ + pooler-url "$INFLIGHT_REF" --json) +GBRAIN_DATABASE_URL=$(echo "$pooler" | jq -r .pooler_url) +export GBRAIN_DATABASE_URL +gbrain init --non-interactive --json +unset SUPABASE_ACCESS_TOKEN DB_PASS GBRAIN_DATABASE_URL INFLIGHT_REF +trap - INT TERM +``` + +After success, emit the PAT revocation reminder: + +> "Setup complete. Revoke the PAT you pasted at +> https://supabase.com/dashboard/account/tokens — we've already discarded +> it from memory and don't need it again. The gbrain project will continue +> working because it uses its own embedded database password." + +### Path 2b (Supabase, manual) + +Walk the user through the supabase.com steps: +1. Login at https://supabase.com/dashboard +2. Click "New Project," name it `gbrain`, pick a region, copy the generated + database password (you'll need it for paste-back? no — it's embedded in + the pooler URL we collect next) +3. Wait ~2 min for the project to initialize +4. Settings → Database → Connection Pooler → Session → copy the URL (port + 6543) + +Then follow the same secret-read + verify + init flow as Path 1. + +### Path 3 (PGLite local) + +```bash +# gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — code +# retrieval beats general-purpose embeddings on real code queries (validated +# A/B). Without the key, gbrain auto-selects (OpenAI 1536d when available). +# Never select gbrain's legacy zeroentropyai recipe for a new brain: the hosted +# API sunsets September 4, 2026 (#2365); the wireup helper warns existing installs. +set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) +if [ -n "${VOYAGE_API_KEY:-}" ]; then + set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 +fi +gbrain init --pglite --json "$@" +``` + +Done. No network, no secrets (beyond Voyage embedding API calls during sync, if +`VOYAGE_API_KEY` is set — ~$0.18 per 1M tokens, pennies per repo). + +### Path 4 (Remote gbrain MCP — HTTP transport with bearer token) + +For users whose brain runs on another machine (Tailscale, ngrok, internal +LAN, or a teammate's server). No local gbrain CLI install, no local DB. +This skill registers the remote MCP and stops; ingestion + indexing happens +on the brain host. + +**4a. Collect MCP URL.** Prompt the user: + +``` +Paste your gbrain MCP URL (e.g. https://wintermute.tail554574.ts.net:3131/mcp): +``` + +Read with plain `read -r` (no secret hygiene needed — the URL alone isn't +a credential). Validate it starts with `https://` (require TLS for any +non-loopback host); refuse `http://` for non-localhost. + +**4b. Collect bearer token via the secret-read helper (D10, never argv).** + +```bash +. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh +read_secret_to_env GBRAIN_MCP_TOKEN "Paste bearer token: " \ + --echo-redacted 's/.\{6\}$/***REDACTED***/' +``` + +**4c. Verify via gstack-gbrain-mcp-verify.** Run the helper; capture the +classified JSON output: + +```bash +verify_json=$(GBRAIN_MCP_TOKEN="$GBRAIN_MCP_TOKEN" \ + ~/.claude/skills/gstack/bin/gstack-gbrain-mcp-verify "$MCP_URL") +status=$(echo "$verify_json" | jq -r .status) +``` + +If `status != "success"`, the helper has already classified the failure +into NETWORK / AUTH / MALFORMED and emitted a one-line remediation hint. +Surface the hint above the raw error from `error_text` and **STOP** with +a clear "fix and re-run /setup-gbrain" message. Do NOT continue to Step 5a +on a failed verify — partial registration would leave the user with a +half-broken state. + +Capture two values from the verify output for downstream steps: +- `SERVER_VERSION` (e.g., `0.27.1`) — written to the CLAUDE.md block in Step 8. +- `URL_FORM_SUPPORTED` (`true|false`) — passed to `gstack-artifacts-init` in + Step 7 to control which form of the brain-admin hookup command is printed. + +**4d. (Path 4) Offer local PGLite for code search.** Per plan D10/D11, ask: + +> D# — Want symbol-aware code search on this machine? +> Project/branch/task: +> ELI10: The remote brain at `` is great for cross-machine knowledge, +> but symbol queries like `gbrain code-def` / `code-refs` / `code-callers` need +> a local index of THIS machine's code. We can spin up a tiny isolated PGLite +> database (~30 seconds, no accounts, ~120 MB disk) just for code, separate +> from your remote brain. Transcripts and artifacts continue routing through +> the artifacts repo to the remote brain — local PGLite stays code-only. +> Stakes: without it, semantic code search in this repo's worktrees falls +> back to Grep. +> Recommendation: A — 30 seconds, no ongoing cost, unlocks the symbol tools. +> Completeness: A=10/10 (full split-engine), B=7/10 (remote-only). +> A) Yes, set up local PGLite for code (recommended) +> ✅ Unlocks `gbrain code-def`, `code-refs`, `code-callers` per worktree +> ✅ Independent engine — won't disturb remote brain or share transcripts +> B) No, remote MCP only +> ✅ Zero local state — only `~/.claude.json` MCP registration +> ❌ Symbol code queries fall back to Grep in this repo's worktrees +> Net: A = full split-engine; B = remote-only. + +**If A (Yes)**: install + init local PGLite with rollback-safe semantics (D7): + +```bash +~/.claude/skills/gstack/bin/gstack-gbrain-install || exit $? +# At this point the local gbrain CLI is on PATH. Init PGLite, but back up any +# existing ~/.gbrain/config.json first (rollback if init fails). +if [ -f "$HOME/.gbrain/config.json" ]; then + BACKUP="$HOME/.gbrain/config.json.gstack-bak-$(date +%s)" + mv "$HOME/.gbrain/config.json" "$BACKUP" +fi +# gstack default for local code-search PGLite: voyage-code-3 (1024d) when +# VOYAGE_API_KEY is set. It wins the A/B over voyage-4-large and OpenAI +# text-embedding-3-large on this codebase's symbol queries. Falls back to +# gbrain's auto-selected provider when the key isn't present. +set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) +if [ -n "${VOYAGE_API_KEY:-}" ]; then + set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 +fi +if ! gbrain init --pglite --json "$@"; then + if [ -n "${BACKUP:-}" ] && [ -f "$BACKUP" ]; then mv "$BACKUP" "$HOME/.gbrain/config.json"; fi + echo "gbrain init failed. Existing config (if any) was restored. PGLite at ~/.gbrain/pglite/ may be in a partial state — \`rm -rf ~/.gbrain/pglite\` to reset." >&2 + echo "Continuing setup without local code search; you can re-run /setup-gbrain to retry." >&2 +fi +``` + +Then continue to Step 5a. The remote-http MCP registration in 5a runs as +today; the local PGLite is independent of MCP registration (Claude Code talks +to the remote brain via MCP for queries; `gbrain` CLI talks to local PGLite +for code-def/refs/callers). + +**If B (No)**: skip the install + init. The local engine stays absent. +`gbrain_local_status` will be `missing-config` (or `no-cli` if gbrain isn't +installed). `/sync-gbrain` will SKIP the code stage cleanly per plan D12. + +**4e. Skip Steps 3, 4 (other paths) and 5 (local doctor) when B was picked.** +When A was picked, Step 3 already ran (via gstack-gbrain-install) and Step 4 +already ran (via `gbrain init --pglite`); jump straight to Step 5a. When B +was picked, Steps 3/4/5 are no-ops; also skip Step 7.5 (transcript ingest) +since memory-stage routes through the artifacts pipeline in remote-http mode +per plan D11. + +The bearer token (`GBRAIN_MCP_TOKEN`) stays in process env until Step 5a's +`claude mcp add --header` consumes it; then `unset GBRAIN_MCP_TOKEN` +immediately. Token security trade-off documented in +`setup-gbrain/memory.md`: brief argv exposure during `claude mcp add`, +resting state in `~/.claude.json` mode 0600. + +### Switch (from detect's existing-engine state) + +```bash +# Going PGLite → Supabase, collect URL first (Path 1 flow), then: +timeout 180s gbrain migrate --to supabase --url "$URL" --json +# Going Supabase → PGLite: +timeout 180s gbrain migrate --to pglite --json +``` + +If `timeout` returns 124 (exit code for timeout): surface D9 message +("Migration didn't complete in 3 minutes — another gstack session may be +holding a lock on the source brain. Close other workspaces and re-run +`/setup-gbrain --switch`. Your original brain is untouched."). STOP. diff --git a/setup-gbrain/sections/brain-init.md.tmpl b/setup-gbrain/sections/brain-init.md.tmpl new file mode 100644 index 000000000..70316c503 --- /dev/null +++ b/setup-gbrain/sections/brain-init.md.tmpl @@ -0,0 +1,268 @@ +Path-specific. Run ONLY the sub-section below for the path picked in Step 2 +(or the Switch flow when Step 2 chose engine migration). + +### Path 1 (Supabase, existing URL) + +Source the secret-read helper, collect URL with `read -s` + redacted preview: + +```bash +. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh +read_secret_to_env GBRAIN_POOLER_URL "Paste Session Pooler URL: " \ + --echo-redacted 's#://[^@]*@#://***@#' +``` + +Then validate structurally: + +```bash +printf '%s' "$GBRAIN_POOLER_URL" | ~/.claude/skills/gstack/bin/gstack-gbrain-supabase-verify - +``` + +If the verify exit code is 3 (direct-connection URL), the verifier's own +message explains the fix; surface it and re-prompt for a Session Pooler URL. + +On success, hand off to gbrain via env var (D10, never argv): + +```bash +GBRAIN_DATABASE_URL="$GBRAIN_POOLER_URL" gbrain init --non-interactive --json +``` + +Then `unset GBRAIN_POOLER_URL GBRAIN_DATABASE_URL` immediately. The URL is +now persisted in `~/.gbrain/config.json` at mode 0600 by gbrain itself. + +### Path 2a (Supabase, auto-provision — D7) + +Show the D11 PAT scope disclosure verbatim BEFORE collecting the token: + +> *This Supabase Personal Access Token grants full read/write/delete access +> to every project in your Supabase account, not just the `gbrain` one we're +> about to create. Supabase doesn't currently support scoped tokens. We use +> this PAT only to: create one project, poll it until healthy, read the +> Session Pooler URL — then discard it from process memory. The token +> remains valid on Supabase's side until you manually revoke it at +> https://supabase.com/dashboard/account/tokens — we recommend revoking +> immediately after setup completes.* + +Then: + +```bash +. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh +read_secret_to_env SUPABASE_ACCESS_TOKEN "Paste PAT: " +``` + +Ask the D17 tier prompt via AskUserQuestion: "Which Supabase tier?" Present +Free (2-project limit, pauses after 7d inactivity) vs Pro ($25/mo, no +pauses, recommended for real use). Explain that tier is **org-level** (per +the Management API contract) — user picks their org based on its current +tier. Pro may require them to upgrade the org first at supabase.com. + +List orgs, pick one (AskUserQuestion if multiple): + +```bash +orgs=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision list-orgs --json) +``` + +If the `.orgs` array is empty, surface: "Your Supabase account has no +organizations. Create one at https://supabase.com/dashboard, then re-run +`/setup-gbrain`." STOP. + +Ask the user for a region (default `us-east-1`; valid values are the 18 +enum values in the Supabase Management API — list a few common ones, let +them pick "Other" for a full list). + +Generate the DB password (never shown to the user): + +```bash +export DB_PASS=$(openssl rand -base64 24) +``` + +Set up a SIGINT trap (D12 basic recovery): + +```bash +trap 'echo ""; echo "gstack-gbrain: interrupted. In-flight ref: $INFLIGHT_REF"; \ + echo "Resume: /setup-gbrain --resume-provision $INFLIGHT_REF"; \ + echo "Delete: https://supabase.com/dashboard/project/$INFLIGHT_REF"; \ + unset SUPABASE_ACCESS_TOKEN DB_PASS; exit 130' INT TERM +``` + +Create + wait + fetch: + +```bash +result=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision \ + create gbrain "$REGION" "$ORG_SLUG" --json) +INFLIGHT_REF=$(echo "$result" | jq -r .ref) +~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision wait "$INFLIGHT_REF" --json +pooler=$(~/.claude/skills/gstack/bin/gstack-gbrain-supabase-provision \ + pooler-url "$INFLIGHT_REF" --json) +GBRAIN_DATABASE_URL=$(echo "$pooler" | jq -r .pooler_url) +export GBRAIN_DATABASE_URL +gbrain init --non-interactive --json +unset SUPABASE_ACCESS_TOKEN DB_PASS GBRAIN_DATABASE_URL INFLIGHT_REF +trap - INT TERM +``` + +After success, emit the PAT revocation reminder: + +> "Setup complete. Revoke the PAT you pasted at +> https://supabase.com/dashboard/account/tokens — we've already discarded +> it from memory and don't need it again. The gbrain project will continue +> working because it uses its own embedded database password." + +### Path 2b (Supabase, manual) + +Walk the user through the supabase.com steps: +1. Login at https://supabase.com/dashboard +2. Click "New Project," name it `gbrain`, pick a region, copy the generated + database password (you'll need it for paste-back? no — it's embedded in + the pooler URL we collect next) +3. Wait ~2 min for the project to initialize +4. Settings → Database → Connection Pooler → Session → copy the URL (port + 6543) + +Then follow the same secret-read + verify + init flow as Path 1. + +### Path 3 (PGLite local) + +```bash +# gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — code +# retrieval beats general-purpose embeddings on real code queries (validated +# A/B). Without the key, gbrain auto-selects (OpenAI 1536d when available). +# Never select gbrain's legacy zeroentropyai recipe for a new brain: the hosted +# API sunsets September 4, 2026 (#2365); the wireup helper warns existing installs. +set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) +if [ -n "${VOYAGE_API_KEY:-}" ]; then + set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 +fi +gbrain init --pglite --json "$@" +``` + +Done. No network, no secrets (beyond Voyage embedding API calls during sync, if +`VOYAGE_API_KEY` is set — ~$0.18 per 1M tokens, pennies per repo). + +### Path 4 (Remote gbrain MCP — HTTP transport with bearer token) + +For users whose brain runs on another machine (Tailscale, ngrok, internal +LAN, or a teammate's server). No local gbrain CLI install, no local DB. +This skill registers the remote MCP and stops; ingestion + indexing happens +on the brain host. + +**4a. Collect MCP URL.** Prompt the user: + +``` +Paste your gbrain MCP URL (e.g. https://wintermute.tail554574.ts.net:3131/mcp): +``` + +Read with plain `read -r` (no secret hygiene needed — the URL alone isn't +a credential). Validate it starts with `https://` (require TLS for any +non-loopback host); refuse `http://` for non-localhost. + +**4b. Collect bearer token via the secret-read helper (D10, never argv).** + +```bash +. ~/.claude/skills/gstack/bin/gstack-gbrain-lib.sh +read_secret_to_env GBRAIN_MCP_TOKEN "Paste bearer token: " \ + --echo-redacted 's/.\{6\}$/***REDACTED***/' +``` + +**4c. Verify via gstack-gbrain-mcp-verify.** Run the helper; capture the +classified JSON output: + +```bash +verify_json=$(GBRAIN_MCP_TOKEN="$GBRAIN_MCP_TOKEN" \ + ~/.claude/skills/gstack/bin/gstack-gbrain-mcp-verify "$MCP_URL") +status=$(echo "$verify_json" | jq -r .status) +``` + +If `status != "success"`, the helper has already classified the failure +into NETWORK / AUTH / MALFORMED and emitted a one-line remediation hint. +Surface the hint above the raw error from `error_text` and **STOP** with +a clear "fix and re-run /setup-gbrain" message. Do NOT continue to Step 5a +on a failed verify — partial registration would leave the user with a +half-broken state. + +Capture two values from the verify output for downstream steps: +- `SERVER_VERSION` (e.g., `0.27.1`) — written to the CLAUDE.md block in Step 8. +- `URL_FORM_SUPPORTED` (`true|false`) — passed to `gstack-artifacts-init` in + Step 7 to control which form of the brain-admin hookup command is printed. + +**4d. (Path 4) Offer local PGLite for code search.** Per plan D10/D11, ask: + +> D# — Want symbol-aware code search on this machine? +> Project/branch/task: +> ELI10: The remote brain at `` is great for cross-machine knowledge, +> but symbol queries like `gbrain code-def` / `code-refs` / `code-callers` need +> a local index of THIS machine's code. We can spin up a tiny isolated PGLite +> database (~30 seconds, no accounts, ~120 MB disk) just for code, separate +> from your remote brain. Transcripts and artifacts continue routing through +> the artifacts repo to the remote brain — local PGLite stays code-only. +> Stakes: without it, semantic code search in this repo's worktrees falls +> back to Grep. +> Recommendation: A — 30 seconds, no ongoing cost, unlocks the symbol tools. +> Completeness: A=10/10 (full split-engine), B=7/10 (remote-only). +> A) Yes, set up local PGLite for code (recommended) +> ✅ Unlocks `gbrain code-def`, `code-refs`, `code-callers` per worktree +> ✅ Independent engine — won't disturb remote brain or share transcripts +> B) No, remote MCP only +> ✅ Zero local state — only `~/.claude.json` MCP registration +> ❌ Symbol code queries fall back to Grep in this repo's worktrees +> Net: A = full split-engine; B = remote-only. + +**If A (Yes)**: install + init local PGLite with rollback-safe semantics (D7): + +```bash +~/.claude/skills/gstack/bin/gstack-gbrain-install || exit $? +# At this point the local gbrain CLI is on PATH. Init PGLite, but back up any +# existing ~/.gbrain/config.json first (rollback if init fails). +if [ -f "$HOME/.gbrain/config.json" ]; then + BACKUP="$HOME/.gbrain/config.json.gstack-bak-$(date +%s)" + mv "$HOME/.gbrain/config.json" "$BACKUP" +fi +# gstack default for local code-search PGLite: voyage-code-3 (1024d) when +# VOYAGE_API_KEY is set. It wins the A/B over voyage-4-large and OpenAI +# text-embedding-3-large on this codebase's symbol queries. Falls back to +# gbrain's auto-selected provider when the key isn't present. +set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) +if [ -n "${VOYAGE_API_KEY:-}" ]; then + set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 +fi +if ! gbrain init --pglite --json "$@"; then + if [ -n "${BACKUP:-}" ] && [ -f "$BACKUP" ]; then mv "$BACKUP" "$HOME/.gbrain/config.json"; fi + echo "gbrain init failed. Existing config (if any) was restored. PGLite at ~/.gbrain/pglite/ may be in a partial state — \`rm -rf ~/.gbrain/pglite\` to reset." >&2 + echo "Continuing setup without local code search; you can re-run /setup-gbrain to retry." >&2 +fi +``` + +Then continue to Step 5a. The remote-http MCP registration in 5a runs as +today; the local PGLite is independent of MCP registration (Claude Code talks +to the remote brain via MCP for queries; `gbrain` CLI talks to local PGLite +for code-def/refs/callers). + +**If B (No)**: skip the install + init. The local engine stays absent. +`gbrain_local_status` will be `missing-config` (or `no-cli` if gbrain isn't +installed). `/sync-gbrain` will SKIP the code stage cleanly per plan D12. + +**4e. Skip Steps 3, 4 (other paths) and 5 (local doctor) when B was picked.** +When A was picked, Step 3 already ran (via gstack-gbrain-install) and Step 4 +already ran (via `gbrain init --pglite`); jump straight to Step 5a. When B +was picked, Steps 3/4/5 are no-ops; also skip Step 7.5 (transcript ingest) +since memory-stage routes through the artifacts pipeline in remote-http mode +per plan D11. + +The bearer token (`GBRAIN_MCP_TOKEN`) stays in process env until Step 5a's +`claude mcp add --header` consumes it; then `unset GBRAIN_MCP_TOKEN` +immediately. Token security trade-off documented in +`setup-gbrain/memory.md`: brief argv exposure during `claude mcp add`, +resting state in `~/.claude.json` mode 0600. + +### Switch (from detect's existing-engine state) + +```bash +# Going PGLite → Supabase, collect URL first (Path 1 flow), then: +timeout 180s gbrain migrate --to supabase --url "$URL" --json +# Going Supabase → PGLite: +timeout 180s gbrain migrate --to pglite --json +``` + +If `timeout` returns 124 (exit code for timeout): surface D9 message +("Migration didn't complete in 3 minutes — another gstack session may be +holding a lock on the source brain. Close other workspaces and re-run +`/setup-gbrain --switch`. Your original brain is untouched."). STOP. diff --git a/setup-gbrain/sections/claude-md-persist.md b/setup-gbrain/sections/claude-md-persist.md new file mode 100644 index 000000000..2e15cbcfd --- /dev/null +++ b/setup-gbrain/sections/claude-md-persist.md @@ -0,0 +1,79 @@ + + +Find-and-replace (or append) the section. Block format depends on mode: + +### Path 4 (Remote MCP) + +```markdown +## GBrain Configuration (configured by /setup-gbrain) +- Mode: remote-http +- MCP URL: {MCP_URL} +- Server version: gbrain v{SERVER_VERSION} (from Step 4c verify) +- Setup date: {today} +- MCP registered: yes (user scope) +- Token: stored in ~/.claude.json (do not commit; never written to CLAUDE.md) +- Artifacts repo: {gstack_artifacts_remote URL or "none"} +- Artifacts sync: {off|artifacts-only|full} +- Current repo policy: {read-write|read-only|deny|unset} +``` + +The bearer token is **never** written to CLAUDE.md (CLAUDE.md is checked +in to git in many projects). It lives only in `~/.claude.json` where +`claude mcp add` placed it. + +### Paths 1, 2a, 2b, 3 (Local stdio) + +```markdown +## GBrain Configuration (configured by /setup-gbrain) +- Mode: local-stdio +- Engine: {pglite|postgres} +- Config file: ~/.gbrain/config.json (mode 0600) +- Setup date: {today} +- MCP registered: {yes/no} +- Artifacts sync: {off|artifacts-only|full} +- Current repo policy: {read-write|read-only|deny|unset} +``` + +**After Step 9 (smoke test) passes, also write the `## GBrain Search Guidance` +block** so the coding agent learns when to prefer `gbrain` over Grep. This +block is gated on the smoke test passing — write the Configuration block +first (so the user knows what state they're in even if the smoke test fails), +then return here after Step 9 and write the guidance block only if smoke +test succeeded. + +When Step 9 passes, find-and-replace (or append) this block. Use HTML-comment +delimiters so removal regex is unambiguous and never eats user content. The +block content is machine-AGNOSTIC — no engine type, no page counts, no +last-sync time. Machine state stays in the Configuration block above. + +```markdown +## GBrain Search Guidance (configured by /sync-gbrain) + + +GBrain is set up and synced on this machine. The agent should prefer gbrain +over Grep when the question is semantic or when you don't know the exact +identifier yet. Two indexed corpora available via the `gbrain` CLI: +- This repo's code (registered as `gstack-code-` source). +- `~/.gstack/` curated memory (registered as `gstack-brain-` source via + the existing federation pipeline). + +Prefer gbrain when: +- "Where is X handled?" / semantic intent, no exact string yet: + `gbrain search ""` or `gbrain query ""` +- "Where is symbol Y defined?" / symbol-based code questions: + `gbrain code-def ` or `gbrain code-refs ` +- "What calls Y?" / "What does Y depend on?": + `gbrain code-callers ` / `gbrain code-callees ` +- "What did we decide last time?" / past plans, retros, learnings: + `gbrain search "" --source gstack-brain-` + +Grep is still right for known exact strings, regex, multiline patterns, and +file globs. The brain auto-syncs incrementally on every gstack skill start. +Run `/sync-gbrain` to force-refresh, `/sync-gbrain --full` for full reindex. + + +``` + +If Step 9 smoke test fails, skip the guidance block write entirely. The user's +next `/sync-gbrain` run will re-evaluate capability and write the block when +the round-trip works. diff --git a/setup-gbrain/sections/claude-md-persist.md.tmpl b/setup-gbrain/sections/claude-md-persist.md.tmpl new file mode 100644 index 000000000..de6ad18b1 --- /dev/null +++ b/setup-gbrain/sections/claude-md-persist.md.tmpl @@ -0,0 +1,77 @@ +Find-and-replace (or append) the section. Block format depends on mode: + +### Path 4 (Remote MCP) + +```markdown +## GBrain Configuration (configured by /setup-gbrain) +- Mode: remote-http +- MCP URL: {MCP_URL} +- Server version: gbrain v{SERVER_VERSION} (from Step 4c verify) +- Setup date: {today} +- MCP registered: yes (user scope) +- Token: stored in ~/.claude.json (do not commit; never written to CLAUDE.md) +- Artifacts repo: {gstack_artifacts_remote URL or "none"} +- Artifacts sync: {off|artifacts-only|full} +- Current repo policy: {read-write|read-only|deny|unset} +``` + +The bearer token is **never** written to CLAUDE.md (CLAUDE.md is checked +in to git in many projects). It lives only in `~/.claude.json` where +`claude mcp add` placed it. + +### Paths 1, 2a, 2b, 3 (Local stdio) + +```markdown +## GBrain Configuration (configured by /setup-gbrain) +- Mode: local-stdio +- Engine: {pglite|postgres} +- Config file: ~/.gbrain/config.json (mode 0600) +- Setup date: {today} +- MCP registered: {yes/no} +- Artifacts sync: {off|artifacts-only|full} +- Current repo policy: {read-write|read-only|deny|unset} +``` + +**After Step 9 (smoke test) passes, also write the `## GBrain Search Guidance` +block** so the coding agent learns when to prefer `gbrain` over Grep. This +block is gated on the smoke test passing — write the Configuration block +first (so the user knows what state they're in even if the smoke test fails), +then return here after Step 9 and write the guidance block only if smoke +test succeeded. + +When Step 9 passes, find-and-replace (or append) this block. Use HTML-comment +delimiters so removal regex is unambiguous and never eats user content. The +block content is machine-AGNOSTIC — no engine type, no page counts, no +last-sync time. Machine state stays in the Configuration block above. + +```markdown +## GBrain Search Guidance (configured by /sync-gbrain) + + +GBrain is set up and synced on this machine. The agent should prefer gbrain +over Grep when the question is semantic or when you don't know the exact +identifier yet. Two indexed corpora available via the `gbrain` CLI: +- This repo's code (registered as `gstack-code-` source). +- `~/.gstack/` curated memory (registered as `gstack-brain-` source via + the existing federation pipeline). + +Prefer gbrain when: +- "Where is X handled?" / semantic intent, no exact string yet: + `gbrain search ""` or `gbrain query ""` +- "Where is symbol Y defined?" / symbol-based code questions: + `gbrain code-def ` or `gbrain code-refs ` +- "What calls Y?" / "What does Y depend on?": + `gbrain code-callers ` / `gbrain code-callees ` +- "What did we decide last time?" / past plans, retros, learnings: + `gbrain search "" --source gstack-brain-` + +Grep is still right for known exact strings, regex, multiline patterns, and +file globs. The brain auto-syncs incrementally on every gstack skill start. +Run `/sync-gbrain` to force-refresh, `/sync-gbrain --full` for full reindex. + + +``` + +If Step 9 smoke test fails, skip the guidance block write entirely. The user's +next `/sync-gbrain` run will re-evaluate capability and write the block when +the round-trip works. diff --git a/setup-gbrain/sections/engine-remediation.md b/setup-gbrain/sections/engine-remediation.md new file mode 100644 index 000000000..361391108 --- /dev/null +++ b/setup-gbrain/sections/engine-remediation.md @@ -0,0 +1,71 @@ + + +### Step 1.5 remediation: broken-engine AskUserQuestion + repair branches + +The user has a non-working local engine (Garry's repro: `~/.gbrain/config.json` +points at a dead Postgres URL). Fire a targeted AskUserQuestion BEFORE Step 2: + +> D# — Your local gbrain engine isn't responding. How do you want to fix it? +> Project/branch/task: +> ELI10: gbrain has a config at `~/.gbrain/config.json` but the engine it points +> at isn't reachable. That could be a transient outage (Postgres container +> stopped, Tailscale down) OR a stale config you want to abandon. Different +> remediation for each case. +> Stakes if we pick wrong: "Switch to PGLite" overwrites your existing config +> (one-way door if the user actually wanted the broken engine). "Retry" preserves +> existing state for transient cases. +> Recommendation: A (Retry) — always try the cheap option first; if engine is +> just temporarily down it'll come back without any destructive change. +> Note: options differ in kind, not coverage — no completeness score. +> A) Retry — re-probe the engine (recommended; ~80ms) +> ✅ Cheapest test: re-runs `gbrain sources list` to see if engine is back +> ✅ Zero side effects; existing config preserved +> ❌ If engine is permanently dead, retries forever; user must choose another option +> B) Switch to local PGLite (one-way — moves existing config to .bak) +> ✅ Fastest path to a working local engine if user has abandoned the old one +> ✅ ~30s; no accounts; private to this machine +> ❌ Destructive — existing config moved to ~/.gbrain/config.json.gstack-bak-{ts} +> C) Switch brain mode (continue to Step 2 path picker) +> ✅ Lets user pick Path 1/2/3/4 to re-init from scratch +> ✅ Preserves existing config until they explicitly init the new one +> ❌ Longer flow if user just wants to repair to PGLite +> D) Quit (do nothing) +> ✅ No cons — this is a hard-stop choice +> ❌ N/A +> Net: A is the right starting move; B/C are explicit destructive paths; D bails. + +**If A (Retry)**: re-run `~/.claude/skills/gstack/bin/gstack-gbrain-detect` +with `GSTACK_DETECT_NO_CACHE=1` (busts the 60s cache). If the new +`gbrain_local_status` is `ok`, continue to Step 2. If still `broken-db` or +`broken-config`, fire the same AskUserQuestion again (the user picks again). + +**If B (Switch to PGLite)** — execute the rollback-safe init sequence (plan D7): + +```bash +BACKUP="$HOME/.gbrain/config.json.gstack-bak-$(date +%s)" +mv "$HOME/.gbrain/config.json" "$BACKUP" +# gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — best for +# code retrieval. Without the key, fall back to gbrain's own auto-selected +# embedding provider chain (OpenAI 1536d when OPENAI_API_KEY is present, etc.). +# Never select gbrain's legacy zeroentropyai recipe for a new brain: the hosted +# API sunsets September 4, 2026 (#2365); the wireup helper warns existing installs. +set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) +if [ -n "${VOYAGE_API_KEY:-}" ]; then + set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 +fi +if ! gbrain init --pglite --json "$@"; then + # Restore on failure + mv "$BACKUP" "$HOME/.gbrain/config.json" + echo "gbrain init failed. Your previous config was restored at $HOME/.gbrain/config.json." >&2 + echo "PGLite directory at ~/.gbrain/pglite/ may be in a partial state — \`rm -rf ~/.gbrain/pglite\` if needed before retrying." >&2 + exit 1 +fi +echo "Switched to local PGLite. Previous config saved at $BACKUP — review before deleting." +``` + +Then jump to Step 5a (MCP registration; the new PGLite engine is registered as +local-stdio). + +**If C (Switch brain mode)**: continue to Step 2's normal path picker. + +**If D (Quit)**: STOP the skill cleanly. diff --git a/setup-gbrain/sections/engine-remediation.md.tmpl b/setup-gbrain/sections/engine-remediation.md.tmpl new file mode 100644 index 000000000..a09b7f148 --- /dev/null +++ b/setup-gbrain/sections/engine-remediation.md.tmpl @@ -0,0 +1,69 @@ +### Step 1.5 remediation: broken-engine AskUserQuestion + repair branches + +The user has a non-working local engine (Garry's repro: `~/.gbrain/config.json` +points at a dead Postgres URL). Fire a targeted AskUserQuestion BEFORE Step 2: + +> D# — Your local gbrain engine isn't responding. How do you want to fix it? +> Project/branch/task: +> ELI10: gbrain has a config at `~/.gbrain/config.json` but the engine it points +> at isn't reachable. That could be a transient outage (Postgres container +> stopped, Tailscale down) OR a stale config you want to abandon. Different +> remediation for each case. +> Stakes if we pick wrong: "Switch to PGLite" overwrites your existing config +> (one-way door if the user actually wanted the broken engine). "Retry" preserves +> existing state for transient cases. +> Recommendation: A (Retry) — always try the cheap option first; if engine is +> just temporarily down it'll come back without any destructive change. +> Note: options differ in kind, not coverage — no completeness score. +> A) Retry — re-probe the engine (recommended; ~80ms) +> ✅ Cheapest test: re-runs `gbrain sources list` to see if engine is back +> ✅ Zero side effects; existing config preserved +> ❌ If engine is permanently dead, retries forever; user must choose another option +> B) Switch to local PGLite (one-way — moves existing config to .bak) +> ✅ Fastest path to a working local engine if user has abandoned the old one +> ✅ ~30s; no accounts; private to this machine +> ❌ Destructive — existing config moved to ~/.gbrain/config.json.gstack-bak-{ts} +> C) Switch brain mode (continue to Step 2 path picker) +> ✅ Lets user pick Path 1/2/3/4 to re-init from scratch +> ✅ Preserves existing config until they explicitly init the new one +> ❌ Longer flow if user just wants to repair to PGLite +> D) Quit (do nothing) +> ✅ No cons — this is a hard-stop choice +> ❌ N/A +> Net: A is the right starting move; B/C are explicit destructive paths; D bails. + +**If A (Retry)**: re-run `~/.claude/skills/gstack/bin/gstack-gbrain-detect` +with `GSTACK_DETECT_NO_CACHE=1` (busts the 60s cache). If the new +`gbrain_local_status` is `ok`, continue to Step 2. If still `broken-db` or +`broken-config`, fire the same AskUserQuestion again (the user picks again). + +**If B (Switch to PGLite)** — execute the rollback-safe init sequence (plan D7): + +```bash +BACKUP="$HOME/.gbrain/config.json.gstack-bak-$(date +%s)" +mv "$HOME/.gbrain/config.json" "$BACKUP" +# gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — best for +# code retrieval. Without the key, fall back to gbrain's own auto-selected +# embedding provider chain (OpenAI 1536d when OPENAI_API_KEY is present, etc.). +# Never select gbrain's legacy zeroentropyai recipe for a new brain: the hosted +# API sunsets September 4, 2026 (#2365); the wireup helper warns existing installs. +set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) +if [ -n "${VOYAGE_API_KEY:-}" ]; then + set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 +fi +if ! gbrain init --pglite --json "$@"; then + # Restore on failure + mv "$BACKUP" "$HOME/.gbrain/config.json" + echo "gbrain init failed. Your previous config was restored at $HOME/.gbrain/config.json." >&2 + echo "PGLite directory at ~/.gbrain/pglite/ may be in a partial state — \`rm -rf ~/.gbrain/pglite\` if needed before retrying." >&2 + exit 1 +fi +echo "Switched to local PGLite. Previous config saved at $BACKUP — review before deleting." +``` + +Then jump to Step 5a (MCP registration; the new PGLite engine is registered as +local-stdio). + +**If C (Switch brain mode)**: continue to Step 2's normal path picker. + +**If D (Quit)**: STOP the skill cleanly. diff --git a/setup-gbrain/sections/manifest.json b/setup-gbrain/sections/manifest.json new file mode 100644 index 000000000..e9cbf0c65 --- /dev/null +++ b/setup-gbrain/sections/manifest.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://gstack.dev/schemas/section-manifest.json", + "skill": "setup-gbrain", + "version": 1, + "note": "PASSIVE registry (v2 plan T9 / CM2). Fields are IDs, file paths, human titles, and human-readable trigger text ONLY. The skeleton's detect (Step 1) + path picker (Step 2) are the ONLY places that decide WHEN a section is read (the install routes are branch-exclusive — at most one init route runs per invocation); required-reads live in the E2E fixtures. No machine predicate here — see docs/designs/v2_PLAN.md:663.", + "sections": [ + { + "id": "engine-remediation", + "file": "engine-remediation.md", + "title": "Step 1.5: broken-local-engine remediation (retry / switch-to-PGLite / re-init AUQ, rollback-safe)", + "trigger": "running the Step 1.5 broken-engine remediation — Step 1's detect returned `gbrain_local_status` of `broken-db` or `broken-config` and no shortcut flag was passed" + }, + { + "id": "brain-init", + "file": "brain-init.md", + "title": "Step 4: initialize the brain — per-path init procedures (Paths 1, 2a, 2b, 3, 4, Switch)", + "trigger": "initializing the brain in Step 4 — run ONLY the procedure for the path picked in Step 2 (Paths 1/2a/2b/3/4 or Switch; also holds the PAT scope disclosure that `--cleanup-orphans` re-uses)" + }, + { + "id": "transcript-gate", + "file": "transcript-gate.md", + "title": "Step 7.5: transcript & memory ingest gate (probe, thresholds, ingest AskUserQuestion)", + "trigger": "running the Step 7.5 transcript & memory ingest gate on Paths 1, 2a, 2b, or 3 (Path 4 skips this section entirely — see the skeleton's skip note)" + }, + { + "id": "claude-md-persist", + "file": "claude-md-persist.md", + "title": "Step 8: persist the GBrain Configuration + Search Guidance blocks in CLAUDE.md", + "trigger": "persisting the Step 8 `## GBrain Configuration` block to CLAUDE.md (and the Search Guidance block after Step 9 passes)" + } + ] +} diff --git a/setup-gbrain/sections/transcript-gate.md b/setup-gbrain/sections/transcript-gate.md new file mode 100644 index 000000000..ca92f79f3 --- /dev/null +++ b/setup-gbrain/sections/transcript-gate.md @@ -0,0 +1,66 @@ + + +After memory sync is wired (Step 7) but before persisting the CLAUDE.md +config (Step 8), offer to bring this Mac's coding-agent transcripts + +curated `~/.gstack/` artifacts into gbrain so the retrieval surface +(per-skill manifests, salience block) has data to surface. + +Run the probe to size the operation: +```bash +bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe +``` + +Read the output. If `Total files in window: 0`, skip — there's nothing +to ingest. Set `gstack-config set transcript_ingest_mode incremental` +silently and continue to Step 8. + +If `New (never ingested)` is < 200 AND total bytes are < 100MB: silent +bulk via `bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --bulk --quiet`. Set +`transcript_ingest_mode=incremental` and continue. + +Otherwise (the "many transcripts on disk" path): AskUserQuestion with +the exact counts AND the value promise. Default scope is **current repo +only, last 90 days**: + +> "Found transcripts in THIS repo () over the last +> 90 days, plus across other repos on this machine ( +> total if all ingested). Ingest THIS repo's transcripts into gbrain? +> +> What you get after this: every gstack skill auto-loads recent salience +> from your past sessions in this repo, so the agent finds your prior +> work without you describing it. You can query 'what was I doing on +> day X' and get a real answer. Per-session pages are searchable, +> taggable, and deletable. Secret scanning runs before any push. +> +> What stays the same: nothing leaves your machine unless gbrain sync +> is enabled (Step 7). Per-repo trust policies still apply. +> +> Multi-Mac note: if you HAVE enabled brain sync (Step 7), these +> transcript pages will sync across your Macs. Caveat: deleting a +> transcript page later removes it from gbrain but git history retains +> it in prior commits. Use `gstack-transcript-prune` to delete in bulk; +> use `git filter-repo` on the brain remote for hard-delete from +> history." + +Options: +- A) Yes — this repo, last 90 days (recommended; ~est min) +- B) Yes — this repo, ALL history +- C) Yes — this repo + other repos on this machine +- D) Skip historical, track new from now (`transcript_ingest_mode=incremental`) +- E) Never ingest transcripts (`transcript_ingest_mode=off`) + +After answer: +```bash +~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode +bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --full --no-brain-sync +``` +(`--no-brain-sync` because Step 7 already wired that path; this just +runs the code import + memory ingest stages. Brain-sync will run on the +next preamble hook.) + +If A/D/E, ingest is incremental from this point on; preamble-boundary +hook runs `bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --incremental --quiet` on every skill +start (cheap mtime fast-path). + +Reference doc for users: `setup-gbrain/memory.md` (linked from CLAUDE.md +Step 8). diff --git a/setup-gbrain/sections/transcript-gate.md.tmpl b/setup-gbrain/sections/transcript-gate.md.tmpl new file mode 100644 index 000000000..573d3c5f8 --- /dev/null +++ b/setup-gbrain/sections/transcript-gate.md.tmpl @@ -0,0 +1,64 @@ +After memory sync is wired (Step 7) but before persisting the CLAUDE.md +config (Step 8), offer to bring this Mac's coding-agent transcripts + +curated `~/.gstack/` artifacts into gbrain so the retrieval surface +(per-skill manifests, salience block) has data to surface. + +Run the probe to size the operation: +```bash +bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe +``` + +Read the output. If `Total files in window: 0`, skip — there's nothing +to ingest. Set `gstack-config set transcript_ingest_mode incremental` +silently and continue to Step 8. + +If `New (never ingested)` is < 200 AND total bytes are < 100MB: silent +bulk via `bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --bulk --quiet`. Set +`transcript_ingest_mode=incremental` and continue. + +Otherwise (the "many transcripts on disk" path): AskUserQuestion with +the exact counts AND the value promise. Default scope is **current repo +only, last 90 days**: + +> "Found transcripts in THIS repo () over the last +> 90 days, plus across other repos on this machine ( +> total if all ingested). Ingest THIS repo's transcripts into gbrain? +> +> What you get after this: every gstack skill auto-loads recent salience +> from your past sessions in this repo, so the agent finds your prior +> work without you describing it. You can query 'what was I doing on +> day X' and get a real answer. Per-session pages are searchable, +> taggable, and deletable. Secret scanning runs before any push. +> +> What stays the same: nothing leaves your machine unless gbrain sync +> is enabled (Step 7). Per-repo trust policies still apply. +> +> Multi-Mac note: if you HAVE enabled brain sync (Step 7), these +> transcript pages will sync across your Macs. Caveat: deleting a +> transcript page later removes it from gbrain but git history retains +> it in prior commits. Use `gstack-transcript-prune` to delete in bulk; +> use `git filter-repo` on the brain remote for hard-delete from +> history." + +Options: +- A) Yes — this repo, last 90 days (recommended; ~est min) +- B) Yes — this repo, ALL history +- C) Yes — this repo + other repos on this machine +- D) Skip historical, track new from now (`transcript_ingest_mode=incremental`) +- E) Never ingest transcripts (`transcript_ingest_mode=off`) + +After answer: +```bash +~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode +bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --full --no-brain-sync +``` +(`--no-brain-sync` because Step 7 already wired that path; this just +runs the code import + memory ingest stages. Brain-sync will run on the +next preamble hook.) + +If A/D/E, ingest is incremental from this point on; preamble-boundary +hook runs `bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --incremental --quiet` on every skill +start (cheap mtime fast-path). + +Reference doc for users: `setup-gbrain/memory.md` (linked from CLAUDE.md +Step 8). diff --git a/test/gbrain-init-voyage-code-3.test.ts b/test/gbrain-init-voyage-code-3.test.ts index a9ca731ba..7365d8e1c 100644 --- a/test/gbrain-init-voyage-code-3.test.ts +++ b/test/gbrain-init-voyage-code-3.test.ts @@ -83,7 +83,7 @@ exit 0 /** * Verbatim reimplementation of the skill template's voyage-code-3 - * conditional. The template (setup-gbrain/SKILL.md.tmpl Path 3, Step 1.5 + * conditional. The template (setup-gbrain/sections/brain-init.md.tmpl Path 3, Step 1.5 * inside the rollback wrapper, Step 4.5 Path 4 Yes branch) instructs the * model to execute this bash; we execute the same bash here and assert the * argv passed to gbrain matches the contract. @@ -202,9 +202,18 @@ gbrain init --pglite --json $GBRAIN_EMBED_FLAGS }); it("template uses the positional-params shape, not an unquoted flags var", () => { + // Carved (token-reduction Phase 4): count across the tmpl UNION — one + // PGLite init site stays in the skeleton, the Path-3/4 sites live in the + // brain-init section. const tmpl = readFileSync( join(import.meta.dir, "..", "setup-gbrain", "SKILL.md.tmpl"), "utf-8", + ) + readFileSync( + join(import.meta.dir, "..", "setup-gbrain", "sections", "brain-init.md.tmpl"), + "utf-8", + ) + readFileSync( + join(import.meta.dir, "..", "setup-gbrain", "sections", "engine-remediation.md.tmpl"), + "utf-8", ); expect(tmpl).not.toContain("$GBRAIN_EMBED_FLAGS"); const sites = tmpl.match(/gbrain init --pglite --json "\$@"/g) || []; @@ -229,8 +238,10 @@ describe("template alignment: the .tmpl actually contains the voyage gate", () = // Belt-and-suspenders: if someone edits the template and drops the // VOYAGE_API_KEY conditional without updating the test above, this catches // it. The shell snippet under test must literally appear in the .tmpl. - const TEMPLATE_PATH = join(import.meta.dir, "..", "setup-gbrain", "SKILL.md.tmpl"); - const tmpl = readFileSync(TEMPLATE_PATH, "utf-8"); + // Carved union — see comment above. + const tmpl = readFileSync(join(import.meta.dir, "..", "setup-gbrain", "SKILL.md.tmpl"), "utf-8") + + readFileSync(join(import.meta.dir, "..", "setup-gbrain", "sections", "brain-init.md.tmpl"), "utf-8") + + readFileSync(join(import.meta.dir, "..", "setup-gbrain", "sections", "engine-remediation.md.tmpl"), "utf-8"); it("setup-gbrain template gates the embedding-model flag on VOYAGE_API_KEY", () => { // Should appear at least once (currently 3 init sites use the same gate). diff --git a/test/helpers/setup-gbrain-fixture.ts b/test/helpers/setup-gbrain-fixture.ts new file mode 100644 index 000000000..e4a8e65c2 --- /dev/null +++ b/test/helpers/setup-gbrain-fixture.ts @@ -0,0 +1,107 @@ +/** + * setup-gbrain E2E fixture builder — carve-aware (token-reduction Phase 4). + * + * setup-gbrain is carved: the generated SKILL.md is a decision-tree skeleton + * whose STOP-Read pointers reference install paths + * (`~/.claude/skills/gstack/setup-gbrain/sections/*.md`) that do not exist in + * a hermetic E2E sandbox. Pointing an agent at the raw skeleton would burn + * turns on failed Reads and never reach the per-path init procedures under + * test. This builder reconstructs a runnable single-file fixture, wave-1 + * style (see the codex fixture in test/skill-e2e-workflow.test.ts): + * + * 1. slice the skeleton from the skill title (dropping the shared preamble — + * CLAUDE.md rule: "E2E test fixtures: extract, don't copy"), + * 2. cut the Section index table (its sections/ paths don't resolve here), + * 3. replace each STOP pointer with the section body the test needs, or an + * explicit "not needed" stub for the rest, and + * 4. run a non-empty guard: every needed section's distinctive anchor must + * be present in the result, so a renamed/emptied section fails loudly + * instead of shipping a silently hollow fixture. + * + * Monolith-tolerant: if the generated SKILL.md has no STOP pointers (pre-carve + * checkout, or a regen that un-carves), the bodies are still inline and the + * anchor guard passes — the builder works on both shapes. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +const ROOT = path.resolve(import.meta.dir, '..', '..'); +const SKILL_MD = path.join(ROOT, 'setup-gbrain', 'SKILL.md'); +const SECTIONS_DIR = path.join(ROOT, 'setup-gbrain', 'sections'); + +const TITLE = '# /setup-gbrain — Coding-Agent Onboarding for gbrain'; + +/** Matches one generated STOP-Read pointer (two lines) and captures the section file name. */ +const STOP_POINTER = + /^> \*\*STOP\.\*\* Before [^\n]*sections\/([a-z0-9-]+\.md)[^\n]*\n> in full\.[^\n]*/gm; + +/** Distinctive per-section anchors — the non-empty guard for inlined content. */ +export const SECTION_ANCHORS: Record = { + 'brain-init.md': '### Path 4 (Remote gbrain MCP', + 'claude-md-persist.md': 'Mode: remote-http', + 'engine-remediation.md': "Your local gbrain engine isn't responding", + 'transcript-gate.md': 'gstack-memory-ingest.ts --probe', +}; + +/** + * Build the fixture text: skeleton (preamble dropped, Section index cut) with + * `neededSections` inlined at their STOP pointers and every other pointer + * replaced by an explicit not-needed stub. Throws on any missing anchor. + */ +export function buildSetupGbrainFixture(neededSections: string[]): string { + for (const file of neededSections) { + if (!(file in SECTION_ANCHORS)) { + throw new Error( + `setup-gbrain fixture: unknown section "${file}" — known: ${Object.keys(SECTION_ANCHORS).join(', ')}`, + ); + } + } + + let full = fs.readFileSync(SKILL_MD, 'utf-8'); + + const titleIdx = full.indexOf(TITLE); + if (titleIdx < 0) throw new Error(`setup-gbrain fixture: title heading not found: "${TITLE}"`); + full = full.slice(titleIdx); + + // Cut the Section index table (heading through its closing --- separator). + const idxStart = full.indexOf('## Section index'); + if (idxStart >= 0) { + const idxEnd = full.indexOf('\n---\n', idxStart); + if (idxEnd < 0) throw new Error('setup-gbrain fixture: Section index has no closing ---'); + full = full.slice(0, idxStart) + full.slice(idxEnd + '\n---\n'.length); + } + + full = full.replace(STOP_POINTER, (_m, file: string) => { + if (!neededSections.includes(file)) { + return '_(Section not included in this fixture — not needed for this run. Continue with the next step.)_'; + } + const secPath = path.join(SECTIONS_DIR, file); + if (!fs.existsSync(secPath)) { + throw new Error( + `setup-gbrain fixture: sections/${file} not generated — run bun run gen:skill-docs`, + ); + } + const body = fs + .readFileSync(secPath, 'utf-8') + .replace(/^\n/gm, '') // strip AUTO-GENERATED header comments + .trim(); + if (body.length < 500) { + throw new Error(`setup-gbrain fixture: sections/${file} is unexpectedly small/empty`); + } + return body; + }); + + // Non-empty guard on the RESULT — holds for both the carved shape (section + // inlined above) and the monolith shape (body was never carved out). + for (const file of neededSections) { + if (!full.includes(SECTION_ANCHORS[file])) { + throw new Error( + `setup-gbrain fixture: needed section "${file}" content missing from fixture ` + + `(anchor not found: "${SECTION_ANCHORS[file]}")`, + ); + } + } + + return full; +} diff --git a/test/setup-gbrain-bin-invocation-paths.test.ts b/test/setup-gbrain-bin-invocation-paths.test.ts index 259f2542c..4788b3f65 100644 --- a/test/setup-gbrain-bin-invocation-paths.test.ts +++ b/test/setup-gbrain-bin-invocation-paths.test.ts @@ -25,9 +25,28 @@ import * as path from 'path'; const ROOT = path.resolve(import.meta.dir, '..'); const TMPL = path.join(ROOT, 'setup-gbrain', 'SKILL.md.tmpl'); +const SECTIONS_DIR = path.join(ROOT, 'setup-gbrain', 'sections'); const MEMORY_DOC = path.join(ROOT, 'setup-gbrain', 'memory.md'); +// Carve-aware (token-reduction Phase 4): setup-gbrain is carved. The Step 7.5 +// ingest-gate body (which owns the R1-R4 invocations) lives in +// sections/transcript-gate.md.tmpl; the skeleton keeps dispatch + the Step 10 +// verdict prose. Negative (no-bare-invocation) checks run over the UNION so a +// stale form can't hide in any template file. const tmpl = fs.readFileSync(TMPL, 'utf-8'); +const transcriptGate = fs.readFileSync( + path.join(SECTIONS_DIR, 'transcript-gate.md.tmpl'), + 'utf-8', +); +const tmplUnion = [tmpl] + .concat( + fs + .readdirSync(SECTIONS_DIR) + .filter((f) => f.endsWith('.md.tmpl')) + .sort() + .map((f) => fs.readFileSync(path.join(SECTIONS_DIR, f), 'utf-8')), + ) + .join('\n'); const memoryDoc = fs.readFileSync(MEMORY_DOC, 'utf-8'); // A "bare invocation" is the tool name immediately followed by a flag/arg @@ -42,46 +61,46 @@ const memoryDoc = fs.readFileSync(MEMORY_DOC, 'utf-8'); const bareMemoryIngest = /\bgstack-memory-ingest\b(?!\.ts)(?:\s|\\\r?\n)+--/; const bareGbrainSync = /\bgstack-gbrain-sync\b(?!\.ts)(?:\s|\\\r?\n)+--/; -describe('setup-gbrain/SKILL.md.tmpl — bin invocation paths', () => { - test('no bare gstack-memory-ingest invocation remains', () => { - expect(tmpl).not.toMatch(bareMemoryIngest); +describe('setup-gbrain templates (skeleton + sections) — bin invocation paths', () => { + test('no bare gstack-memory-ingest invocation remains anywhere in the union', () => { + expect(tmplUnion).not.toMatch(bareMemoryIngest); }); - test('no bare gstack-gbrain-sync invocation remains', () => { - expect(tmpl).not.toMatch(bareGbrainSync); + test('no bare gstack-gbrain-sync invocation remains anywhere in the union', () => { + expect(tmplUnion).not.toMatch(bareGbrainSync); }); - test('the probe step uses bun run + .ts (R1)', () => { - expect(tmpl).toContain( + test('the probe step uses bun run + .ts (R1, transcript-gate section)', () => { + expect(transcriptGate).toContain( 'bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe' ); }); - test('the silent-bulk mention uses bun run + .ts (R2)', () => { - expect(tmpl).toContain( + test('the silent-bulk mention uses bun run + .ts (R2, transcript-gate section)', () => { + expect(transcriptGate).toContain( 'bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --bulk --quiet' ); }); - test('the post-answer full-sync step uses bun run + .ts (R3)', () => { - expect(tmpl).toContain( + test('the post-answer full-sync step uses bun run + .ts (R3, transcript-gate section)', () => { + expect(transcriptGate).toContain( 'bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --full --no-brain-sync' ); }); - test('the preamble-hook incremental-sync mention uses bun run + .ts (R4)', () => { - expect(tmpl).toContain( + test('the preamble-hook incremental-sync mention uses bun run + .ts (R4, transcript-gate section)', () => { + expect(transcriptGate).toContain( 'bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --incremental --quiet' ); }); test('the neighboring gstack-config line in the post-answer block is untouched (bash script, no extension)', () => { - expect(tmpl).toContain( + expect(transcriptGate).toContain( '~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode ' ); }); - test('the prose-only mention naming the tool as a sentence subject is left unchanged (KTD4 — not a literal invocation)', () => { + test('the prose-only mention naming the tool as a sentence subject is left unchanged (KTD4 — not a literal invocation; Step 10 verdict, skeleton)', () => { expect(tmpl).toContain('gstack-memory-ingest now persists staged transcripts to'); }); }); diff --git a/test/setup-gbrain-path4-structure.test.ts b/test/setup-gbrain-path4-structure.test.ts index 1363e0696..074c56adc 100644 --- a/test/setup-gbrain-path4-structure.test.ts +++ b/test/setup-gbrain-path4-structure.test.ts @@ -1,8 +1,17 @@ // setup-gbrain Path 4 structural lint. // -// Verifies the SKILL.md.tmpl has the prose contract that Path 4 (Remote MCP) -// depends on: STOP gates after verify failures, never-write-token rules, -// mode-aware CLAUDE.md block, idempotent re-run path. +// Verifies the skill's templates carry the prose contract that Path 4 +// (Remote MCP) depends on: STOP gates after verify failures, never-write-token +// rules, mode-aware CLAUDE.md block, idempotent re-run path. +// +// Carve-aware (token-reduction Phase 4): setup-gbrain is carved — the +// SKILL.md.tmpl is a decision-tree skeleton (detect, path dispatch, verify, +// MCP registration, verdict) and the branch-exclusive install bodies live in +// setup-gbrain/sections/*.md.tmpl. Each pin below targets the file that OWNS +// the content: dispatch/verdict pins hit the skeleton, per-path init pins hit +// sections/brain-init.md.tmpl, the CLAUDE.md block pins hit +// sections/claude-md-persist.md.tmpl, and the token-security regressions run +// over the union so a marker can't silently vanish during a re-carve. // // Why a structural test instead of a full Agent SDK E2E: // - Side effects (claude.json mutation, MCP registration) are covered @@ -21,110 +30,157 @@ import * as fs from 'fs'; import * as path from 'path'; const ROOT = path.resolve(import.meta.dir, '..'); -const TMPL = path.join(ROOT, 'setup-gbrain', 'SKILL.md.tmpl'); +const SKILL_DIR = path.join(ROOT, 'setup-gbrain'); +const SECTIONS_DIR = path.join(SKILL_DIR, 'sections'); -const tmpl = fs.readFileSync(TMPL, 'utf-8'); +// Skeleton template — always loaded; owns detect, path dispatch, Steps 5/5a/6/7/9/10. +const skeleton = fs.readFileSync(path.join(SKILL_DIR, 'SKILL.md.tmpl'), 'utf-8'); +// Per-path init procedures (Paths 1/2a/2b/3/4 + Switch) — Step 4 body. +const brainInit = fs.readFileSync(path.join(SECTIONS_DIR, 'brain-init.md.tmpl'), 'utf-8'); +// Step 8 CLAUDE.md persist body (both mode blocks + the gated guidance write). +const claudeMdPersist = fs.readFileSync( + path.join(SECTIONS_DIR, 'claude-md-persist.md.tmpl'), + 'utf-8', +); +// Skeleton + every section template — total behavior, order-stable. +const union = [skeleton] + .concat( + fs + .readdirSync(SECTIONS_DIR) + .filter((f) => f.endsWith('.md.tmpl')) + .sort() + .map((f) => fs.readFileSync(path.join(SECTIONS_DIR, f), 'utf-8')), + ) + .join('\n'); -describe('setup-gbrain Path 4 (Remote MCP) — structural contract', () => { - test('Step 2 lists Path 4 as one of the path options', () => { - // "4 — Remote gbrain MCP" with em-dash (—, U+2014 — one codepoint). - expect(tmpl).toMatch(/\*\*4 . Remote gbrain MCP/); +describe('setup-gbrain carve — dispatch stays in the skeleton', () => { + test('the path-dispatch step (Step 2 picker) stays always-loaded', () => { + expect(skeleton).toContain('## Step 2: Pick a path (AskUserQuestion)'); }); - test('Step 4 has a Path 4 sub-section', () => { - expect(tmpl).toMatch(/### Path 4 \(Remote gbrain MCP/); + test('the skeleton routes to all four sections and renders the index', () => { + expect(skeleton).toContain('{{SECTION_INDEX:setup-gbrain}}'); + for (const id of ['engine-remediation', 'brain-init', 'transcript-gate', 'claude-md-persist']) { + expect(skeleton).toContain(`{{SECTION:${id}}}`); + } + }); + + test('the carved bodies moved OUT of the skeleton (no leak-back)', () => { + // Step 4 per-path init: + expect(skeleton).not.toContain('### Path 1 (Supabase, existing URL)'); + expect(skeleton).not.toContain('read_secret_to_env GBRAIN_MCP_TOKEN'); + // Step 1.5 remediation AUQ: + expect(skeleton).not.toContain("Your local gbrain engine isn't responding"); + // Step 7.5 ingest gate body: + expect(skeleton).not.toContain('gstack-memory-ingest.ts --probe'); + // Step 8 block formats: + expect(skeleton).not.toContain('Mode: remote-http'); + }); +}); + +describe('setup-gbrain Path 4 (Remote MCP) — structural contract', () => { + test('Step 2 lists Path 4 as one of the path options (skeleton)', () => { + // "4 — Remote gbrain MCP" with em-dash (—, U+2014 — one codepoint). + expect(skeleton).toMatch(/\*\*4 . Remote gbrain MCP/); + }); + + test('Step 4 has a Path 4 sub-section (brain-init section)', () => { + expect(brainInit).toMatch(/### Path 4 \(Remote gbrain MCP/); }); test('Step 4 collects the bearer via read_secret_to_env, never argv', () => { // The secret-read helper is the canonical token-capture pattern. // Without it, tokens land in shell history. - expect(tmpl).toContain('read_secret_to_env GBRAIN_MCP_TOKEN'); + expect(brainInit).toContain('read_secret_to_env GBRAIN_MCP_TOKEN'); }); test('Step 4c invokes gstack-gbrain-mcp-verify and STOPs on failure', () => { - expect(tmpl).toContain('gstack-gbrain-mcp-verify'); + expect(brainInit).toContain('gstack-gbrain-mcp-verify'); // The STOP rule is what prevents partial registration after auth fail. - const path4Section = tmpl.split('### Path 4')[1] || ''; + const path4Section = brainInit.split('### Path 4')[1] || ''; expect(path4Section).toMatch(/STOP/); }); test('Step 4d explicitly skips Steps 3, 4 (other paths), 5, 7.5 in remote mode', () => { - expect(tmpl).toMatch(/4d.*[Ss]kip Steps? 3, 4.*5.*7\.5/s); + expect(brainInit).toMatch(/4d.*[Ss]kip Steps? 3, 4.*5.*7\.5/s); }); - test('Step 5a has a Path 4 branch with claude mcp add --transport http', () => { - expect(tmpl).toMatch(/Path 4 \(Remote MCP/); - expect(tmpl).toMatch(/claude mcp add --scope user --transport http gbrain/); - expect(tmpl).toContain('Authorization: Bearer $GBRAIN_MCP_TOKEN'); + test('Step 5a has a Path 4 branch with claude mcp add --transport http (skeleton)', () => { + expect(skeleton).toMatch(/Path 4 \(Remote MCP/); + expect(skeleton).toMatch(/claude mcp add --scope user --transport http gbrain/); + expect(skeleton).toContain('Authorization: Bearer $GBRAIN_MCP_TOKEN'); // Token must be unset after registration so it doesn't linger in env. - expect(tmpl).toMatch(/unset GBRAIN_MCP_TOKEN/); + expect(skeleton).toMatch(/unset GBRAIN_MCP_TOKEN/); }); test('Step 5a removes any prior gbrain registration before adding the new one', () => { // Otherwise local-stdio + remote-http coexist, which breaks routing. - expect(tmpl).toMatch(/claude mcp remove gbrain/); + expect(skeleton).toMatch(/claude mcp remove gbrain/); }); - test('Step 7 calls gstack-artifacts-init with --url-form-supported flag', () => { - expect(tmpl).toMatch(/gstack-artifacts-init.*--url-form-supported/); + test('Step 7 calls gstack-artifacts-init with --url-form-supported flag (skeleton)', () => { + expect(skeleton).toMatch(/gstack-artifacts-init.*--url-form-supported/); }); - test('Step 8 CLAUDE.md block branches on mode', () => { + test('Step 8 CLAUDE.md block branches on mode (claude-md-persist section)', () => { // The remote-http block has Mode: remote-http; local-stdio block has Engine:. - expect(tmpl).toMatch(/### Path 4 \(Remote MCP\)/); - expect(tmpl).toMatch(/Mode: remote-http/); - expect(tmpl).toMatch(/Mode: local-stdio/); + expect(claudeMdPersist).toMatch(/### Path 4 \(Remote MCP\)/); + expect(claudeMdPersist).toMatch(/Mode: remote-http/); + expect(claudeMdPersist).toMatch(/Mode: local-stdio/); }); test('Step 8 explicitly says the bearer is never written to CLAUDE.md', () => { // Token-leak regression guard. CLAUDE.md is committed in many projects. - expect(tmpl).toMatch(/bearer token is \*\*never\*\* written to CLAUDE\.md/); + expect(claudeMdPersist).toMatch(/bearer token is \*\*never\*\* written to CLAUDE\.md/); }); test('Step 9 smoke test on Path 4 prints a placeholder, never the real token', () => { // Don't paste the token into the curl example the user might share. - expect(tmpl).toMatch(//); + expect(skeleton).toMatch(//); }); test('Step 10 verdict block has a remote-http variant separate from local-stdio', () => { - expect(tmpl).toMatch(/### Path 4 \(Remote MCP\)/); - expect(tmpl).toMatch(/mode: remote-http/); - expect(tmpl).toMatch(/N\/A.*remote mode/); + expect(skeleton).toMatch(/### Path 4 \(Remote MCP\)/); + expect(skeleton).toMatch(/mode: remote-http/); + expect(skeleton).toMatch(/N\/A.*remote mode/); }); test('idempotency: re-running with gbrain_mcp_mode=remote-http skips Step 2', () => { // Re-run path stays graceful; no double-registration. - expect(tmpl).toMatch(/gbrain_mcp_mode=remote-http/); + expect(skeleton).toMatch(/gbrain_mcp_mode=remote-http/); }); - test('Step 5 (local doctor) explicitly skips on Path 4', () => { - expect(tmpl).toMatch(/SKIP entirely on Path 4 \(Remote MCP\)/); + test('Step 5 (local doctor) explicitly skips on Path 4 (skeleton)', () => { + expect(skeleton).toMatch(/SKIP entirely on Path 4 \(Remote MCP\)/); }); - test('Step 7.5 (transcript ingest) explicitly skips on Path 4', () => { + test('Step 7.5 (transcript ingest) explicitly skips on Path 4 (skeleton)', () => { // Transcript ingest needs local gbrain CLI which Path 4 doesn't install. - const matches = tmpl.match(/SKIP entirely on Path 4 \(Remote MCP\)/g); + // The skip notes are DISPATCH — they must stay in the always-loaded + // skeleton (Steps 3, 5, and 7.5 each carry one). + const matches = skeleton.match(/SKIP entirely on Path 4 \(Remote MCP\)/g); expect(matches?.length).toBeGreaterThanOrEqual(2); }); }); describe('setup-gbrain Path 4 — token security regressions', () => { - test('the template never inlines a real-shaped bearer string', () => { + test('no template (skeleton or section) inlines a real-shaped bearer string', () => { // We never want a literal "gbrain_" token to appear in the - // template — placeholders only. This catches the failure mode where - // someone copies a real token into the template by accident. + // templates — placeholders only. This catches the failure mode where + // someone copies a real token into a template by accident. const realTokenShape = /gbrain_[a-f0-9]{40,}/; - expect(tmpl).not.toMatch(realTokenShape); + expect(union).not.toMatch(realTokenShape); }); test('Path 4 always uses env-var $GBRAIN_MCP_TOKEN, never inline strings', () => { - // Find every reference to the bearer header in Path 4 and verify it's - // either an env-var expansion or an explicit placeholder. Allow: + // Find every reference to the bearer header in Path 4 (across the + // skeleton AND sections) and verify it's either an env-var expansion + // or an explicit placeholder. Allow: // - $GBRAIN_MCP_TOKEN (env-var expansion) // - , , (placeholder) // - "..." (rest-of-doc-text continuation; a doc note showing how // `claude mcp add --header` shapes its argv). - const path4Section = tmpl.match(/### Path 4 \(Remote MCP[\s\S]*?(?=###|## )/g)?.join('') || ''; + const path4Section = union.match(/### Path 4 \(Remote MCP[\s\S]*?(?=###|## )/g)?.join('') || ''; const bearerLines = path4Section.match(/Bearer\s+\S+/g) || []; for (const line of bearerLines) { expect(line).toMatch(/Bearer (\$GBRAIN_MCP_TOKEN||||\.\.\."?)/); diff --git a/test/skill-e2e-setup-gbrain-bad-token.test.ts b/test/skill-e2e-setup-gbrain-bad-token.test.ts index f4ad75080..14e63fcb2 100644 --- a/test/skill-e2e-setup-gbrain-bad-token.test.ts +++ b/test/skill-e2e-setup-gbrain-bad-token.test.ts @@ -7,6 +7,11 @@ // regression guard for the "verify failed → STOP" gate. // // Cost: ~$0.30-$0.50 per run. Gate-tier (EVALS=1 EVALS_TIER=gate). +// +// Carve-aware: the Step 4 Path 4 body (collect URL/token, verify, STOP rule) +// lives in setup-gbrain/sections/brain-init.md, so the fixture inlines that +// section into the skeleton via buildSetupGbrainFixture. Step 8 is not needed: +// on a failed verify the skill STOPs before any CLAUDE.md write. import { test, expect } from 'bun:test'; import { describeE2ETier } from './helpers/e2e-gate'; @@ -15,6 +20,7 @@ import * as os from 'os'; import * as path from 'path'; import * as http from 'http'; import { runAgentSdkTest, passThroughNonAskUserQuestion, resolveClaudeBinary } from './helpers/agent-sdk-runner'; +import { buildSetupGbrainFixture } from './helpers/setup-gbrain-fixture'; // Periodic-tier (companion to skill-e2e-setup-gbrain-remote.test.ts). // Deterministic gate coverage lives in setup-gbrain-path4-structure.test.ts. @@ -86,7 +92,10 @@ describeE2E('/setup-gbrain Path 4 — bad token STOPs cleanly', () => { let modelTextOutput = ''; try { - const skillPath = path.resolve(import.meta.dir, '..', 'setup-gbrain', 'SKILL.md'); + // Carve-aware fixture: skeleton + brain-init inlined (non-empty guard + // inside the builder). The test drives Steps 4a-4c to the STOP. + const skillPath = path.join(gstackHome, 'setup-gbrain-SKILL.md'); + fs.writeFileSync(skillPath, buildSetupGbrainFixture(['brain-init.md'])); const result = await runAgentSdkTest({ systemPrompt: { type: 'preset', preset: 'claude_code' }, userPrompt: diff --git a/test/skill-e2e-setup-gbrain-path4-local-pglite.test.ts b/test/skill-e2e-setup-gbrain-path4-local-pglite.test.ts index 59275355b..0c4e72b81 100644 --- a/test/skill-e2e-setup-gbrain-path4-local-pglite.test.ts +++ b/test/skill-e2e-setup-gbrain-path4-local-pglite.test.ts @@ -29,6 +29,7 @@ import { passThroughNonAskUserQuestion, resolveClaudeBinary, } from './helpers/agent-sdk-runner'; +import { buildSetupGbrainFixture } from './helpers/setup-gbrain-fixture'; const describeE2E = describeE2ETier('periodic'); @@ -166,11 +167,14 @@ describeE2E('/setup-gbrain Path 4 + Step 4.5 Yes → local PGLite for code', () process.env.GBRAIN_MCP_TOKEN = 'gbrain_fake_token_for_test'; try { - const skillPath = path.resolve( - import.meta.dir, - '..', - 'setup-gbrain', - 'SKILL.md', + // Carve-aware fixture (see test/helpers/setup-gbrain-fixture.ts): + // skeleton + brain-init (Step 4 Path 4 body incl. the Step 4d local + // PGLite offer this test says Yes to) + claude-md-persist (Step 8 sits + // on the walked path to Step 10). Non-empty guard inside the builder. + const skillPath = path.join(sandboxHome, 'setup-gbrain-SKILL.md'); + fs.writeFileSync( + skillPath, + buildSetupGbrainFixture(['brain-init.md', 'claude-md-persist.md']), ); const result = await runAgentSdkTest({ systemPrompt: { type: 'preset', preset: 'claude_code' }, diff --git a/test/skill-e2e-setup-gbrain-remote.test.ts b/test/skill-e2e-setup-gbrain-remote.test.ts index c310cd641..1429c151e 100644 --- a/test/skill-e2e-setup-gbrain-remote.test.ts +++ b/test/skill-e2e-setup-gbrain-remote.test.ts @@ -10,6 +10,10 @@ // Cost: ~$0.30-$0.50 per run. Gate-tier (EVALS=1 EVALS_TIER=gate). // // See setup-gbrain/SKILL.md.tmpl Step 4 (Path 4) for the contract under test. +// The Step 4 body lives in setup-gbrain/sections/brain-init.md (carved), so +// the fixture is built via buildSetupGbrainFixture: skeleton + the brain-init +// and claude-md-persist sections inlined (Step 8 writes the Mode: remote-http +// block this test asserts on). import { test, expect } from 'bun:test'; import { describeE2ETier } from './helpers/e2e-gate'; @@ -18,6 +22,7 @@ import * as os from 'os'; import * as path from 'path'; import * as http from 'http'; import { runAgentSdkTest, passThroughNonAskUserQuestion, resolveClaudeBinary } from './helpers/agent-sdk-runner'; +import { buildSetupGbrainFixture } from './helpers/setup-gbrain-fixture'; // Periodic-tier: the model's interpretation of "follow Path 4 only" is // non-deterministic (it sometimes skips Step 8 CLAUDE.md write, sometimes @@ -144,7 +149,11 @@ describeE2E('/setup-gbrain Path 4 (Remote MCP) — happy path', () => { let modelTextOutput = ''; try { - const skillPath = path.resolve(import.meta.dir, '..', 'setup-gbrain', 'SKILL.md'); + // Carve-aware fixture: skeleton + brain-init (Step 4 Path 4 body) + + // claude-md-persist (Step 8 block formats), STOP pointers resolved + // inline so no Read escapes the sandbox. Non-empty guard inside. + const skillPath = path.join(gstackHome, 'setup-gbrain-SKILL.md'); + fs.writeFileSync(skillPath, buildSetupGbrainFixture(['brain-init.md', 'claude-md-persist.md'])); const result = await runAgentSdkTest({ systemPrompt: { type: 'preset', preset: 'claude_code' }, env: childEnv,