diff --git a/CLAUDE.md b/CLAUDE.md index dc62ad561..2093f4585 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -881,6 +881,12 @@ The active skill lives at `~/.claude/skills/gstack/`. After making changes: 2. Fetch and reset in the skill directory: `cd ~/.claude/skills/gstack && git fetch origin && git reset --hard origin/main` 3. Rebuild: `cd ~/.claude/skills/gstack && bun run build` +**If you use gbrain:** the `git reset --hard` in step 2 reverts the brain-aware +(`GBRAIN_CONTEXT_LOAD` / `GBRAIN_SAVE_RESULTS`) blocks that `gstack-config +gbrain-refresh` renders into the install (those generated blocks differ from +`main` by design). After deploying, re-run `gstack-config gbrain-refresh` to +restore them across all your projects' Claude sessions. It's idempotent. + Or copy the binaries directly: - `cp browse/dist/browse ~/.claude/skills/gstack/browse/dist/browse` - `cp design/dist/design ~/.claude/skills/gstack/design/dist/design` diff --git a/bin/gstack-config b/bin/gstack-config index 01defcef8..ec465b281 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -396,8 +396,29 @@ case "${1:-}" in case "$STATUS" in ok) - echo "Detected gbrain v$VERSION → brain-aware blocks will render in planning-skill SKILL.md files." - echo "Run 'bun run gen:skill-docs' in the gstack repo (or re-run ./setup) to regenerate now." + echo "Detected gbrain v$VERSION." + # Render brain-aware blocks INTO the global install so EVERY project's + # Claude sessions get them (other projects read SKILL.md + sections from + # ~/.claude/skills/gstack via absolute paths baked at gen time). Guards + # (never mutate an arbitrary directory): the target must exist, not be a + # symlink (a symlinked install points at a dev worktree — rendering there + # would dirty tracked source), and look like a real gstack clone. + INSTALL_DIR="$HOME/.claude/skills/gstack" + if [ ! -d "$INSTALL_DIR" ]; then + echo "No global install at $INSTALL_DIR — nothing to render. (Dev workspaces get blocks via bin/dev-setup.)" + elif [ -L "$INSTALL_DIR" ]; then + echo "Skip: $INSTALL_DIR is a symlink (likely a dev worktree). Rendering there would dirty tracked source — run bin/dev-setup in that worktree instead." + elif [ ! -f "$INSTALL_DIR/VERSION" ] || [ ! -f "$INSTALL_DIR/package.json" ]; then + echo "Skip: $INSTALL_DIR doesn't look like a gstack clone (missing VERSION/package.json) — refusing to modify it." + elif ! command -v bun >/dev/null 2>&1; then + echo "Skip: bun not on PATH — can't render. Install bun, then re-run 'gstack-config gbrain-refresh'." + elif ( cd "$INSTALL_DIR" && bun run gen:skill-docs:user --host claude >/dev/null 2>&1 ); then + echo "Rendered brain-aware blocks into $INSTALL_DIR — now live across all your projects' Claude sessions." + echo "Note: this dirties the install's git tree (generated blocks differ from main, by design)." + echo " A 'git reset --hard origin/main' there reverts them; re-run 'gstack-config gbrain-refresh' to restore." + else + echo "Warning: render failed. Run 'cd $INSTALL_DIR && bun run gen:skill-docs:user --host claude' manually to see the error." + fi ;; *) echo "gbrain not detected (local-status: $STATUS) → brain-aware blocks will be suppressed in planning-skill SKILL.md files." diff --git a/test/gbrain-refresh-install-render.test.ts b/test/gbrain-refresh-install-render.test.ts new file mode 100644 index 000000000..f1494d47d --- /dev/null +++ b/test/gbrain-refresh-install-render.test.ts @@ -0,0 +1,60 @@ +import { describe, test, expect } from 'bun:test'; +import * as path from 'path'; +import * as fs from 'fs'; + +// Static tripwires for the C (machine-wide) render in `gstack-config +// gbrain-refresh`. The render mutates the shared global install, so the guards +// that stop it from touching the wrong directory are load-bearing — these fail +// CI if any guard is dropped. +const ROOT = path.resolve(import.meta.dir, '..'); +const SRC = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-config'), 'utf-8'); + +// Pull out just the gbrain-refresh `ok)` branch so assertions can't be +// satisfied by unrelated text elsewhere in the file. +function okBranch(): string { + const start = SRC.indexOf('gbrain-refresh)'); + const ok = SRC.indexOf('ok)', start); + const end = SRC.indexOf(';;', ok); + if (start < 0 || ok < 0 || end < 0) throw new Error('Could not locate gbrain-refresh ok) branch'); + return SRC.slice(ok, end); +} + +describe('gstack-config gbrain-refresh: machine-wide render guards', () => { + const branch = okBranch(); + + test('targets the global install', () => { + expect(branch).toContain('$HOME/.claude/skills/gstack'); + }); + + test('refuses a symlinked install (would dirty a dev worktree)', () => { + expect(branch).toMatch(/\[ -L "\$INSTALL_DIR" \]/); + }); + + test('verifies it is a real gstack clone before mutating it', () => { + expect(branch).toContain('$INSTALL_DIR/VERSION'); + expect(branch).toContain('$INSTALL_DIR/package.json'); + }); + + test('requires bun on PATH', () => { + expect(branch).toContain('command -v bun'); + }); + + test('renders the :user variant in place into the install', () => { + expect(branch).toContain('gen:skill-docs:user --host claude'); + }); + + test('is self-documenting about the reset --hard / re-run cycle', () => { + expect(branch).toContain('reset --hard'); + expect(branch).toContain('gbrain-refresh'); + }); +}); + +describe('CLAUDE.md: deploy section documents the re-run', () => { + test('notes re-running gbrain-refresh after reset --hard', () => { + const claudeMd = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf-8'); + const idx = claudeMd.indexOf('## Deploying to the active skill'); + expect(idx).toBeGreaterThan(-1); + const section = claudeMd.slice(idx, idx + 1200); + expect(section).toContain('gbrain-refresh'); + }); +});