Files
gstack/sync-gbrain/SKILL.md.tmpl
T
Garry Tan 9fd03fae9e v1.58.4.0 fix: high-priority community bug wave + PTY plan-mode smoke gate (#2077)
* fix(gbrain): stop forcing GBRAIN_PREPARE on transaction-mode poolers (#1965)

buildGbrainEnv auto-set GBRAIN_PREPARE=true whenever DATABASE_URL targeted
port 6543, and the /sync-gbrain capability check exported it for the rest
of the skill run. Both had the semantics inverted: gbrain auto-disables
prepared statements on transaction-mode poolers because they break every
write there ("prepared statement does not exist"); GBRAIN_PREPARE=true is
gbrain's documented override for SESSION-mode poolers on 6543, not a
requirement for transaction mode. The #1435 search symptom the auto-set
worked around was fixed gbrain-side.

Remove both force-sets. A caller-set GBRAIN_PREPARE (either value) still
passes through untouched, preserving the session-mode-on-6543 escape hatch.
isTransactionModePooler stays exported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gbrain): classify probe timeout as its own status; sync proceeds instead of skipping (#1964)

The 5s engine probe misclassified healthy-but-slow engines (cold Supabase
pooler connections measured at 6.9-10.7s) as broken-config, so /sync-gbrain
silently skipped code+memory and told the user their config was malformed.

- New "timeout" status: probe killed at the deadline with no recognized
  stderr pattern. Default deadline is now 15s, overridable via
  GSTACK_GBRAIN_PROBE_TIMEOUT_MS (tests set 300ms against a fake that
  sleeps 2s).
- Sync stages PROCEED on timeout with a stderr warning naming the env knob;
  a genuinely-dead engine surfaces its real error at the first operation
  instead of a false config diagnosis.
- Consistency everywhere "ok" gated behavior: gstack-gbrain-detect --is-ok
  exits 0 on timeout, and gen-skill-docs' detection gate accepts it, so a
  slow engine no longer silently suppresses brain-aware features.
- Status cache: key now includes the effective probe timeout (raising it
  invalidates a cached timeout) and GBRAIN_HOME; config detection honors
  GBRAIN_HOME so relocated-home users stop being misclassified as
  missing-config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bins): cygpath-normalize SCRIPT_DIR for bun imports; surface learnings-log errors (#1950)

Under Windows git-bash, pwd yields a POSIX path (/c/Users/...) that Bun on
Windows cannot resolve as an ES module specifier. gstack-learnings-log
interpolates SCRIPT_DIR into a bun -e import, so every invocation died with
"Cannot find module" — and 2>/dev/null swallowed the error, silently
dropping every AI-logged learning for Windows users.

- 3-line cygpath -m guard in gstack-learnings-log and gstack-question-log
  (which gains the same import shape in the next commit). Matches the
  duplicated IS_WINDOWS convention in setup; no shared shell lib exists.
- learnings-log adopts question-log's set +e / TMPERR capture pattern
  wholesale: validation errors now print to stderr. The old
  `if [ $? -ne 0 ]` check was dead code under set -euo pipefail — the
  script exited at the failing assignment before reaching it.
- New test/bin-windows-bun-import-paths.test.ts: static invariant (any
  bash bin interpolating $SCRIPT_DIR into a bun -e import must carry the
  guard) + behavioral end-to-end run invoked via `bash <bin>` — added to
  the windows-free-tests workflow list so the conversion is proven on the
  only platform where the bug exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(question-log): dedupe INJECTION_PATTERNS via lib/jsonl-store (#1934)

bin/gstack-question-log carried a local copy of the injection-pattern list,
so pattern fixes to lib/jsonl-store.ts never propagated — including the
/override[:\s]/i false-positive fix arriving via community PR #1940.
Import the shared hasInjection instead (enabled by the previous commit's
cygpath guard). question-log also gets the lib's stricter superset
(human:, disregard, from-now-on, approve-all patterns).

Tests pin the contract in a #1940-order-independent way: an "Override:
ignore all previous instructions" header is rejected, "prose overrides the
deterministic table" is accepted, and a static invariant keeps local
INJECTION_PATTERNS duplicates out of the bin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): community-pulse + both dashboards never report fake zeros (#1947)

The security-signaling surface failed open at three layers — every failure
mode read as a reassuring "0 attacks" / "0 installs":

- community-pulse edge function: supabase-js returns {data,error} without
  throwing, and all five queries discarded `error` — a DB outage produced
  real-looking zeros via the SUCCESS path, and the catch (also returning
  zeros with HTTP 200) was unreachable for query failures. Every query now
  destructures and throws; the catch serves the stale cache (marked
  "stale": true) when one exists, else 503 {"error":"pulse_unavailable"}.
  Success responses carry "status":"ok" so clients can distinguish
  authoritative data from legacy backends. NOTE: the edge function deploys
  out-of-band (supabase functions deploy community-pulse).
- gstack-security-dashboard: captures the HTTP status; non-200 / network
  failure / error body / missing section → "unknown — backend error";
  jq missing → "unknown — install jq" (the lossy grep fallback broke on
  nested arrays and under-reported attacks as zero — removed); a 200
  without the new marker shows figures with an "unverified (legacy
  backend)" note. Also fixes a latent display bug: the TOTAL grep matched
  the digit 7 inside "attacks_last_7_days" and misreported every count.
- gstack-community-dashboard: same class — curl || echo "{}" plus
  grep || echo "0" printed "Weekly active installs: 0" on any failure.
  Now "unknown — backend error (HTTP N)".

test/security-dashboard-fallback.test.ts pins the matrix (200+marker,
200-legacy, 503, network failure) x (jq present, jq absent) for both bins:
"unknown" states never render as 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): redact error_message spans before they leave the machine (#1947)

error_message was uploaded with only quote/newline escaping — stack traces
and failed-API errors can embed credentials, private paths, and hostnames,
and the sync path strips only _repo_slug/_branch.

New lib/redact-engine.ts export redactFindingSpans(): replaces EVERY
finding's span with <REDACTED-{id}> regardless of tier (applyRedactions is
the interactive PII-only path and exits nonzero on credential findings, so
it can't serve machine egress). Returns null when a span can't be located —
callers drop the whole payload rather than risk a leak.

gstack-telemetry-log pipes error_message through it at LOG time, so the
local JSONL at rest is clean too; surrounding text survives for crash
triage. FAIL CLOSED: bun missing, engine error, or non-JSON-string output
all null the field. Tests pin: embedded ghp_ token → <REDACTED-github.pat>
with context intact; redactor unavailable → null; raw bytes on disk never
contain the token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact): prepush guard fails closed on git failure; /ship owns hook install (#1946)

Two gaps closed:

1. Fail closed. The git() helper returned "" on ANY non-zero exit or
   maxBuffer overflow (status null), addedLinesFor produced an empty
   string, and the push sailed through unscanned — fail-open on exactly
   the oversized-diff case where a large secret-bearing blob is most
   likely. The diff call now uses a strict variant that throws; main
   blocks with a clear message naming the GSTACK_REDACT_PREPUSH=skip
   escape valve. Probe calls (symbolic-ref, rev-parse, merge-base) keep
   the permissive helper — their failures are normal control flow.

2. Install path. The hook was installed by nothing ("opt-in, installed by
   nothing" was the issue's words). ./setup runs in the gstack checkout —
   the wrong repo for a per-project hook — so it gets a one-line hint
   only. /ship owns per-repo install: config redact_prepush_hook=true +
   hook missing → silent install (consent already given); config unset +
   no ~/.gstack/.redact-prepush-prompted marker → one-time machine-wide
   AskUserQuestion offer, answer persisted. ship/SKILL.md regenerated in
   this same commit (check-freshness bisect discipline).

Tests: unscannable diff (bogus SHAs) → exit 1 + valve named; empty-but-
successful diff → exit 0; static asserts pin setup as hint-only and the
ship template as the installer surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(redact): six new credential patterns — GitLab, HuggingFace, npm, DigitalOcean, Bearer, GCP SA (#1946)

Coverage gaps from the #1946 security review, including token types for
tooling gstack itself drives (glab):

HIGH (block): gitlab.token (glpat-/glptt-/gldt-), huggingface.token (hf_),
npm.token (npm_), digitalocean.token (dop_v1_), gcp.service_account (the
JSON-escaped "private_key" form that dodges pem.private_key's literal-block
match when minified, confirmed by "private_key_id" proximity).

MEDIUM (warn): auth.bearer — the most FP-prone shape in the set (docs are
full of "Authorization: Bearer <token>"), so it requires header-context
proximity and the same entropy>=3.0 + placeholder validator recipe as
env.kv. "Bearer YOUR_TOKEN_HERE" never fires; calibration over coverage,
per the cries-wolf principle.

All shapes are linear-time; test/redact-pattern-lint.test.ts covers them
automatically. Engine tests add positive + placeholder-negative cases per
pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage-audit additions for the fix wave

Ship Step 7 gap-fill (all passing, 248 tests across the touched suites):
memory + dream stage probe-timeout proceeds, gbrain-detect override paths,
stale-flag passthrough, 200-body-missing-.security fail-closed case,
telemetry redaction edges, and credential-pattern edge cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: pre-landing review fixes

Review army findings (1 critical, auto-fixed with regression tests):

- CRITICAL (security specialist, verified live): redactFindingSpans spliced
  only the regex capture span, and pem.private_key / gcp.service_account
  capture just the BEGIN-header — the key body survived "redaction" and
  shipped via telemetry. Marker-only patterns now drop the whole payload
  (null, fail closed). Overlapping spans (Bearer+JWT on the same bytes) are
  coalesced before splicing so stale offsets can't leave partial secret
  bytes behind.
- gitStrict: drop the dead `|| r.status === null` disjunct (null !== 0
  already covers it); add the signal-kill/null-status regression test the
  docstring promised.
- security-dashboard human mode flags stale snapshots ("figures may be out
  of date") instead of presenting frozen counts as current.
- community-dashboard marker check uses jq when available — the grep-only
  variant misclassified whitespaced/reserialized bodies as legacy.
- telemetry fail-closed test now shadows bun with a failing stub
  (deterministic on any host layout); stale "five status cases" describe
  title renamed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: adversarial review fixes (Claude + Codex cross-model passes)

Both adversarial passes ran against the wave; every FIXABLE finding landed
with a regression test:

- probeTimeoutMs clamps to >=1ms: a fractional override floored to 0, and
  execFileSync treats timeout:0 as NO timeout — the probe that exists to
  bound hangs could hang forever (found by both models independently).
- /ship silent hook install now requires the hooks dir to live inside
  .git: with core.hooksPath (husky's COMMITTED .husky/), the chaining
  installer would have renamed the team's committed pre-push and written a
  machine-local wrapper into the working tree (found by both models).
- gstack-config gbrain-refresh accepts the "timeout" status — the last
  consumer still gating on literal "ok" (Codex); gstack-gbrain-detect's
  config-derived fields honor GBRAIN_HOME so the detection JSON can't
  report status ok alongside config_exists false (Codex).
- prepush: a remote sha absent locally (shallow clone / stale fetch) falls
  back to the merge-base/empty-tree range — scans MORE, never blocks a
  legitimate push into training users toward --no-verify.
- dashboards: curl's own 000 no longer doubles to "HTTP 000000"; the
  community dashboard flags stale snapshots like the security one; array
  sections parse via jq (the sed/grep loops truncated at the first ']');
  the no-jq marker grep tolerates whitespace.
- telemetry: multi-line redactor output nulls the field instead of
  corrupting the JSONL record; setup's hint fires only when the config key
  is genuinely unset (an explicit false is a recorded decline); the /ship
  prompt marker honors GSTACK_HOME.

Kept as designed (cross-model tension noted): Bearer stays MEDIUM in the
prepush gate — a HIGH Bearer would block every docs example; the entropy
validator can't eliminate that FP class, and MEDIUM warns visibly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump version and changelog (v1.57.11.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: P1 TODO — eval harness live progress + incremental persistence

Root-caused during this ship: a killed eval run was indistinguishable from a
healthy one for hours (per-file output buffering across mega test files, no
incremental eval-store writes, no honest liveness signal). Full context and
starting points in the entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: fix operational-learning E2E fixture — copy lib/jsonl-store.ts

Pre-existing breakage, proven on main: gstack-learnings-log has imported
lib/jsonl-store.ts (shared injection patterns) since v1.57.5.0 / #1910, but
the fixture copies only the bin scripts — the bin exits 1 before writing
anything, on main silently (stderr swallowed) and on this branch loudly
(the #1950 error-surfacing made the four-day-old failure visible). A real
install always ships bin/ and lib/ together; the fixture now does too.
Verified: the fixture-shaped invocation writes the learning (exit 0) with
lib present, exits 1 on both main and this branch without it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ios-qa): isolate E2E tests under --concurrent (3 real races)

The ios-qa E2E file failed intermittently under `bun test --concurrent`
(the eval harness default). Three distinct shared-state races, all fixed:

1. Shared pidfile: a module-level `workDir` reassigned in beforeEach was
   clobbered by parallel tests, so concurrent daemons collided on the same
   pidfile and the loser returned `already_running`. Each test now gets its
   own dir via makeWorkDir().
2. process.env path globals: tests set GSTACK_IOS_AUDIT_PATH /
   _ATTEMPTS_PATH / _ALLOWLIST_PATH on the shared process env; concurrent
   tests stomped each other's audit/attempts destinations. Threaded
   auditPath/attemptsPath/allowlistPath through DaemonOptions (and
   mintForCaller) as explicit args — env is no longer load-bearing.
3. afterEach cleanup race: the per-test cleanup drained a shared dir array,
   so the first test to finish deleted still-running tests' workDirs
   mid-assertion. Moved to afterAll (cleans once, after all settle).

Verified: 5/5 clean full-suite runs at --max-concurrency 15 (was
intermittent); daemon unit suite 91/91; daemon source compiles. The paths
default to the env-derived locations when options are omitted, so the
production CLI path is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(pty): pin spawned claude to EVALS model chain (default claude-sonnet-4-6)

launchClaudePty spawned the interactive `claude` TUI with no --model flag, so
the child inherited the operator's ~/.claude/settings.json model. On a
slow-thinking model that meant 5+ min of extended thinking on empty plan-mode
context, timing out the plan-mode smoke tests regardless of contention. Pin the
model via opts.model ?? EVALS_MODEL ?? 'claude-sonnet-4-6' — byte-identical to
session-runner.ts:144, so PTY and `claude -p` evals always agree.

Pushed before extraArgs (last flag wins, so a per-test --model still overrides).
Placement leaves the spawn region byte-stable for a clean merge with the
in-flight hermetic-env branch. Plumbed model through the three plan-skill
wrappers. Static-grep tripwires guard the pin, its fallback chain, the
before-extraArgs ordering, and all three wrapper forwards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(pty): detect markdown bold-bullet prose AUQs (fixes office-hours smoke)

office-hours auto-mode renders its mode question as `- **Building a startup**`
markdown bullets (office-hours/SKILL.md.tmpl:102) with no letter/number marker.
isProseAUQVisible only matched `A)`-style lettered or `1.`-style numbered
options, so the question went undetected: the model surfaced it at ~2m19s
(well under the 300s budget) but the harness kept scoring the run "working"
off the spinner glyphs and timed out — a false timeout on a question that was
already on screen.

Add Pattern 3: when an interrogative line ('?') is present AND 3+ bold-bullet
markers (`- **`) appear in the 4KB tail, classify as a prose AUQ. Bold is the
discriminator vs incidental prose bullets; the line anchor is dropped (stripAnsi
can collapse option lines) and the existing `❯ 1.` cursor gate still defers to a
live native list. Wires through the existing classifyVisible 'asked' path and the
timeout high-water-mark, so office-hours now classifies 'asked' instead of
'timeout'. Five unit cases: the office-hours render passes; no-'?', <3-bullet,
plain-bullet, and native-cursor cases stay false.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(pty): detect stripAnsi-collapsed prose AUQs + judge spinner-precedence

The plan-eng/plan-design plan-mode + finding-floor smokes timed out even when
the skill HAD rendered a complete prose AskUserQuestion and was waiting: the PTY
strips cursor-positioning escapes, collapsing the option newlines/spaces so
"A) ..." arrives as "A(recommended)" / "-B:" and "Reply with A, B, or C" as
"ReplywithA,B,orC". Every line-anchored detector (Patterns 1-3) returns false on
those bytes, so proseAUQEverObserved never latched and the run timed out on a
question that was already on screen.

Add Pattern 4/5: a two-signal collapsed-form detector — a reply/recommendation
marker (space-insensitive "reply with [A-D]", "Recommendation:", or
"(recommended)") AND 2+ distinct A-D letters each punctuated by ) : or (. The
conjunction is what separates a real AUQ from incidental report prose; verified
true on the verbatim failing-run buffers where Patterns 1-3 return false.

Also fix the Haiku judge spinner bias: of 614 verdicts, 569 were 'working' and
95 of those noted a question was visible — Claude Code keeps the spinner
animating at an idle prose decision, so the judge coin-flipped. Add a precedence
override: when an option list AND a Recommendation/Reply instruction are both
visible, classify WAITING even with spinner glyphs. Kept the strict dual-signal
gate (never option-list-alone) so auto-decide-preserved doesn't flip.

5 unit tests pin the two-signal contract (2 true on real collapsed bytes, 3
false guards). 90 -> 95 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(plan-review): ask-first scope gate for plan-eng + plan-design review

On an empty/cold invocation, plan-eng-review and plan-design-review would dive
straight into repo exploration (plan-eng) or a 7-pass mockup+audit (plan-design)
and only ask the user much later, if at all. plan-ceo-review already asks first
via an unconditional Step-0 gate and behaves well; these two did not.

Add a hard-STOP scope gate as the FIRST operational instruction in each skill
(above the design-doc check / pre-review audit / mockup defaults it explicitly
overrides): the first tool call must be AskUserQuestion confirming the review
target, before any git/Read/Grep/Glob/Bash or mockup generation. Under
--disallowedTools the options render as plain column-0 lettered prose with a
Recommendation + "Reply with A, B, or C" line so the answer is detectable.

This is correct cold-start UX (confirm what to review before grinding a full
review on nothing) and it is the product half of the plan-mode smoke fix; the
harness collapsed-form detector is the deterministic half that catches the ask
however it renders. Templates + regenerated SKILL.md (default variant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(tiers): reclassify stochastic plan-eng/plan-design ask-first smokes as periodic

plan-eng-review and plan-design-review run a long explore/audit before their
first AskUserQuestion, so whether the plan-mode + finding-floor smokes reach a
terminal outcome within the 300s/600s budget depends on stochastic ask-first
compliance (measured ~50-67%/run even with the hardened gate). Per the
"non-deterministic -> periodic" tiering rule, move the four affected smokes
(plan-eng/plan-design review-plan-mode + finding-floor) to periodic.

The deterministic harness fix (collapsed-form detector + judge precedence) and
the ask-first gate lift these from always-failing to mostly-passing and are the
real product+harness improvements; periodic monitoring tracks the rate weekly
without blocking PRs on an LLM coin-flip. plan-ceo/plan-devex ask-first reliably
and stay gate-tier.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(evals): gate the deterministic PTY plan-mode smokes in CI

The real-PTY plan-mode smokes never ran in CI — the gate was local-only. Add an
e2e-pty-plan-smoke matrix suite running the two deterministically-reliable ones
(office-hours-auto-mode, plan-mode-no-op) so a regression there blocks PRs. The
stochastic plan-eng/plan-design ask-first smokes stay periodic (touchfiles
E2E_TIERS) and are not CI-gated.

A fresh CI container has no ~/.claude.json, so the spawned interactive `claude`
would wedge on the onboarding + API-key-approval dialog. Add a scoped seed step
(hasCompletedOnboarding + key approval, its own ANTHROPIC_API_KEY env) before the
run — mirrors what the hermetic E2E child env seeds. Per-suite timeout override
(35 min) via matrix.suite.timeout so the PTY suite has headroom for --retry 2
without bumping the other 12 suites. Report runner count 12 -> 13.

Validate via workflow_dispatch before relying on the gate (PTY-in-CI is new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(evals): install gstack skill registry for the PTY smoke suite

The first dry-run of e2e-pty-plan-smoke failed: the spawned interactive `claude`
printed "Unknown command: /plan-ceo-review". .claude/skills is gitignored, so a
fresh CI checkout has no gstack skill registry and the TUI can't resolve
/office-hours or /plan-ceo-review.

Add a Register step (scoped to the suite, after Seed, before Run) that mirrors
setup's --no-prefix user-scoped registry minimally: $HOME/.claude/skills/gstack
-> repo (resolves the preambles' absolute ~/.claude/skills/gstack/bin/* and
<skill>/sections/* paths) + per-skill SKILL.md/sections symlinks for the two
skills these tests invoke. HOME is /github/home in this container and the runner
adds no HOME/CLAUDE_CONFIG_DIR override (no hermetic mode), so $HOME is the right
anchor — the Seed step already proved claude reads it. No ./setup (binary build
+ Chromium + fonts + /dev/tty prompt); SKILL.md + bin/ + sections/ are committed.

Self-validating: fails the step loudly on a dangling symlink or missing
`name:` frontmatter, so a moved target surfaces here instead of as a silent
35-min "Unknown command" timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v1.58.4.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-21 07:15:19 -07:00

484 lines
22 KiB
Cheetah

---
name: sync-gbrain
preamble-tier: 2
version: 1.0.0
description: |
Keep gbrain current with this repo's code and refresh agent search
guidance in CLAUDE.md. Wraps the gstack-gbrain-sync orchestrator with
state probing, native code-surface registration, capability checks,
and a verdict block. Re-runnable, idempotent. Use when: "sync gbrain",
"refresh gbrain", "re-index this repo", "gbrain search isn't finding
things". (gstack)
triggers:
- sync gbrain
- refresh gbrain
- reindex repo
- update gbrain
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
- AskUserQuestion
---
{{PREAMBLE}}
# /sync-gbrain — Keep gbrain current and teach the agent to use it
You are running the canonical "keep this brain up to date" verb. /setup-gbrain
installs gbrain once; /sync-gbrain runs every time the user wants the brain
refreshed against this repo's current state, and refreshes the agent-side
guidance in CLAUDE.md so the coding agent knows when to prefer `gbrain`
search over Grep.
**Architecture (post-codex review):** This skill uses gbrain v0.20.0+'s
**native code surfaces** (`gbrain sources add`, `gbrain sync --strategy code`,
`gbrain reindex-code`, `gbrain code-def/code-refs/code-callers/code-callees`).
It does NOT use `gbrain import` (that path is for markdown directories).
It does NOT touch `~/.gstack/` indexing (the existing `gstack-gbrain-source-wireup`
owns that — never double-store).
## User-invocable
When the user types `/sync-gbrain`, run this skill. Argument modes (parsed by
the skill itself, not a dispatcher binary):
- `/sync-gbrain` — incremental sync (default; mtime fast-path; ~50ms steady-state)
- `/sync-gbrain --full` — full code reindex via `gbrain reindex-code` (~25-35 min on a big repo). Auto-builds the call graph (`gbrain dream`) **only when it was never built**.
- `/sync-gbrain --dream` — build this source's call graph (`gbrain code-callers`/`code-callees`) via a source-scoped `gbrain dream --source <id>` cycle; ~minutes; runs lock-free after the sync stages. Always forces, even if already built. Only produces a graph on a code-aware schema pack; otherwise the run reports a WARN explaining why the graph is still empty.
- `/sync-gbrain --no-dream` — skip the dream cycle that `--full` would otherwise auto-run.
- `/sync-gbrain --code-only` — only run the code stage; skip memory + brain-sync
- `/sync-gbrain --dry-run` — preview what would sync; no writes anywhere
- `/sync-gbrain --no-memory` / `--no-brain-sync` — selectively skip stages
- `/sync-gbrain --quiet` — suppress per-stage output
- `/sync-gbrain --refresh-cache` — force-rebuild brain-aware planning cache (v1.48; replaces /brain-refresh-context per D1 fold). Skips code + memory stages; routes to `gstack-brain-cache refresh --project <slug>`.
- `/sync-gbrain --audit` — emit summary of gstack-owned pages per project + sensitive-content audit (v1.48 / D10 lifecycle). Read-only.
Pass-through args go straight to the orchestrator at
`{{BIN_DIR}}/gstack-gbrain-sync.ts`.
**`--refresh-cache` short-circuit:** when this flag is present, the skill
runs ONLY the cache refresh (`gstack-brain-cache refresh --project <slug>`
for the current worktree's slug, plus a cross-project refresh of
user-profile if `gstack/user-profile/<user-slug>` exists). Code +
memory + brain-sync stages are skipped. Useful when the user knows the
brain has new info gstack should pick up before the next planning skill.
**`--audit` short-circuit:** when this flag is present, the skill runs
`gstack-brain-cache list --project <slug> --json`, summarizes by page
type, then scans for any cached salience entries that ended up outside
the SALIENCE_DEFAULT_ALLOWLIST (T17 / D9 leak check). Read-only; no
modifications to brain or cache.
---
## Step 1: State probe
Before doing anything, check that /setup-gbrain has been run on this Mac.
```bash
~/.claude/skills/gstack/bin/gstack-gbrain-detect 2>/dev/null
```
**Brain trust policy gate (v1.48 / Phase 1.5 / D4 — added by T13+T5c):**
If `gbrain_mcp_mode == "remote-http"` from the detect output AND the per-
endpoint policy is `unset`, the policy question MUST fire here before
the orchestrator runs. Local engines auto-set to `personal` silently per
the per-transport default table.
```bash
_HASH=$(~/.claude/skills/gstack/bin/gstack-config endpoint-hash 2>/dev/null)
_POLICY=$(~/.claude/skills/gstack/bin/gstack-config get brain_trust_policy@$_HASH 2>/dev/null || echo unset)
echo "BRAIN_TRUST_POLICY[$_HASH]: $_POLICY"
```
If `_POLICY == "unset"` AND `_HASH != "local"`, AskUserQuestion per the
Step 9.5 wording in `/setup-gbrain` (personal vs shared, with persistence
to `brain_trust_policy@<hash>` and conditional `artifacts_sync_mode=full`
flip for personal). Then continue.
If `_POLICY == "unset"` AND `_HASH == "local"`, auto-set personal:
```bash
~/.claude/skills/gstack/bin/gstack-config set brain_trust_policy@$_HASH personal
```
**Split-engine model (v1.34.0.0+).** Code stage runs locally against the
per-machine gbrain engine (PGLite or whatever `gbrain config` points to),
with each worktree of a repo registered as its own source. **Memory stage
also runs locally** in local-stdio MCP mode — `gstack-memory-ingest` shells
out to `gbrain import` against the same local engine. In remote-http MCP
mode (Path 4), the memory stage instead persists staged markdown to
`~/.gstack/transcripts/<run-id>/` and the artifacts pipeline pushes it to
the brain admin's pull job (plan D11). Brain-sync (the `gstack-brain-sync`
push to git) is the one stage that never touches local engine and runs
regardless of mode.
Practically: local PGLite stays code-only on remote-http machines; the
remote brain holds everything else. Local-stdio machines mix code +
transcripts in one local engine, as they always have.
Also check the per-repo trust policy. If `gstack-gbrain-repo-policy get` for
this repo returns `deny`, STOP:
> "This repo's gbrain trust policy is `deny`. Run `/setup-gbrain --repo` to
> change it before syncing."
---
## Step 1.5: Local engine pre-flight (plan D12)
Read `gbrain_local_status` from the Step 1 detect output. Branch as follows
BEFORE invoking the orchestrator:
- **`ok`**: proceed to Step 2 normally.
- **`timeout`**: proceed to Step 2 — the engine is most likely healthy but
slow (cold pooler connection, #1964). Tell the user in one line: "Engine
probe timed out (>15s) — proceeding; raise `GSTACK_GBRAIN_PROBE_TIMEOUT_MS`
if your pooler is slow." Do NOT treat this as a broken config.
- **`no-cli`**: STOP. "Local gbrain CLI not installed. Run `/setup-gbrain`
first."
- **`missing-config`** AND `gbrain_mcp_mode == "remote-http"`: tell the user
"Your brain queries (the `mcp__gbrain__*` tools) work via remote MCP, but
symbol code search needs a local PGLite. Run `/setup-gbrain` and pick
'Yes' at the new 'local code index' prompt (Step 4.5), or run
`gbrain init --pglite --json --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024`
directly (drop the voyage flags if `VOYAGE_API_KEY` isn't set). Continuing
without code stage."
Then proceed to Step 2 — the orchestrator's `runCodeImport()` and
`runMemoryIngest()` will return SKIP per plan D12; only `runBrainSyncPush()`
will run. Do NOT abort.
- **`missing-config`** AND `gbrain_mcp_mode != "remote-http"`: STOP. "Local
gbrain CLI is installed but no engine config. Run `/setup-gbrain` first."
- **`broken-config`** OR **`broken-db`**: STOP with a clear message:
```
Local gbrain config at ~/.gbrain/config.json points at an unreachable
engine (status: {gbrain_local_status}). Two options:
1. Re-run /setup-gbrain — Step 1.5 offers Retry / Switch to PGLite /
Switch brain mode / Quit (plan D4).
2. Repair manually: mv ~/.gbrain/config.json ~/.gbrain/config.json.bak
&& gbrain init --pglite --json --embedding-model voyage:voyage-code-3 \
--embedding-dimensions 1024 (drop voyage flags if VOYAGE_API_KEY unset)
Re-run /sync-gbrain after.
```
Do NOT continue — the orchestrator would skip code+memory and only run
brain-sync, which is a degraded state the user should fix explicitly.
This pre-flight short-circuits the orchestrator before it spends ~80ms
probing the engine again. The orchestrator independently runs the same
classifier for defense-in-depth, but Step 1.5's STOP is where the user
gets the actionable remediation message.
---
## Step 2: Run the orchestrator
Pass user args to the orchestrator. Do not paraphrase them — pass through
as-is.
```bash
bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts <user-args>
```
The orchestrator runs three stages: code → memory → brain-sync (per the
plan's storage tiering). Each stage failure is non-fatal; subsequent stages
still run. State is persisted to `~/.gstack/.gbrain-sync-state.json` via
tmp-file + atomic rename. Concurrent runs are blocked by a lock file at
`~/.gstack/.sync-gbrain.lock` (5-min stale-takeover).
---
## Step 3: Code-index health check
After the sync run, query gbrain for the cwd source's page_count:
```bash
SOURCE_ID=$(grep -o '"source_id":"[^"]*"' ~/.gstack/.gbrain-sync-state.json 2>/dev/null \
| head -1 | sed 's/.*"source_id":"//;s/".*//')
PAGES=$(gbrain sources list --json 2>/dev/null \
| jq -r --arg id "$SOURCE_ID" '.sources[] | select(.id==$id) | .page_count' 2>/dev/null \
|| echo 0)
echo "cwd source: $SOURCE_ID, page_count: $PAGES"
```
If `PAGES` is 0 or empty AND the user did NOT pass `--no-code` AND mode was
not `--full`, AskUserQuestion via the format in the preamble:
> D1 — This repo has 0 indexed pages in gbrain. Run a full code reindex now?
>
> ELI10: gbrain hasn't indexed this repo's code yet. The semantic search
> tools (`gbrain search`, `code-def`, `code-refs`) will return nothing
> until we run a full pass. Takes ~25-35 minutes on a big Mac.
>
> Recommendation: A — the brain is unusable for code search until indexed,
> and Step 2 of this skill already verified gbrain is configured correctly.
>
> Note: options differ in kind, not coverage — no completeness score.
>
> A) Run /sync-gbrain --full now (recommended)
> B) Skip — I'll run it later
If A: re-invoke the orchestrator with `--full --code-only`.
If B: continue to Step 4 with the empty-corpus state recorded.
---
## Step 3.5: Call-graph health check (offer `--dream`)
`gbrain code-callers` / `code-callees` (who-calls-this / what-this-calls) return
`count: 0` until a `gbrain dream` cycle runs the `resolve_symbol_edges` phase for
this source — not done by the code import in Step 2.
**One hard prerequisite:** building a call graph requires this source's active
**schema pack to extract code symbols** (the `extract_atoms` phase). On a pack
that doesn't declare it (e.g. `gbrain-base` / `gbrain-base-v2`), a `dream` cycle
completes but `resolve_symbol_edges` matches nothing — the graph stays empty no
matter how many times you run it. So "build the call graph" is only meaningful on
a code-aware pack. The `--dream` stage detects this and reports it honestly
(a WARN row) rather than claiming a build that didn't happen. gbrain exposes pack
capability only at cycle runtime (no pre-flight query as of 0.41.x), so we can't
detect it before running. `code-def` / `code-refs` need the same symbol
extraction; they are NOT free "direct lookups" on a non-code-aware pack.
Detect whether this source's call graph is built via doctor's `cycle_freshness`
check, matching the cwd `SOURCE_ID` literally:
```bash
SOURCE_ID=$(grep -o '"source_id":"[^"]*"' ~/.gstack/.gbrain-sync-state.json 2>/dev/null \
| head -1 | sed 's/.*"source_id":"//;s/".*//')
CYCLE=$(gbrain doctor --json --fast 2>/dev/null \
| jq -r --arg id "$SOURCE_ID" '
(.checks[] | select(.name=="cycle_freshness")) as $c
| if $c.status=="ok" then "completed"
elif ($c.message | index($id)) then "never"
else "unknown" end' 2>/dev/null || echo unknown)
# index($id) = literal substring (NOT test() regex), matching the lib reader in
# cycleCompleted(). A fail/warn that doesn't name this source → "unknown" (don't
# mask other-source failures).
echo "call graph for $SOURCE_ID: $CYCLE"
```
If `CYCLE == never` AND the user did NOT pass `--dream`/`--full` AND Step 3
`PAGES > 0`, AskUserQuestion via the format in the preamble:
> D2 — This repo's call graph isn't built. Build it now?
>
> ELI10: `gbrain code-callers`/`code-callees` (who calls this function / what it
> calls) return nothing until the `resolve_symbol_edges` phase runs for this
> source. `gbrain dream --source <this source>` runs it (scoped to this
> worktree's code, takes a few minutes). It only produces a graph if this
> source's schema pack extracts code symbols; if it doesn't, the run completes
> but the graph stays empty and the dream row will say so.
>
> Recommendation: A — call-graph queries return 0 until this runs, and the code
> index is already populated. If A comes back as a WARN ("pack does not extract
> code symbols"), the fix is a code-aware schema pack, not re-running dream.
>
> Note: options differ in kind, not coverage — no completeness score.
>
> A) Run /sync-gbrain --dream now (recommended)
> B) Skip — I'll run it later
If A: re-invoke the orchestrator with `--dream --code-only` (skips memory +
brain-sync; the dream stage still runs because it's gated on `--dream`). Then
report the dream stage's ACTUAL row — `OK call graph built (N edges)` vs a
`WARN` that names why the graph is still empty (non-code-aware pack, missing
embedding key, or 0 edges matched). Do not claim success on a WARN.
If B: continue to Step 4 with the call-graph-not-built state recorded for the
verdict.
If `CYCLE == completed` or `unknown`, do not prompt — but note `completed` means
only that a cycle has run, not that edges exist (a non-code-aware pack reports
`completed` with an empty graph). Step 5's verdict row surfaces the real state.
---
## Step 4: Refresh `## GBrain Search Guidance` block in CLAUDE.md
Capability check (per /plan-eng-review §6):
```bash
SLUG="_capability_check_$$"
CAPABILITY_OK=0
if [ -f ~/.gbrain/config.json ] && \
gbrain --version 2>/dev/null | grep -q '^gbrain '; then
# Do NOT export GBRAIN_PREPARE here (#1965). gbrain auto-disables prepared
# statements on transaction-mode poolers (port 6543) — forcing them on
# breaks every write with "prepared statement does not exist". Users on a
# session-mode pooler at 6543 can set GBRAIN_PREPARE=true themselves (the
# gbrain banner documents this override).
if echo "ping" | gbrain put "$SLUG" >/dev/null 2>&1; then
# Retry search up to 3 times with 1s delay — under transaction-mode
# pooling the search index may not be visible on the next connection
# immediately after the put.
for _attempt in 1 2 3; do
if gbrain search "ping" 2>/dev/null | grep -q "$SLUG"; then
CAPABILITY_OK=1
break
fi
sleep 1
done
fi
fi
gbrain delete "$SLUG" 2>/dev/null || true
```
Then update CLAUDE.md based on capability state:
**If `CAPABILITY_OK=1`** — write or update the block. Idempotent: find the
HTML-comment-delimited block; replace its body if it exists; append at the
end of CLAUDE.md if it doesn't. NEVER duplicate. Block is machine-AGNOSTIC
(no engine, no page counts, no last-sync time — those are in the existing
`## GBrain Configuration` block).
Verbatim block content (copy exactly):
```markdown
## GBrain Search Guidance (configured by /sync-gbrain)
<!-- gstack-gbrain-search-guidance:start -->
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.
**This worktree is pinned to a worktree-scoped code source** via the
`.gbrain-source` file in the repo root (kubectl-style context).
`gbrain code-def`, `code-refs`, `code-callers`, `code-callees`, `search`, and
`query` from anywhere under this worktree route to that source by default —
no `--source` flag needed (gbrain >= 0.41.38.0; on older gbrain the call-graph
commands need `--source "$(cat .gbrain-source)"`). Conductor sibling worktrees
of the same repo each have their own pin and their own indexed pages, so
semantic results match the code on disk here.
Call-graph queries (`code-callers`/`code-callees`) also need the graph to be
built first — run `/sync-gbrain --dream` (or `--full`) if they return
`count: 0`. This only works if this source's gbrain schema pack extracts code
symbols; on a non-code-aware pack `--dream` completes but the graph stays empty
and reports a WARN. `code-def`/`code-refs` need the same extraction.
Two indexed corpora available via the `gbrain` CLI:
- This worktree's code (auto-pinned via `.gbrain-source`).
- `~/.gstack/` curated memory (registered as `gstack-brain-<user>` source via
the existing federation pipeline).
Prefer gbrain when:
- "Where is X handled?" / semantic intent, no exact string yet:
`gbrain search "<terms>"` or `gbrain query "<question>"`
- "Where is symbol Y defined?" / symbol-based code questions:
`gbrain code-def <symbol>` or `gbrain code-refs <symbol>`
- "What calls Y?" / "What does Y depend on?":
`gbrain code-callers <symbol>` / `gbrain code-callees <symbol>`
- "What did we decide last time?" / past plans, retros, learnings:
`gbrain search "<terms>" --source gstack-brain-<user>`
Grep is still right for known exact strings, regex, multiline patterns, and
file globs. Run `/sync-gbrain` after meaningful code changes; for ongoing
auto-sync across all worktrees, run `gbrain autopilot --install` once per
machine — gbrain's daemon handles incremental refresh on a schedule.
Safety: don't run `/sync-gbrain` while `gbrain autopilot` is active — the
orchestrator refuses destructive source ops when it detects a running autopilot
to avoid racing it (#1734). Prefer registering user repos with `gbrain sources
add --path <dir>` (no `--url`): URL-managed sources can auto-reclone, and the
sync code walk for them requires an explicit `--allow-reclone` opt-in.
<!-- gstack-gbrain-search-guidance:end -->
```
Use the Read + Edit tools. The find-and-replace target is the entire region
from `<!-- gstack-gbrain-search-guidance:start -->` through
`<!-- gstack-gbrain-search-guidance:end -->`. If those markers are missing,
search for `## GBrain Search Guidance (configured by /sync-gbrain)` heading
and replace from there to the next `## ` or EOF. If no heading exists, append
the entire block at the end of CLAUDE.md.
**Atomic write:** write the new CLAUDE.md content to a tmp file alongside it
(e.g., `CLAUDE.md.sync-gbrain.tmp`) then `mv` to atomic-rename, so a crash
mid-write never leaves the file half-modified.
**If `CAPABILITY_OK=0`** — REMOVE the block entirely if present. Use the same
Edit tool to strip the start/end-marker region. The `## GBrain Configuration`
block stays in place (it's a record of the install, not a capability claim).
Do NOT crash if CLAUDE.md is missing or unwritable — log a warning and
continue.
---
## Step 5: Verdict block (idempotent doctor output)
Print a status block matching `/setup-gbrain` Step 10 conventions. Each row
is `[OK]/[FIX]/[WARN]/[ERR]`. Reuse `gbrain doctor --json --fast` for
informational rows but DO NOT gate the guidance block on doctor (per
/plan-eng-review §6 — doctor is too strict for unrelated reasons).
```
gbrain status: GREEN
CLI ............. OK <gbrain version>
Engine .......... OK <pglite|supabase>
Capability ...... OK write+search round-trip
CWD source ...... OK <gstack-code-{repo_slug}> (page_count=<N>)
Call graph ...... OK <N> edges resolved (code-callers/callees live)
~/.gstack source. OK <gstack-brain-{user}> (page_count=<N>) — managed by /setup-gbrain
Memory sync ..... OK <artifacts_sync_mode>
CLAUDE.md ....... OK ## GBrain Search Guidance present
Last sync ....... OK <last_sync from state file>
Run `/sync-gbrain` again any time gbrain feels off; safe and idempotent.
```
The **Call graph** row reports the most authoritative signal available:
1. **If a dream stage ran this invocation** (`--dream`, or `--full` auto-build),
mirror its row verbatim — it's the ground truth for this run:
- `OK <N> edges resolved (code-callers/callees live)`
- `WARN dream ran but this source's schema pack does not extract code symbols
— switch to a code-aware pack (\`gbrain schema use <pack>\`)`
- `WARN dream ran but the embed phase failed (missing embedding key)`
- `WARN dream ran but resolved 0 edges (no code symbols matched yet)`
2. **Otherwise** fall back to the `CYCLE` value from Step 3.5, with honest wording
(a completed cycle proves a cycle ran, NOT that edges exist):
- `completed` → `OK cycle complete — code-callers/callees live IF this source's pack extracts code symbols`
- `never` → `WARN call graph not built — run /sync-gbrain --dream`
- `unknown` → `WARN could not probe call graph (doctor unavailable) — run /sync-gbrain --dream if code-callers returns 0`
Any `WARN` Call graph row flips the verdict to YELLOW.
If any row is YELLOW or RED, the verdict line says so and the failing rows
surface a one-line "next action" (e.g., `Capability ...... ERR capability
check failed; CLAUDE.md guidance block REMOVED — run /setup-gbrain to repair`).
A `never`/`unknown` Call graph row flips the verdict to YELLOW.
---
## Concurrency note
This skill is safe to run concurrently from multiple terminals on the same
Mac. The orchestrator acquires a lock at `~/.gstack/.sync-gbrain.lock` before
any state-file or CLAUDE.md mutation and exits with code 2 if another sync is
in flight. Stale locks (process died) auto-clear after 5 minutes.
## Cross-machine note
The `## GBrain Search Guidance` block is committed to the repo's CLAUDE.md
and travels with `git push`/`git pull` — NOT through `~/.gstack/.brain-allowlist`
(which is for `~/.gstack/` brain-sync only). On a different Mac with a synced
CLAUDE.md but no local gbrain, /sync-gbrain detects the mismatch via the
capability check and REMOVES the block (the local agent shouldn't be told to
use a tool that isn't installed).
## Status reporting
End with a Completion Status (per the preamble protocol):
- **DONE** — all stages green, CLAUDE.md guidance block present, verdict GREEN.
- **DONE_WITH_CONCERNS** — sync ran but at least one stage failed or capability
check failed. List which.
- **BLOCKED** — could not acquire lock, gbrain not on PATH, or per-repo policy
is deny. State the blocker.
- **NEEDS_CONTEXT** — /setup-gbrain has not been run, or `gbrain doctor` shows
a state that requires user decision (e.g., engine migration).