Skill Tracer
Skills › gstack › gstack › gbrain.ts

gbrain.ts

267 lines · scripts/resolvers/gbrain.ts at 3f79d81

1/**
2 * GBrain resolver — brain-first lookup and save-to-brain for thinking skills.
3 *
4 * GBrain is a "mod" for gstack. When installed, coding skills become brain-aware:
5 * they search the brain for context before starting and save results after finishing.
6 *
7 * These resolvers are suppressed on hosts that don't support brain features
8 * (via suppressedResolvers in each host config). For those hosts,
9 * {{GBRAIN_CONTEXT_LOAD}}, {{GBRAIN_SAVE_RESULTS}}, {{BRAIN_PREFLIGHT}},
10 * {{BRAIN_CACHE_REFRESH}}, and {{BRAIN_WRITE_BACK}} all resolve to empty string.
11 *
12 * Compatible with GBrain >= v0.10.0 (search CLI, doctor --fast --json, entity enrichment).
13 *
14 * Brain-aware planning (T4 / v1.48 plan): adds three new resolvers powered by
15 * the bin/gstack-brain-cache CLI and scripts/brain-cache-spec.ts. The new
16 * resolvers fire only for the 5 planning skills registered in
17 * SKILL_DIGEST_SUBSETS (office-hours, plan-ceo-review, plan-eng-review,
18 * plan-design-review, plan-devex-review).
19 */
20import type { TemplateContext } from './types';
21import {
22 SKILL_DIGEST_SUBSETS,
23 SKILL_CALIBRATION_WEIGHTS,
24 BRAIN_CACHE_ENTITIES,
25 getSkillSubset,
26 getInvalidationTargets,
27} from '../brain-cache-spec';
29// Per-skill slug + title + tag metadata for SAVE_RESULTS. The full save
30// template (heredoc body, entity-stub instructions, throttle handling,
31// backlinks) lives in docs/gbrain-write-surfaces.md §Save Template and is
32// read on-demand by the agent. Compressing the inline prose keeps the
33// token footprint at ~150 tokens per skill (down from ~500), so users with
34// gbrain installed pay a small overhead and users without it (whose hosts
35// have GBRAIN_SAVE_RESULTS suppressed at gen-time) pay nothing.
36interface SkillSaveMeta {
37 slugPrefix: string;
38 title: string;
39 tag: string;
42const skillSaveMap: Record<string, SkillSaveMeta> = {
43 'office-hours': { slugPrefix: 'office-hours', title: 'Office Hours', tag: 'design-doc' },
44 'investigate': { slugPrefix: 'investigations', title: 'Investigation', tag: 'investigation' },
45 'plan-ceo-review': { slugPrefix: 'ceo-plans', title: 'CEO Plan', tag: 'ceo-plan' },
46 'plan-eng-review': { slugPrefix: 'eng-reviews', title: 'Eng Review', tag: 'eng-review' },
47 'plan-design-review': { slugPrefix: 'design-reviews', title: 'Design Review', tag: 'design-review' },
48 'plan-devex-review': { slugPrefix: 'devex-reviews', title: 'Devex Review', tag: 'devex-review' },
49 'retro': { slugPrefix: 'retros', title: 'Retro', tag: 'retro' },
50 'ship': { slugPrefix: 'releases', title: 'Release', tag: 'release' },
51 'cso': { slugPrefix: 'security-audits', title: 'Security Audit', tag: 'security-audit' },
52 'design-consultation': { slugPrefix: 'design-systems', title: 'Design System', tag: 'design-system' },
53};
55export function generateGBrainContextLoad(ctx: TemplateContext): string {
56 let base = `## Brain Context Load
58**Skip this entire section if \`gbrain\` is not on PATH.**
60Extract 2-4 keywords from the user's request. Search the brain:
61\`gbrain search "<keywords>"\`. Read the top 3 results with
62\`gbrain get "<slug>"\`. Use that context to inform your analysis.
64If \`gbrain search\` returns no results or any non-zero exit, proceed
65without brain context. Full search/read protocol + examples:
66see \`docs/gbrain-write-surfaces.md\` §Context Load.`;
68 if (ctx.skillName === 'investigate') {
69 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\`.`;
70 }
72 return base;
75export function generateGBrainSaveResults(ctx: TemplateContext): string {
76 // gbrain v0.18+ uses `gbrain put <slug>` (NOT the deprecated `put_page`
77 // MCP op). Compressed in v1.50.0.0: the inline heredoc + entity-stub +
78 // throttle + backlink prose moved to docs/gbrain-write-surfaces.md
79 // §Save Template, which the agent reads on demand when it actually
80 // saves. The compact pointer keeps non-gbrain users' token overhead
81 // near zero when their host's static suppression is overridden by
82 // detection.
83 const meta = skillSaveMap[ctx.skillName];
85 if (!meta) {
86 return `## Save Results to Brain
88**Skip this entire section if \`gbrain\` is not on PATH.**
90If the skill output is worth preserving, write the page (frontmatter + markdown)
91into a private file with your file-write tool and run
92\`gbrain put "<slug>" < "<page-file>"\`. Full template (frontmatter shape,
93entity stubs, throttle handling): see \`docs/gbrain-write-surfaces.md\` §Save Template.`;
94 }
96 return `## Save Results to Brain
98**Skip this entire section if \`gbrain\` is not on PATH.**
100Save the output: run \`mkdir -p .gstack/tmp && mktemp .gstack/tmp/page.XXXXXX\` at the
101repo root, write the page into the printed file with your file-write tool
102(frontmatter \`title: "${meta.title}: <feature name>"\`, \`tags: [${meta.tag}, <feature-slug>]\`,
103then the output in markdown), then:
105\`\`\`bash
106gbrain put "${meta.slugPrefix}/<feature-slug>" < "<page-file>" && rm -f "<page-file>"
107\`\`\`
109Read the saved page back before claiming persistence. Then create stub pages for
110its person/org entities. Any non-zero exit (throttle included) is transient: don't
111retry inline. Entity stubs, throttling and backlinks: see
112\`docs/gbrain-write-surfaces.md\` §Save Template.`;
115// ────────────────────────────────────────────────────────────────────
116// Brain-aware planning resolvers (T4 / v1.48 plan)
117// ────────────────────────────────────────────────────────────────────
119/**
120 * Returns true when this skill is registered for brain preflight. Skills not
121 * in SKILL_DIGEST_SUBSETS get an empty BRAIN_PREFLIGHT block (no behavior).
122 */
123function isPreflightSkill(skillName: string): boolean {
124 return Object.prototype.hasOwnProperty.call(SKILL_DIGEST_SUBSETS, skillName);
127/**
128 * Renders the per-skill BRAIN_PREFLIGHT block. The rendered output is a single
129 * bash script that:
130 * 1. Reads each digest file from gstack-brain-cache get (one call per digest)
131 * 2. Falls back to "(brain context unavailable)" on missing
132 * 3. Concatenates outputs into a single ## Brain Context block injected
133 * into the skill's prompt context
134 * 4. Tells the agent: "use this context to skip already-known questions"
135 *
136 * The cache CLI handles cold-refresh + lock dedup + stale-but-usable
137 * fallback internally. From the resolver's perspective the call is one
138 * shell command per digest.
139 */
140export function generateBrainPreflight(ctx: TemplateContext): string {
141 if (!isPreflightSkill(ctx.skillName)) return '';
142 const subset = getSkillSubset(ctx.skillName);
143 const binDir = ctx.paths.binDir;
144 // Build the bash that loads each digest. Per-skill subset is small (2-5 entries).
145 const loadLines = subset.map((entityName) => {
146 const entity = BRAIN_CACHE_ENTITIES[entityName];
147 if (!entity) return '';
148 const projectFlag = entity.scope === 'per-project' ? '--project "$SLUG"' : '';
149 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'`;
150 }).join('\n');
151 const usageByEntity: Record<string, string> = {
152 product: 'If `product` digest names the value prop, target user, or stage, do not re-ask.',
153 goals: 'If `goals` digest lists active goals, frame recommendations against them.',
154 'recent-decisions': 'If `recent-decisions` digest names a prior scope/architecture choice, flag if this plan contradicts.',
155 'user-profile': 'If `user-profile` digest carries calibration pattern statements ("tends to over-engineer security"), surface them when relevant.',
156 'developer-persona': 'If `developer-persona` digest describes the builder workflow or friction tolerance, adapt the DX recommendations.',
157 brand: 'If `brand` digest names visual principles or constraints, use them before asking about design taste.',
158 'competitive-intel': 'If `competitive-intel` digest names peer products or workflow expectations, use them as comparison context.',
159 salience: 'If `salience` digest surfaces recent local context, treat it as a pointer to verify rather than a standalone fact.',
160 };
161 const usageLines = subset
162 .map((entityName) => usageByEntity[entityName])
163 .filter(Boolean)
164 .map((line) => `- ${line}`)
165 .join('\n');
167 return `## Brain Context (preflight)
169${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'}
170for this project. The cache layer handles staleness, refresh, and stale-but-
171usable fallback automatically. Skip questions whose answers are already
172present in the loaded context; ground recommendations in what the brain
173prints for this skill.
175\`\`\`bash
176SLUG=$(${binDir}/gstack-slug --get SLUG 2>/dev/null) || true
178 printf '## Brain Context\\n\\n'
179${loadLines}
180} > /tmp/.gstack-brain-context-$$.md 2>/dev/null
181[ -s /tmp/.gstack-brain-context-$$.md ] && cat /tmp/.gstack-brain-context-$$.md
182rm -f /tmp/.gstack-brain-context-$$.md 2>/dev/null || true
183\`\`\`
185**How to use this context:**
186${usageLines}
187- If a digest is \`(no X digest available yet)\`, treat that section as cold; ask the user.
189**Privacy:** Salience digest is filtered by allowlist (D9 default: \`projects/\`,
190\`gstack/\`, \`concepts/\` only). Personal/family/therapy content never leaks here.
191`;
194/**
195 * Renders the at-skill-end background refresh hook. Fires after the skill's
196 * own work completes (telemetry has already logged); kicks any digest whose
197 * age exceeds half its TTL but hasn't yet expired, so the NEXT invocation
198 * gets a fresh cache without paying the cold-miss tax.
199 *
200 * Subordinate to {{TELEMETRY}} — runs after. Doesn't block the user.
201 */
202export function generateBrainCacheRefresh(ctx: TemplateContext): string {
203 if (!isPreflightSkill(ctx.skillName)) return '';
204 const binDir = ctx.paths.binDir;
205 return `## Brain Cache Background Refresh
207${ctx.skillName === 'plan-ceo-review' ? `After the exit gate passes, start this nonblocking refresh before telemetry.
208Then return to the finalization instructions below; the user need not wait for
209the refresh process.` : `After the skill's work completes (and telemetry has logged), kick a
210background refresh of any cache digest that's getting close to its TTL.
211This is non-blocking — the user doesn't wait. Next invocation benefits
212from the warm cache.`}
214\`\`\`bash
215SLUG=$(${binDir}/gstack-slug --get SLUG 2>/dev/null) || true
216(${binDir}/gstack-brain-cache refresh --project "$SLUG" 2>/dev/null &) || true
217\`\`\`
218`;
221/**
222 * Renders the calibration write-back block. ONLY emits when the skill makes
223 * typed decisions worth a kind=bet take AND the brain trust policy is
224 * personal. Phase 2 / E5 cross-skill calibration.
225 *
226 * Gated behind BRAIN_CALIBRATION_WRITEBACK feature flag in the resolver
227 * output — the flag stays false until upstream gbrain ships takes_add MCP
228 * op (T8). When the flag flips, the existing skill templates pick up the
229 * write-back behavior without any template changes.
230 */
231export function generateBrainWriteBack(ctx: TemplateContext): string {
232 if (!isPreflightSkill(ctx.skillName)) return '';
233 const weight = SKILL_CALIBRATION_WEIGHTS[ctx.skillName];
234 if (weight == null) return '';
235 // List the cache digests this skill's writes should invalidate. Multiple
236 // skills write to multiple entities; the invalidation map captures this.
237 const invalidatesEntities = getInvalidationTargets(`/${ctx.skillName}`);
238 const invalidateBash = invalidatesEntities
239 .map((e) => ` ${ctx.paths.binDir}/gstack-brain-cache invalidate ${e} --project "$SLUG" 2>/dev/null || true`)
240 .join('\n');
242 return `## Brain Calibration Write-Back (gated)
244${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
245output or gstack config shows \`brain_trust_policy@<endpoint-hash>=personal\`.
246If unknown, skip. If both gates pass, record one durable
247typed prediction with \`mcp__gbrain__takes_add\`; if unavailable, use
248\`mcp__gbrain__put_page\` with a gstack:takes fence block.
250Take frontmatter:
251\`\`\`yaml
252kind: bet
253holder: <user identity from whoami>
254claim: <one-line prediction the skill is making>
255weight: ${weight}
256since_date: <today's date>
257expected_resolution: <date in 1-3 months depending on skill>
258source_skill: ${ctx.skillName}
259\`\`\`
261After write, invalidate affected digests:
263\`\`\`bash
264SLUG=$(${ctx.paths.binDir}/gstack-slug --get SLUG 2>/dev/null) || true
265${invalidateBash || ' # (no per-skill invalidation targets configured)'}
266\`\`\``;