diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 71aa1a34c..63d8100f6 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -128,8 +128,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 275236cd3..ad0257791 100755 --- a/setup +++ b/setup @@ -1277,24 +1277,26 @@ 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" @@ -1306,19 +1308,10 @@ if [ -x "$DETECT_BIN" ]; then # 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)." + if [ -n "$USER_LOCAL_RENDER_REASON" ]; then + USER_LOCAL_RENDER_REASON="$USER_LOCAL_RENDER_REASON + brain-aware blocks" 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" + 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)." @@ -1330,6 +1323,22 @@ if [ -x "$DETECT_BIN" ]; then 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 2fb783ffd..4136bc521 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -393,6 +393,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'); @@ -2507,6 +2540,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');