From 3625f4c7216f00342fe6fb78fcd26031f47783b0 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 8 Jun 2026 06:20:11 -0700 Subject: [PATCH] feat(dev-setup): render gbrain :user variant to an untracked workspace dir Stops the dev/Conductor workspace from dirtying tracked SKILL.md source. setup honors GSTACK_SKIP_GBRAIN_REGEN (passed inline by dev-setup, never exported) and skips the in-place :user regen; detection is still persisted (PID-unique tmp so concurrent workspaces can't clobber it). dev-setup instead renders the :user variant into .claude/gstack-rendered (gitignored, per-workspace) and repoints the workspace SKILL.md symlinks at it, so the workspace gets brain-aware blocks while the worktree stays canonical. dev-teardown removes the render. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + bin/dev-setup | 46 ++++++++++++++- bin/dev-teardown | 9 ++- setup | 33 ++++++++--- test/dev-setup-render-isolation.test.ts | 77 +++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 test/dev-setup-render-isolation.test.ts diff --git a/.gitignore b/.gitignore index 9fde8011f..23c6094e5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ make-pdf/dist/ bin/gstack-global-discover* .gstack/ .claude/skills/ +.claude/gstack-rendered/ .claude/scheduled_tasks.lock .claude/*.lock .agents/ diff --git a/bin/dev-setup b/bin/dev-setup index 0d8460f91..00a286706 100755 --- a/bin/dev-setup +++ b/bin/dev-setup @@ -72,7 +72,48 @@ fi # no-op skip (no install, no decline marker). A dev workspace must never mutate # global settings.json. To install the hooks, run `./setup --plan-tune-hooks` # directly (outside dev-setup). Saved prefix/other config preferences still apply. -"$GSTACK_LINK/setup" --plan-tune-hooks=prompt /dev/null; then + echo "" + echo "gbrain detected — rendering brain-aware skills into .claude/gstack-rendered (workspace-only, untracked)..." + rm -rf "$RENDER_DIR" + if ( cd "$REPO_ROOT" && bun run gen:skill-docs:user --host claude --out-dir "$RENDER_DIR" >/dev/null 2>&1 ); then + # Repoint each project-local SKILL.md symlink whose worktree target has a + # rendered counterpart. The skill DIRECTORY name (basename of the symlink + # target's dir) maps to RENDER_DIR//SKILL.md, which is robust to + # frontmatter renames and the gstack- prefix on the link name. + repointed=0 + for skill_link in "$REPO_ROOT"/.claude/skills/*/SKILL.md; do + [ -L "$skill_link" ] || continue + target="$(readlink "$skill_link")" + skilldir="$(basename "$(dirname "$target")")" + rendered="$RENDER_DIR/$skilldir/SKILL.md" + if [ -f "$rendered" ]; then ln -snf "$rendered" "$skill_link"; repointed=$((repointed + 1)); fi + done + echo " $repointed workspace skills now serve brain-aware blocks (worktree stays canonical)." + else + echo " warning: brain-aware render failed — workspace uses canonical skills." + fi +fi echo "" echo "Dev mode active. Skills resolve from this working tree." @@ -80,4 +121,7 @@ echo " .claude/skills/gstack → $REPO_ROOT" echo " .agents/skills/gstack → $REPO_ROOT" echo "Edit any SKILL.md and test immediately — no copy/deploy needed." echo "" +echo "To make brain-aware blocks live across your OTHER projects too, run:" +echo " gstack-config gbrain-refresh" +echo "" echo "To tear down: bin/dev-teardown" diff --git a/bin/dev-teardown b/bin/dev-teardown index dc8f74260..06189e189 100755 --- a/bin/dev-teardown +++ b/bin/dev-teardown @@ -24,9 +24,16 @@ if [ -d "$CLAUDE_SKILLS" ]; then fi rmdir "$CLAUDE_SKILLS" 2>/dev/null || true - rmdir "$REPO_ROOT/.claude" 2>/dev/null || true fi +# ─── Clean up the untracked brain-aware render (bin/dev-setup step 7) ── +RENDER_DIR="$REPO_ROOT/.claude/gstack-rendered" +if [ -d "$RENDER_DIR" ]; then + rm -rf "$RENDER_DIR" + removed+=("claude/gstack-rendered") +fi +rmdir "$REPO_ROOT/.claude" 2>/dev/null || true + # ─── Clean up .agents/skills/ ──────────────────────────────── AGENTS_SKILLS="$REPO_ROOT/.agents/skills" if [ -d "$AGENTS_SKILLS" ]; then diff --git a/setup b/setup index 37991eda7..4ef26454c 100755 --- a/setup +++ b/setup @@ -1286,22 +1286,37 @@ fi DETECT_BIN="$SOURCE_GSTACK_DIR/bin/gstack-gbrain-detect" GBRAIN_STATE_DIR="${GSTACK_HOME:-$HOME/.gstack}" DETECTION_FILE="$GBRAIN_STATE_DIR/gbrain-detection.json" +# PID-unique tmp so concurrent setups (parallel Conductor workspaces) can't +# clobber each other's in-flight detection write. +DETECTION_TMP="$DETECTION_FILE.$$.tmp" mkdir -p "$GBRAIN_STATE_DIR" if [ -x "$DETECT_BIN" ]; then - if "$DETECT_BIN" > "$DETECTION_FILE.tmp" 2>/dev/null; then - mv "$DETECTION_FILE.tmp" "$DETECTION_FILE" - if grep -q '"gbrain_local_status": "ok"' "$DETECTION_FILE" 2>/dev/null; then - log "gbrain detected — regenerating Claude SKILL.md with brain-aware blocks (~250 token overhead per planning skill)..." - ( - cd "$SOURCE_GSTACK_DIR" - bun_cmd run gen:skill-docs:user --host claude 2>&1 | tail -3 - ) || log " warning: gen:skill-docs:user failed — run 'bun run gen:skill-docs:user' manually if you want brain-aware blocks" + if "$DETECT_BIN" > "$DETECTION_TMP" 2>/dev/null; then + mv "$DETECTION_TMP" "$DETECTION_FILE" + # Single source of truth for "is gbrain usable" — `--is-ok` runs live + # detection (exit 0 iff ok), so setup, bin/dev-setup, and gstack-config + # all gate on the same check instead of re-grepping the JSON. + if "$DETECT_BIN" --is-ok 2>/dev/null; then + if [ -n "${GSTACK_SKIP_GBRAIN_REGEN:-}" ]; then + # Dev/source tree (set by bin/dev-setup): never regenerate tracked + # SKILL.md in place — that dirties checked-in source. Detection is + # still persisted above; the dev workspace renders the :user variant + # into an untracked dir, and other projects get blocks via + # `gstack-config gbrain-refresh`. + log "gbrain detected — GSTACK_SKIP_GBRAIN_REGEN set: leaving tracked SKILL.md canonical (dev/source tree)." + else + log "gbrain detected — regenerating Claude SKILL.md with brain-aware blocks (~250 token overhead per planning skill)..." + ( + cd "$SOURCE_GSTACK_DIR" + bun_cmd run gen:skill-docs:user --host claude 2>&1 | tail -3 + ) || log " warning: gen:skill-docs:user failed — run 'bun run gen:skill-docs:user' manually if you want brain-aware blocks" + fi else log "gbrain not detected — brain-aware blocks suppressed in planning-skill SKILL.md files (zero token overhead)." log " To enable: install gbrain via /setup-gbrain, then re-run ./setup or 'gstack-config gbrain-refresh'." fi else - rm -f "$DETECTION_FILE.tmp" + rm -f "$DETECTION_TMP" log " warning: gstack-gbrain-detect failed — brain-aware blocks will stay suppressed" fi fi diff --git a/test/dev-setup-render-isolation.test.ts b/test/dev-setup-render-isolation.test.ts new file mode 100644 index 000000000..d6c815b71 --- /dev/null +++ b/test/dev-setup-render-isolation.test.ts @@ -0,0 +1,77 @@ +import { describe, test, expect } from 'bun:test'; +import * as path from 'path'; +import * as fs from 'fs'; + +// Static tripwires for the B2 render-isolation wiring. These fail CI if a +// refactor drops a load-bearing line, re-introducing the "dev-setup dirties +// tracked SKILL.md" drift (or worse, leaks the skip-guard into real installs). +const ROOT = path.resolve(import.meta.dir, '..'); +const read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), 'utf-8'); + +describe('dev-setup: worktree stays canonical', () => { + const devSetup = read('bin/dev-setup'); + + test('passes GSTACK_SKIP_GBRAIN_REGEN inline on the nested setup call', () => { + expect(devSetup).toContain('GSTACK_SKIP_GBRAIN_REGEN=1 "$GSTACK_LINK/setup"'); + }); + + test('never exports GSTACK_SKIP_GBRAIN_REGEN (would leak into other setup paths)', () => { + expect(devSetup).not.toMatch(/export\s+GSTACK_SKIP_GBRAIN_REGEN/); + }); + + test('renders the :user variant into an out-dir, not in place', () => { + expect(devSetup).toContain('--out-dir'); + expect(devSetup).toContain('.claude/gstack-rendered'); + }); + + test('gates the render on gstack-gbrain-detect --is-ok', () => { + expect(devSetup).toContain('--is-ok'); + }); +}); + +describe('setup: honors GSTACK_SKIP_GBRAIN_REGEN', () => { + const setup = read('setup'); + + test('skips the in-place :user regen when the guard is set', () => { + expect(setup).toContain('${GSTACK_SKIP_GBRAIN_REGEN:-}'); + // The guard must wrap the in-place render, not the detection persist. + const idx = setup.indexOf('GSTACK_SKIP_GBRAIN_REGEN'); + const after = setup.slice(idx, idx + 600); + expect(after).toContain('leaving tracked SKILL.md canonical'); + }); + + test('uses a PID-unique detection tmp (no concurrent clobber)', () => { + expect(setup).toContain('$DETECTION_FILE.$$.tmp'); + }); + + test('gates detection on the shared --is-ok check', () => { + expect(setup).toContain('"$DETECT_BIN" --is-ok'); + }); +}); + +describe('gen-skill-docs: section rewrite is gated on --out-dir', () => { + const gen = read('scripts/gen-skill-docs.ts'); + + test('rewriteSectionBase is a no-op without --out-dir', () => { + expect(gen).toContain('function rewriteSectionBase'); + const idx = gen.indexOf('function rewriteSectionBase'); + const body = gen.slice(idx, idx + 400); + expect(body).toContain('if (!OUT_DIR) return content'); + expect(body).toContain('sections'); // surgical: regex targets only /sections/ paths + }); +}); + +describe('dev-teardown: removes the untracked render', () => { + const teardown = read('bin/dev-teardown'); + + test('rm -rf the gstack-rendered dir', () => { + expect(teardown).toContain('gstack-rendered'); + expect(teardown).toMatch(/rm -rf .*RENDER_DIR/); + }); +}); + +describe('.gitignore: render dir is declared untracked', () => { + test('.claude/gstack-rendered/ is ignored', () => { + expect(read('.gitignore')).toContain('.claude/gstack-rendered/'); + }); +});