mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-17 07:10:12 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ make-pdf/dist/
|
||||
bin/gstack-global-discover*
|
||||
.gstack/
|
||||
.claude/skills/
|
||||
.claude/gstack-rendered/
|
||||
.claude/scheduled_tasks.lock
|
||||
.claude/*.lock
|
||||
.agents/
|
||||
|
||||
+45
-1
@@ -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
|
||||
#
|
||||
# GSTACK_SKIP_GBRAIN_REGEN=1 is passed INLINE (not exported) so it scopes to
|
||||
# exactly this nested setup call and can't leak into any other setup path. It
|
||||
# tells setup NOT to regenerate the gbrain :user variant into the tracked
|
||||
# worktree (that would dirty checked-in source). We render it into an untracked
|
||||
# per-workspace dir below instead.
|
||||
GSTACK_SKIP_GBRAIN_REGEN=1 "$GSTACK_LINK/setup" --plan-tune-hooks=prompt </dev/null
|
||||
|
||||
# 7. Brain-aware (gbrain) blocks — render into an untracked workspace dir.
|
||||
#
|
||||
# The worktree's SKILL.md files stay canonical (the guard above). If gbrain is
|
||||
# installed, render the :user variant (with GBRAIN_CONTEXT_LOAD +
|
||||
# GBRAIN_SAVE_RESULTS) into .claude/gstack-rendered (gitignored, per-workspace)
|
||||
# and repoint the workspace's SKILL.md symlinks at it. gen-skill-docs --out-dir
|
||||
# also rewrites the section-base path so section reads resolve to the render, not
|
||||
# the global install. Result: this workspace gets the full gbrain experience
|
||||
# while git stays clean. Other projects pick up blocks via `gstack-config
|
||||
# gbrain-refresh` (printed below).
|
||||
GBRAIN_DETECT="$REPO_ROOT/bin/gstack-gbrain-detect"
|
||||
RENDER_DIR="$REPO_ROOT/.claude/gstack-rendered"
|
||||
if [ -x "$GBRAIN_DETECT" ] && "$GBRAIN_DETECT" --is-ok 2>/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/<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"
|
||||
|
||||
+8
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user