From 7b7e2662e5c66f3a189cf57cabb85bc3106dfbc6 Mon Sep 17 00:00:00 2001 From: maxpetrusenkoagent Date: Fri, 12 Jun 2026 21:04:17 -0400 Subject: [PATCH] fix: honor terse skill rendering in user installs --- scripts/gen-skill-docs.ts | 18 +++++++++++- setup | 58 ++++++++++++++++++++++++++----------- test/gen-skill-docs.test.ts | 47 ++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 18 deletions(-) diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 5fea07713..39c2ed0de 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -126,8 +126,24 @@ const CATALOG_MODE: 'trim' | 'full' = (() => { // the model skips them when EXPLAIN_LEVEL: terse appears in the preamble echo). // Opt-in via the build flag so most users get the runtime-flexible default. const EXPLAIN_LEVEL_ARG = process.argv.find(a => a.startsWith('--explain-level')); +function loadConfiguredExplainLevel(): 'default' | 'terse' { + if (!RESPECT_DETECTION) return 'default'; + const stateDir = process.env.GSTACK_STATE_ROOT + || process.env.GSTACK_HOME + || process.env.GSTACK_STATE_DIR + || path.join(process.env.HOME || '', '.gstack'); + const configPath = path.join(stateDir, 'config.yaml'); + try { + const config = fs.readFileSync(configPath, 'utf-8'); + const match = config.match(/^\s*explain_level\s*:\s*(default|terse)\s*$/m); + return match?.[1] === 'terse' ? 'terse' : 'default'; + } catch { + return 'default'; + } +} + const EXPLAIN_LEVEL: 'default' | 'terse' = (() => { - if (!EXPLAIN_LEVEL_ARG) return 'default'; + if (!EXPLAIN_LEVEL_ARG) return loadConfiguredExplainLevel(); const val = EXPLAIN_LEVEL_ARG.includes('=') ? EXPLAIN_LEVEL_ARG.split('=')[1] : process.argv[process.argv.indexOf(EXPLAIN_LEVEL_ARG) + 1]; diff --git a/setup b/setup index 37991eda7..b495ffca9 100755 --- a/setup +++ b/setup @@ -1268,44 +1268,68 @@ if [ "$NO_TEAM_MODE" -eq 1 ]; then log "Team mode disabled: auto-update hook removed." fi -# ─── GBrain detection + conditional SKILL.md regen ────────────────────── +# ─── User-local SKILL.md regen ─────────────────────────────────────────── # # Detect whether gbrain is installed and persist the result to # ~/.gstack/gbrain-detection.json so gen-skill-docs can decide whether to -# render GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS blocks. If detected, -# regenerate the Claude-host SKILL.md files with the un-suppressed -# (compressed) brain-aware blocks via `bun run gen:skill-docs:user`. +# render GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS blocks. # -# If gbrain is not detected, the canonical no-gbrain SKILL.md files -# (which were just generated above by `gen:skill-docs --host claude` if -# applicable, or which are checked in) stay as-is. Zero token overhead -# for non-gbrain users. +# Also honor user-local explain_level=terse at setup/session-update time. The +# canonical checked-in SKILL.md files stay full-prose; `gen:skill-docs:user` +# applies local config only when setup updates a user's installed skills. # # Users who install gbrain after running ./setup should re-run setup OR # call `gstack-config gbrain-refresh` + `bun run gen:skill-docs:user`. DETECT_BIN="$SOURCE_GSTACK_DIR/bin/gstack-gbrain-detect" GBRAIN_STATE_DIR="${GSTACK_HOME:-$HOME/.gstack}" DETECTION_FILE="$GBRAIN_STATE_DIR/gbrain-detection.json" +USER_LOCAL_RENDER_REASON="" +USER_EXPLAIN_LEVEL="$("$GSTACK_CONFIG" get explain_level 2>/dev/null || echo default)" +if [ "$USER_EXPLAIN_LEVEL" = "terse" ]; then + USER_LOCAL_RENDER_REASON="explain_level=terse" +fi +# 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 "$USER_LOCAL_RENDER_REASON" ]; then + USER_LOCAL_RENDER_REASON="$USER_LOCAL_RENDER_REASON + brain-aware blocks" + else + USER_LOCAL_RENDER_REASON="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 +if [ -n "$USER_LOCAL_RENDER_REASON" ]; 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 local blocks via setup/session-update. + log "user-local SKILL.md render needed ($USER_LOCAL_RENDER_REASON) — GSTACK_SKIP_GBRAIN_REGEN set: leaving tracked SKILL.md canonical (dev/source tree)." + else + log "regenerating Claude SKILL.md with user-local settings ($USER_LOCAL_RENDER_REASON)..." + ( + 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 --host claude' manually to apply user-local settings" + fi +fi + # 11. Plan-tune cathedral hook install (T8). # # Registers PostToolUse (deterministic AUQ capture) + PreToolUse (preference diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 3554094ca..2ec9fe4dd 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -391,6 +391,39 @@ describe('gen-skill-docs', () => { expect(Buffer.byteLength(writingStyle, 'utf-8')).toBeLessThan(2_000); }); + test('user-local generation structurally honors explain_level: terse', () => { + const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-user-config-')); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-user-render-')); + try { + fs.writeFileSync(path.join(stateDir, 'config.yaml'), 'explain_level: terse\n'); + const result = Bun.spawnSync([ + 'bun', 'run', 'scripts/gen-skill-docs.ts', + '--respect-detection', + '--host', 'claude', + '--out-dir', outDir, + ], { + cwd: ROOT, + stdout: 'pipe', + stderr: 'pipe', + env: { ...process.env, GSTACK_HOME: stateDir, GSTACK_STATE_ROOT: stateDir }, + }); + const stderr = result.stderr.toString(); + const stdout = result.stdout.toString(); + + expect(result.exitCode, `${stdout}\n${stderr}`).toBe(0); + + const content = fs.readFileSync(path.join(outDir, 'plan-eng-review', 'SKILL.md'), 'utf-8'); + expect(content).toContain('Terse mode (build-time)'); + expect(content).not.toContain('Curated jargon list lives'); + expect(content).not.toContain('## Completeness Principle — Boil the Ocean'); + expect(content).not.toContain('## Confusion Protocol'); + expect(content).not.toContain('## Context Health (soft directive)'); + } finally { + fs.rmSync(stateDir, { recursive: true, force: true }); + fs.rmSync(outDir, { recursive: true, force: true }); + } + }); + test('slim voice section preserves the gstack voice contract', () => { const content = readSkillUnion('plan-eng-review'); // carved: review body moved to section const voice = extractMarkdownSection(content, '## Voice'); @@ -2478,6 +2511,20 @@ describe('setup script validation', () => { expect(setupContent).toContain('GSTACK_CONFIG'); }); + test('setup applies user-local skill rendering for terse mode without requiring gbrain', () => { + const regenSectionStart = setupContent.indexOf('# ─── User-local SKILL.md regen'); + const regenSectionEnd = setupContent.indexOf('# 11. Plan-tune cathedral hook install', regenSectionStart); + const regenSection = setupContent.slice(regenSectionStart, regenSectionEnd); + const gbrainElseStart = regenSection.indexOf('gbrain not detected'); + const userRenderIndex = regenSection.indexOf('gen:skill-docs:user --host claude'); + + expect(regenSection).toContain('get explain_level'); + expect(regenSection).toContain('explain_level=terse'); + expect(userRenderIndex).toBeGreaterThan(-1); + expect(gbrainElseStart).toBeGreaterThan(-1); + expect(userRenderIndex).toBeGreaterThan(gbrainElseStart); + }); + test('setup supports --prefix flag', () => { expect(setupContent).toContain('--prefix)'); expect(setupContent).toContain('SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1');