/** * 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'; export function generateGBrainContextLoad(ctx: TemplateContext): string { let base = `## Brain Context Load Before starting this skill, search your brain for relevant context: 1. Extract 2-4 keywords from the user's request (nouns, error names, file paths, technical terms). Search GBrain: \`gbrain search "keyword1 keyword2"\` Example: for "the login page is broken after deploy", search \`gbrain search "login broken deploy"\` Search returns lines like: \`[slug] Title (score: 0.85) - first line of content...\` 2. If few results, broaden to the single most specific keyword and search again. 3. For each result page, read it: \`gbrain get_page ""\` Read the top 3 pages for context. 4. Use this brain context to inform your analysis. If GBrain is not available or returns no results, proceed without brain context. Any non-zero exit code from gbrain commands should be treated as a transient failure.`; if (ctx.skillName === 'investigate') { base += `\n\nIf the user's request is about tracking, extracting, or researching structured data (e.g., "track this data", "extract from emails", "build a tracker"), route to GBrain's data-research skill instead: \`gbrain call data-research\`. This skill has a 7-phase pipeline optimized for structured data extraction.`; } return base; } export function generateGBrainSaveResults(ctx: TemplateContext): string { // gbrain v0.18+ renamed `put_page` → `put ` and moved --title/--tags // into YAML frontmatter inside --content. These templates render into // SKILL.md files as user-facing instructions; using the old subcommand // ships broken copy-paste to every gstack user. const skillSaveMap: Record = { 'office-hours': 'Save the design document as a brain page:\n```bash\ngbrain put "office-hours/" --content "$(cat <<\'EOF\'\n---\ntitle: "Office Hours: "\ntags: [design-doc, ]\n---\n\nEOF\n)"\n```', 'investigate': 'Save the root cause analysis as a brain page:\n```bash\ngbrain put "investigations/" --content "$(cat <<\'EOF\'\n---\ntitle: "Investigation: "\ntags: [investigation, ]\n---\n\nEOF\n)"\n```', 'plan-ceo-review': 'Save the CEO plan as a brain page:\n```bash\ngbrain put "ceo-plans/" --content "$(cat <<\'EOF\'\n---\ntitle: "CEO Plan: "\ntags: [ceo-plan, ]\n---\n\nEOF\n)"\n```', 'retro': 'Save the retrospective as a brain page:\n```bash\ngbrain put "retros/" --content "$(cat <<\'EOF\'\n---\ntitle: "Retro: "\ntags: [retro, ]\n---\n\nEOF\n)"\n```', 'plan-eng-review': 'Save the architecture decisions as a brain page:\n```bash\ngbrain put "eng-reviews/" --content "$(cat <<\'EOF\'\n---\ntitle: "Eng Review: "\ntags: [eng-review, ]\n---\n\nEOF\n)"\n```', 'ship': 'Save the release notes as a brain page:\n```bash\ngbrain put "releases/" --content "$(cat <<\'EOF\'\n---\ntitle: "Release: "\ntags: [release, ]\n---\n\nEOF\n)"\n```', 'cso': 'Save the security audit as a brain page:\n```bash\ngbrain put "security-audits/" --content "$(cat <<\'EOF\'\n---\ntitle: "Security Audit: "\ntags: [security-audit, ]\n---\n\nEOF\n)"\n```', 'design-consultation': 'Save the design system as a brain page:\n```bash\ngbrain put "design-systems/" --content "$(cat <<\'EOF\'\n---\ntitle: "Design System: "\ntags: [design-system, ]\n---\n\nEOF\n)"\n```', }; const saveInstruction = skillSaveMap[ctx.skillName] || 'Save the skill output as a brain page if the results are worth preserving:\n```bash\ngbrain put "" --content "$(cat <<\'EOF\'\n---\ntitle: ""\ntags: [, ]\n---\n\nEOF\n)"\n```'; return `## Save Results to Brain After completing this skill, persist the results to your brain for future reference: ${saveInstruction} After saving the page, extract and enrich mentioned entities: for each actual person name or company/organization name found in the output, \`gbrain search ""\` to check if a page exists. If not, create a stub page: \`\`\`bash gbrain put "entities/" --content "$(cat <<'EOF' --- title: "" tags: [entity, person] --- Stub page. Mentioned in output. EOF )" \`\`\` Only extract actual person names and company/organization names. Skip product names, section headings, technical terms, and file paths. Throttle errors appear as: exit code 1 with stderr containing "throttle", "rate limit", "capacity", or "busy". If GBrain returns a throttle or rate-limit error on any save operation, defer the save and move on. The brain is busy — the content is not lost, just not persisted this run. Any other non-zero exit code should also be treated as a transient failure. Add backlinks to related brain pages if they exist. If GBrain is not available, skip this step. After brain operations complete, note in your completion output: how many pages were found in the initial search, how many entities were enriched, and whether any operations were throttled. This helps the user see brain utilization over time.`; } // ──────────────────────────────────────────────────────────────────── // 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'); return `## Brain Context (preflight) 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 already knows about the user, the product, the goals, and recent decisions. \`\`\`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:** - If \`product\` digest names the value prop, target user, or stage — don't re-ask. - If \`goals\` digest lists active goals — frame recommendations against them. - If \`recent-decisions\` digest names a prior scope/architecture choice — flag if this plan contradicts. - If \`user-profile\` digest carries calibration pattern statements ("tends to over-engineer security") — surface them when relevant. - 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 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 (Phase 2 / gated) When the skill makes a typed prediction worth tracking (scope decision, TTHW target, architectural bet, wedge commitment), it MAY write a \`kind=bet\` take to the brain so a calibration profile builds over time. **Gated on two things:** 1. Brain trust policy for the active endpoint is \`personal\` (check via \`${ctx.paths.binDir}/gstack-config get brain_trust_policy@\`). Shared brains skip write-back to avoid polluting team calibration. 2. Feature flag \`BRAIN_CALIBRATION_WRITEBACK\` is set (today: false; flips to true when upstream gbrain v0.42+ ships \`takes_add\` MCP op). When both gates pass, the write-back path uses \`mcp__gbrain__takes_add\` to record a take with weight ${weight} (per SKILL_CALIBRATION_WEIGHTS). If the MCP op is unavailable, fall back to \`mcp__gbrain__put_page\` with a gstack:takes fence block (documented but uglier path). Mandatory take frontmatter shape: \`\`\`yaml kind: bet holder: claim: weight: ${weight} since_date: expected_resolution: source_skill: ${ctx.skillName} \`\`\` After write, invalidate the affected digests so the next preflight reflects the new state: \`\`\`bash eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)" 2>/dev/null || true ${invalidateBash || ' # (no per-skill invalidation targets configured)'} \`\`\` `; }