mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
* fix(land-and-deploy): never merge over red, pending or missing CI (#2995) /land-and-deploy gated only on required checks, so a repo with none (every GitHub Free private repo, any repo without branch protection) skipped the CI wait and could merge over red CI. The verdict now comes from bin/gstack-ci-gate, a tested helper bound to the head SHA: all checks by default, cancel/unknown buckets fail, NO_CHECKS needs explicit per-head approval, and both merge commands (including --auto) run only after a PASS for the same SHA they pin with --match-head-commit. Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com> * fix(slug): give nested-group projects their own state bucket and verify checkpoint identity (#3003, #2901) gstack-slug kept only the last two remote path segments, so customer-a/product/repo and customer-b/product/repo shared one bucket and /context-restore could present another project's checkpoint. - One canonical-remote rule (bash + TS, golden vectors) used by all six resolvers: lowercased host, no userinfo/port/.git, Azure DevOps and Bitbucket Server normalized. 3+ segment remotes get <last-two>-<16 hex>; two-segment remotes (all of GitHub) are unchanged. - Versioned sticky cache (v2:) keeps cache hits git-free. - context-save stamps the credential-free remote and root; context-restore only treats this project's checkpoints as latest, reports foreign/unverified ones first, and treats a different root with the same remote (Conductor handoff) as information. - gstack-slug --adopt-legacy copies only files the user confirms. - question-preference-hook reads the same versioned cache, so never-ask preferences fire; unknown identity asks instead of auto-deciding. * fix(gbrain): brain context loads, managed sync works, memory sync stops duplicating (#2883, #2985, #2922) - brain-context-load called 'gbrain list_pages' (an MCP tool name), so every skill's brain context was 0 bytes. It now uses 'gbrain list' with gbrain's real filter flags, and prints one line when queries fail. - The code-stage sync always passes --no-pull (managed gbrain >= 0.51 refuses without it); a gbrain that rejects the flag fails with an upgrade message and is never retried without it. stderr is streamed with a bounded tail and quoted in failure summaries. - /sync-gbrain gains --sources (and GSTACK_MEMORY_INGEST_SOURCES); by default it skips types already synced through a federated curated source, so curated pages stop being re-duplicated. Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com> Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com> * fix(codex,cso): clean Codex reviews stop failing; semgrep and trivy SARIF results import (#2769, #3011) - Codex review gate is PASS / FAIL / UNVERIFIED. Untagged output (what a clean 'codex review --base' prints) is UNVERIFIED, not FAIL, and never a false PASS; P2/P3-only output passes. - import-sarif resolves semgrep's undeclared %SRCROOT% and trivy's ROOTPATH (including git-URL targets) through explicit, captured mappings; declared bases must sit inside the source root or the run's checkout, URIs are percent-decoded, and rejected results are counted with rule, base and path instead of silently dropped. * fix: version strings are not IPs, failed update checks are not 'up to date', private security reporting (#2856, #2786, #2984) - The pre-push redact hook exempts a root VERSION file that is only a version string and the version token in CHANGELOG release headings; the engine allowlist is untouched. - gstack-update-check caches an unreadable remote as CHECK_FAILED with a short TTL instead of UP_TO_DATE; /gstack-upgrade never reports 'latest' after a failed check. - SECURITY.md points reporters at GitHub private vulnerability reporting. - pr-title-sync (pull_request_target, write token) pins actions/checkout to a reviewed SHA. * fix(autoplan): publication guard accepts real Claude Code journals (#3007, #3009, #2968, #2977, #2986, #3006) The guard required a session journal to start with a user message, but Claude Code starts it with SessionStart hook attachments (and a forked compacted session starts with a compact_boundary), so every phase was denied for anyone with a SessionStart hook. Windows path spellings (C:/, /c/, drive case) were rejected too. - Reader accepts the captured startup, compact, clear and fork shapes (claude 2.1.284 fixture), computes root uniqueness over every candidate, and returns typed reason codes instead of one string. - Hard denials (competing root, foreign cwd, sidechain, agent, cycle, malformed, identity) say what happened and how to run the reviews by hand; unflushed journals still retry. - A well-formed, stable journal whose root shape is unrecognized now warns ('phase publication was NOT verified') and continues, without overriding Claude Code's own permission check; no bypass switch. - Windows paths fold before validation, so '..' is still rejected. - Periodic drift canary runs the reader on fresh journals from the pinned claude. Co-authored-by: Charles M Berg <77517285+CharlesBerg85@users.noreply.github.com> Co-authored-by: laurenbdaniels-coder <264343335+laurenbdaniels-coder@users.noreply.github.com> Co-authored-by: Jordan Mayer <176445995+maydaycyber@users.noreply.github.com> Co-authored-by: elmesery-dev <elmesery.dev@gmail.com> * v1.91.13.0: severe bug fix wave release notes, stale gbrain verbs in docs * test: seed the versioned slug cache in the memory-injection deny case (#2901) The preference hook now resolves the project bucket from gstack-slug's v2 slug-cache entry instead of basename(cwd); this fixture still wrote preferences under the basename bucket without the entry the skill preamble always writes first. * fix: full-suite follow-ups for the fix wave - gstack-slug's --help heredoc is over 512B, so it gets the BASH_COMPAT=50 pipe-deadlock guard like the other bins. - Refresh the codex/factory ship goldens for the plan-gates slug change. - The periodic lane gains the autoplan journal drift canary (107 paid files); raise the eval-slices cap to 25 so every slice still starts at once until its recorded duration lets the planner pack it. * fix(plan-eng-review): clearer Step 0 sequencing, write routes and section outputs The workflow judge scored plan-eng-review's instructions at actionability 3.67 (bar 4) on text unchanged from main. Resolve the ambiguities it named together, without changing any gate or output: Step 0 names the Read's order and keeps one STOP; the pending-call, read-only, forbidden-write and failed-save rules become one Write routes block; header/label limits and grid checks are concrete; Performance gets a checklist; each section has a stated output shape; Lake Score is defined. Same total length (parity cap). --------- Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com> Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com> Co-authored-by: Charles M Berg <77517285+CharlesBerg85@users.noreply.github.com> Co-authored-by: laurenbdaniels-coder <264343335+laurenbdaniels-coder@users.noreply.github.com> Co-authored-by: Jordan Mayer <176445995+maydaycyber@users.noreply.github.com> Co-authored-by: elmesery-dev <elmesery.dev@gmail.com>
274 lines
13 KiB
TypeScript
274 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' },
|
|
'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 "<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.`;
|
|
|
|
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 <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, save it via
|
|
\`gbrain put "<slug>" --content "<frontmatter + markdown>"\`. 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}/<feature-slug>" --content "$(cat <<'EOF'
|
|
---
|
|
title: "${meta.title}: <feature name>"
|
|
tags: [${meta.tag}, <feature-slug>]
|
|
---
|
|
<skill output in markdown>
|
|
EOF
|
|
)"
|
|
\`\`\`
|
|
|
|
Read the saved page back before claiming persistence. 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<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
|
|
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@<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
|
|
eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)" 2>/dev/null || true
|
|
${invalidateBash || ' # (no per-skill invalidation targets configured)'}
|
|
\`\`\``;
|
|
}
|