mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-07 17:28:23 +01:00
* feat: retire /investigate after a no-gain eval (GST-1717)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(skill-start): core routing offer drops every non-core rule line (GST-1717)
The retired /investigate rule became 'Bugs/errors → fix them directly', which no
longer matches '→ invoke /', so proactive=core leaked it into the offered section.
Match any '- … → ' rule line.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* revert(review): keep review/checklist.md at its no-loss-pinned bytes (GST-1717)
The pin names replay GSTA-224 H1; a reviewer checklist edit merges only after a
passing no-loss replay (crew rule 15). The /investigate flake line stays until a
replayed repin.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: repair free-suite fallout from retiring /investigate (GST-1717)
CI run 42340956269 at 83a8a9a: drop fixtures, ledger rows, allowlist entries and
counts for the retired skill and its deleted paid files; record the harness
change as non-behavioral; the test-of-test detector accepts path.join(ROOT, 'bin');
uncovered-prompt fixtures use a tracked prompt, since an untracked path now
reads as a deleted one.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(relink): back up a retired skill's real SKILL.md before cleaning it (GST-1717)
With the skill's source gone, the customized-file check never ran, so a
banner-carrying local copy of investigate/SKILL.md was deleted instead of moved
to the backup root (red-team review, pass 1).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(qm): restore the review checklist's flake-line rewrite (GST-1717)
review/checklist.md keeps its pinned /investigate line, so the QM edition needs
its replaceText rule back; qm/ stays fresh against the generator.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(changelog): name the setup flag break, the relink backup and the pinned checklist line (GST-1717)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(setup,relink): back up a retired skill's real SKILL.md instead of deleting it (GST-1717)
Scoped fix round under grant a-360dd62bd46a (outside-voice blocking finding and
adversarial #1, #4). setup's prune compares a copied host entry with the render
before removing that render, and backs up a copy that differs or whose render is
gone; relink no longer takes the marker's rm -rf for a skill whose source is
gone, so a real SKILL.md is backed up and the user's files stay. setup reports a
failed remove-source of the retired gate hooks on stderr.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(pr-profile): a changed prompt counts as deleted only when it is gone from disk (GST-1717)
An untracked prompt file on disk still needs prompt validation (targeted review
finding); harness change recorded as non-behavioral.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(changelog): exact backup wording, the hook-removal warning and GST-1744 (GST-1717)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(setup): one backup folder per host for a retired skill's copies (GST-1717)
Under grant a-c95576b8b1f9 (fix-check#1): every copy host moved its
gstack-investigate/SKILL.md to the same backup path, so a later host's copy
overwrote an earlier host's edits. A sandboxed ./setup --host factory run showed
gen-skill-docs removes the retired render before the prune, so every real copy
is backed up; each host now gets its own folder.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(changelog): say exactly what setup backs up for a retired skill (GST-1717)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(changelog): end the two colon sentences prose-lint flagged (GST-1717)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(retired-skills): name plan-eng-review once behind a pregate-literal waiver (GST-1717)
pregate's literals check read the GSTACK_RETIRED_SKILLS change as an old
value still in the tests. plan-eng-review is still retired; investigate was
appended. The fixture names it once, with the waiver.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
263 lines
13 KiB
TypeScript
263 lines
13 KiB
TypeScript
/**
|
|
* 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<string, SkillSaveMeta> = {
|
|
'office-hours': { slugPrefix: 'office-hours', title: 'Office Hours', tag: 'design-doc' },
|
|
'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 {
|
|
const 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 "<keywords>"\`. Read the top 3 results with
|
|
\`gbrain get "<slug>"\`. 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.`;
|
|
|
|
return base;
|
|
}
|
|
|
|
export function generateGBrainSaveResults(ctx: TemplateContext): string {
|
|
// gbrain v0.18+ uses `gbrain put <slug>` (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, write the page (frontmatter + markdown)
|
|
into a private file with your file-write tool and run
|
|
\`gbrain put "<slug>" < "<page-file>"\`. Full template (frontmatter shape,
|
|
entity stubs, 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.**
|
|
|
|
Save the output: run \`mkdir -p .gstack/tmp && mktemp .gstack/tmp/page.XXXXXX\` at the
|
|
repo root, write the page into the printed file with your file-write tool
|
|
(frontmatter \`title: "${meta.title}: <feature name>"\`, \`tags: [${meta.tag}, <feature-slug>]\`,
|
|
then the output in markdown), then:
|
|
|
|
\`\`\`bash
|
|
gbrain put "${meta.slugPrefix}/<feature-slug>" < "<page-file>" && rm -f "<page-file>"
|
|
\`\`\`
|
|
|
|
Read the saved page back before claiming persistence. Then create stub pages for
|
|
its person/org entities. Any non-zero exit (throttle included) is transient: don't
|
|
retry inline. Entity stubs, throttling and backlinks: 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<string, string> = {
|
|
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
|
|
SLUG=$(${binDir}/gstack-slug --get SLUG 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
|
|
SLUG=$(${binDir}/gstack-slug --get SLUG 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@<endpoint-hash>=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: <user identity from whoami>
|
|
claim: <one-line prediction the skill is making>
|
|
weight: ${weight}
|
|
since_date: <today's date>
|
|
expected_resolution: <date in 1-3 months depending on skill>
|
|
source_skill: ${ctx.skillName}
|
|
\`\`\`
|
|
|
|
After write, invalidate affected digests:
|
|
|
|
\`\`\`bash
|
|
SLUG=$(${ctx.paths.binDir}/gstack-slug --get SLUG 2>/dev/null) || true
|
|
${invalidateBash || ' # (no per-skill invalidation targets configured)'}
|
|
\`\`\``;
|
|
}
|