/** * GBrain resolver — brain-first lookup and save-to-brain for thinking skills. * * GBrain is a "mod" for gstack. When installed, coding skills become brain-aware: * they search the brain for context before starting and save results after finishing. * * These resolvers are suppressed on hosts that don't support brain features * (via suppressedResolvers in each host config). For those hosts, * {{GBRAIN_CONTEXT_LOAD}}, {{GBRAIN_SAVE_RESULTS}}, {{BRAIN_PREFLIGHT}}, * {{BRAIN_CACHE_REFRESH}}, and {{BRAIN_WRITE_BACK}} all resolve to empty string. * * Compatible with GBrain >= v0.10.0 (search CLI, doctor --fast --json, entity enrichment). * * Brain-aware planning (T4 / v1.48 plan): adds three new resolvers powered by * the bin/gstack-brain-cache CLI and scripts/brain-cache-spec.ts. The new * resolvers fire only for the 5 planning skills registered in * SKILL_DIGEST_SUBSETS (office-hours, plan-ceo-review, plan-eng-review, * plan-design-review, plan-devex-review). */ import type { TemplateContext } from './types'; import { SKILL_DIGEST_SUBSETS, SKILL_CALIBRATION_WEIGHTS, BRAIN_CACHE_ENTITIES, getSkillSubset, getInvalidationTargets, } from '../brain-cache-spec'; // Per-skill slug + title + tag metadata for SAVE_RESULTS. The full save // template (heredoc body, entity-stub instructions, throttle handling, // backlinks) lives in docs/gbrain-write-surfaces.md §Save Template and is // read on-demand by the agent. Compressing the inline prose keeps the // token footprint at ~150 tokens per skill (down from ~500), so users with // gbrain installed pay a small overhead and users without it (whose hosts // have GBRAIN_SAVE_RESULTS suppressed at gen-time) pay nothing. interface SkillSaveMeta { slugPrefix: string; title: string; tag: string; } const skillSaveMap: Record = { 'office-hours': { slugPrefix: 'office-hours', title: 'Office Hours', tag: 'design-doc' }, 'investigate': { slugPrefix: 'investigations', title: 'Investigation', tag: 'investigation' }, 'plan-ceo-review': { slugPrefix: 'ceo-plans', title: 'CEO Plan', tag: 'ceo-plan' }, 'plan-eng-review': { slugPrefix: 'eng-reviews', title: 'Eng Review', tag: 'eng-review' }, 'plan-design-review': { slugPrefix: 'design-reviews', title: 'Design Review', tag: 'design-review' }, 'plan-devex-review': { slugPrefix: 'devex-reviews', title: 'Devex Review', tag: 'devex-review' }, 'retro': { slugPrefix: 'retros', title: 'Retro', tag: 'retro' }, 'ship': { slugPrefix: 'releases', title: 'Release', tag: 'release' }, 'cso': { slugPrefix: 'security-audits', title: 'Security Audit', tag: 'security-audit' }, 'design-consultation': { slugPrefix: 'design-systems', title: 'Design System', tag: 'design-system' }, }; export function generateGBrainContextLoad(ctx: TemplateContext): string { let base = `## Brain Context Load **Skip this entire section if \`gbrain\` is not on PATH.** Extract 2-4 keywords from the user's request. Search the brain: \`gbrain search ""\`. Read the top 3 results with \`gbrain get_page ""\`. Use that context to inform your analysis. If \`gbrain search\` returns no results or any non-zero exit, proceed without brain context. Full search/read protocol + examples: see \`docs/gbrain-write-surfaces.md\` §Context Load.`; if (ctx.skillName === 'investigate') { base += `\n\nFor structured-data extraction requests ("track this", "extract from emails", "build a tracker"), route to GBrain's data-research skill instead: \`gbrain call data-research\`.`; } return base; } export function generateGBrainSaveResults(ctx: TemplateContext): string { // gbrain v0.18+ uses `gbrain put ` (NOT the deprecated `put_page` // MCP op). Compressed in v1.50.0.0: the inline heredoc + entity-stub + // throttle + backlink prose moved to docs/gbrain-write-surfaces.md // §Save Template, which the agent reads on demand when it actually // saves. The compact pointer keeps non-gbrain users' token overhead // near zero when their host's static suppression is overridden by // detection. const meta = skillSaveMap[ctx.skillName]; if (!meta) { return `## Save Results to Brain **Skip this entire section if \`gbrain\` is not on PATH.** If the skill output is worth preserving, save it via \`gbrain put "" --content ""\`. Full template (heredoc body, frontmatter shape, entity-stub instructions, throttle handling): see \`docs/gbrain-write-surfaces.md\` §Save Template.`; } return `## Save Results to Brain **Skip this entire section if \`gbrain\` is not on PATH.** After completing this skill, save the output: \`\`\`bash gbrain put "${meta.slugPrefix}/" --content "$(cat <<'EOF' --- title: "${meta.title}: " tags: [${meta.tag}, ] --- EOF )" \`\`\` Then extract person/org entities and create stub pages for each one. Throttle errors (exit 1 with "throttle"/"rate limit"/"busy") and any other non-zero exit are transient — don't retry inline. Full entity-stub template, throttle handling, and backlink protocol: see \`docs/gbrain-write-surfaces.md\` §Save Template.`; } // ──────────────────────────────────────────────────────────────────── // Brain-aware planning resolvers (T4 / v1.48 plan) // ──────────────────────────────────────────────────────────────────── /** * Returns true when this skill is registered for brain preflight. Skills not * in SKILL_DIGEST_SUBSETS get an empty BRAIN_PREFLIGHT block (no behavior). */ function isPreflightSkill(skillName: string): boolean { return Object.prototype.hasOwnProperty.call(SKILL_DIGEST_SUBSETS, skillName); } /** * Renders the per-skill BRAIN_PREFLIGHT block. The rendered output is a single * bash script that: * 1. Reads each digest file from gstack-brain-cache get (one call per digest) * 2. Falls back to "(brain context unavailable)" on missing * 3. Concatenates outputs into a single ## Brain Context block injected * into the skill's prompt context * 4. Tells the agent: "use this context to skip already-known questions" * * The cache CLI handles cold-refresh + lock dedup + stale-but-usable * fallback internally. From the resolver's perspective the call is one * shell command per digest. */ export function generateBrainPreflight(ctx: TemplateContext): string { if (!isPreflightSkill(ctx.skillName)) return ''; const subset = getSkillSubset(ctx.skillName); const binDir = ctx.paths.binDir; // Build the bash that loads each digest. Per-skill subset is small (2-5 entries). const loadLines = subset.map((entityName) => { const entity = BRAIN_CACHE_ENTITIES[entityName]; if (!entity) return ''; const projectFlag = entity.scope === 'per-project' ? '--project "$SLUG"' : ''; return ` printf '\\n### %s\\n\\n' "${entityName}"\n ${binDir}/gstack-brain-cache get ${entityName} ${projectFlag} 2>/dev/null || printf '_(no ${entityName} digest available yet)_\\n'`; }).join('\n'); const usageByEntity: Record = { product: 'If `product` digest names the value prop, target user, or stage, do not re-ask.', goals: 'If `goals` digest lists active goals, frame recommendations against them.', 'recent-decisions': 'If `recent-decisions` digest names a prior scope/architecture choice, flag if this plan contradicts.', 'user-profile': 'If `user-profile` digest carries calibration pattern statements ("tends to over-engineer security"), surface them when relevant.', 'developer-persona': 'If `developer-persona` digest describes the builder workflow or friction tolerance, adapt the DX recommendations.', brand: 'If `brand` digest names visual principles or constraints, use them before asking about design taste.', 'competitive-intel': 'If `competitive-intel` digest names peer products or workflow expectations, use them as comparison context.', salience: 'If `salience` digest surfaces recent local context, treat it as a pointer to verify rather than a standalone fact.', }; const usageLines = subset .map((entityName) => usageByEntity[entityName]) .filter(Boolean) .map((line) => `- ${line}`) .join('\n'); return `## Brain Context (preflight) ${ctx.skillName === 'plan-eng-review' ? 'After the Scope gate, before later review questions, load the brain\'s structured context' : 'Before asking any clarifying questions, load the brain\'s structured context'} for this project. The cache layer handles staleness, refresh, and stale-but- usable fallback automatically. Skip questions whose answers are already present in the loaded context; ground recommendations in what the brain prints for this skill. \`\`\`bash eval "$(${binDir}/gstack-slug 2>/dev/null)" 2>/dev/null || true { printf '## Brain Context\\n\\n' ${loadLines} } > /tmp/.gstack-brain-context-$$.md 2>/dev/null [ -s /tmp/.gstack-brain-context-$$.md ] && cat /tmp/.gstack-brain-context-$$.md rm -f /tmp/.gstack-brain-context-$$.md 2>/dev/null || true \`\`\` **How to use this context:** ${usageLines} - If a digest is \`(no X digest available yet)\`, treat that section as cold; ask the user. **Privacy:** Salience digest is filtered by allowlist (D9 default: \`projects/\`, \`gstack/\`, \`concepts/\` only). Personal/family/therapy content never leaks here. `; } /** * Renders the at-skill-end background refresh hook. Fires after the skill's * own work completes (telemetry has already logged); kicks any digest whose * age exceeds half its TTL but hasn't yet expired, so the NEXT invocation * gets a fresh cache without paying the cold-miss tax. * * Subordinate to {{TELEMETRY}} — runs after. Doesn't block the user. */ export function generateBrainCacheRefresh(ctx: TemplateContext): string { if (!isPreflightSkill(ctx.skillName)) return ''; const binDir = ctx.paths.binDir; return `## Brain Cache Background Refresh ${ctx.skillName === 'plan-ceo-review' ? `After the exit gate passes, start this nonblocking refresh before telemetry. Then return to the finalization instructions below; the user need not wait for the refresh process.` : `After the skill's work completes (and telemetry has logged), kick a background refresh of any cache digest that's getting close to its TTL. This is non-blocking — the user doesn't wait. Next invocation benefits from the warm cache.`} \`\`\`bash eval "$(${binDir}/gstack-slug 2>/dev/null)" 2>/dev/null || true (${binDir}/gstack-brain-cache refresh --project "$SLUG" 2>/dev/null &) || true \`\`\` `; } /** * Renders the calibration write-back block. ONLY emits when the skill makes * typed decisions worth a kind=bet take AND the brain trust policy is * personal. Phase 2 / E5 cross-skill calibration. * * Gated behind BRAIN_CALIBRATION_WRITEBACK feature flag in the resolver * output — the flag stays false until upstream gbrain ships takes_add MCP * op (T8). When the flag flips, the existing skill templates pick up the * write-back behavior without any template changes. */ export function generateBrainWriteBack(ctx: TemplateContext): string { if (!isPreflightSkill(ctx.skillName)) return ''; const weight = SKILL_CALIBRATION_WEIGHTS[ctx.skillName]; if (weight == null) return ''; // List the cache digests this skill's writes should invalidate. Multiple // skills write to multiple entities; the invalidation map captures this. const invalidatesEntities = getInvalidationTargets(`/${ctx.skillName}`); const invalidateBash = invalidatesEntities .map((e) => ` ${ctx.paths.binDir}/gstack-brain-cache invalidate ${e} --project "$SLUG" 2>/dev/null || true`) .join('\n'); return `## Brain Calibration Write-Back (gated) ${ctx.skillName === 'plan-eng-review' ? '`BRAIN_CALIBRATION_WRITEBACK` is a reserved default-off gate; this runtime does not set it. Skip this section and continue the finish sequence. Do not enable it or infer permission from brain availability. The contract below is retained for future gated integration, not an instruction to write now.\n\n' : ''}Skip unless \`BRAIN_CALIBRATION_WRITEBACK\` is set and the preamble/brain-health output or gstack config shows \`brain_trust_policy@=personal\`. If unknown, skip. If both gates pass, record one durable typed prediction with \`mcp__gbrain__takes_add\`; if unavailable, use \`mcp__gbrain__put_page\` with a gstack:takes fence block. Take frontmatter: \`\`\`yaml kind: bet holder: claim: weight: ${weight} since_date: expected_resolution: source_skill: ${ctx.skillName} \`\`\` After write, invalidate affected digests: \`\`\`bash eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)" 2>/dev/null || true ${invalidateBash || ' # (no per-skill invalidation targets configured)'} \`\`\``; }