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/');
+ });
+});