diff --git a/scripts/resolvers/preamble.ts b/scripts/resolvers/preamble.ts index dd490202..533864fc 100644 --- a/scripts/resolvers/preamble.ts +++ b/scripts/resolvers/preamble.ts @@ -37,6 +37,9 @@ import { generateWritingStyleMigration } from './preamble/generate-writing-style // Host-specific instructions import { generateBrainHealthInstruction } from './preamble/generate-brain-health-instruction'; +// GBrain cross-machine sync (runs at skill start; end-side handled in completion-status) +import { generateBrainSyncBlock } from './preamble/generate-brain-sync-block'; + // Behavioral / voice import { generateVoiceDirective } from './preamble/generate-voice-directive'; @@ -84,6 +87,7 @@ export function generatePreamble(ctx: TemplateContext): string { generateVendoringDeprecation(ctx), generateSpawnedSessionCheck(), generateBrainHealthInstruction(ctx), + generateBrainSyncBlock(ctx), generateModelOverlay(ctx), generateVoiceDirective(tier), ...(tier >= 2 ? [ diff --git a/scripts/resolvers/preamble/generate-brain-sync-block.ts b/scripts/resolvers/preamble/generate-brain-sync-block.ts new file mode 100644 index 00000000..6a378e58 --- /dev/null +++ b/scripts/resolvers/preamble/generate-brain-sync-block.ts @@ -0,0 +1,124 @@ +/** + * gbrain-sync preamble block. + * + * Emits bash that runs at every skill invocation: + * 1. If ~/.gstack-brain-remote.txt exists AND ~/.gstack/.git is missing, + * surface a restore-available hint (does NOT auto-run restore). + * 2. If sync is on, run `gstack-brain-sync --once` (drain + push). + * 3. On first skill of the day (24h cache via .brain-last-pull): + * `git fetch` + ff-only merge (JSONL merge driver handles conflicts). + * 4. Emit a `BRAIN_SYNC:` status line so every skill surfaces health. + * + * Also emits prose instructions for the host LLM to fire a one-time privacy + * stop-gate via AskUserQuestion when gbrain_sync_mode is unset and gbrain + * is available on the host. + * + * Block emitted across all tiers. Internal bash short-circuits when feature + * is disabled; cost is <5ms. + * + * Skill-end sync is handled by the completion-status generator via a call + * to `gstack-brain-sync --discover-new` + `--once`. + */ +import type { TemplateContext } from '../types'; + +export function generateBrainSyncBlock(ctx: TemplateContext): string { + const isBrainHost = ctx.host === 'gbrain' || ctx.host === 'hermes'; + return `## GBrain Sync (skill start) + +\`\`\`bash +# gbrain-sync: drain pending writes, pull once per day. Silent no-op when +# the feature isn't initialized or gbrain_sync_mode is "off". See +# docs/gbrain-sync.md. + +_GSTACK_HOME="\${GSTACK_HOME:-$HOME/.gstack}" +_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt" +_BRAIN_SYNC_BIN="${ctx.paths.binDir}/gstack-brain-sync" +_BRAIN_CONFIG_BIN="${ctx.paths.binDir}/gstack-config" + +_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off) + +# New-machine hint: URL file present, local .git missing, sync not yet enabled. +if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then + _BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]') + if [ -n "$_BRAIN_NEW_URL" ]; then + echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL" + echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)" + fi +fi + +# Active-sync path. +if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then + # Once-per-day pull. + _BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull" + _BRAIN_NOW=$(date +%s) + _BRAIN_DO_PULL=1 + if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then + _BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0) + _BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST )) + [ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0 + fi + if [ "$_BRAIN_DO_PULL" = "1" ]; then + ( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true + echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE" + fi + # Drain pending queue, push. + "$_BRAIN_SYNC_BIN" --once 2>/dev/null || true +fi + +# Status line — always emitted, easy to grep. +if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then + _BRAIN_QUEUE_DEPTH=0 + [ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ') + _BRAIN_LAST_PUSH="never" + [ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never) + echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH" +else + echo "BRAIN_SYNC: off" +fi +\`\`\` + +${isBrainHost ? `If the bash output shows \`BRAIN_SYNC: brain repo detected\`, the user copied their remote URL file to this machine but hasn't restored yet. Offer to run \`gstack-brain-restore\` via AskUserQuestion. If the user agrees, run the command; otherwise continue without sync.` : ''} + +**Privacy stop-gate (fires ONCE per machine).** + +If the bash output shows \`BRAIN_SYNC: off\` AND the config value +\`gbrain_sync_mode_prompted\` is \`false\` AND gbrain is detected on this host +(either \`gbrain doctor --fast --json\` succeeds or the \`gbrain\` binary is in PATH), +fire a one-time privacy gate via AskUserQuestion: + +> gstack can publish your session memory (learnings, plans, designs, retros) to a +> private GitHub repo that GBrain indexes across your machines. Higher tiers +> include behavioral data (session timelines, developer profile). How much do you +> want to sync? + +Options: +- A) Everything allowlisted (recommended — maximum cross-machine memory) +- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile +- C) Decline — keep everything local + +After the user answers, run (substituting the chosen value): + +\`\`\`bash +# Chosen mode: full | artifacts-only | off +"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode +"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true +\`\`\` + +If A or B was chosen AND \`~/.gstack/.git\` doesn't exist, ask a follow-up: +"Set up the GBrain sync repo now? (runs \`gstack-brain-init\`)" +- A) Yes, run it now +- B) Show me the command, I'll run it myself + +Do not block the skill. Emit the question, continue the skill workflow. The +next skill run picks up wherever this left off. + +**At skill END (before the telemetry block),** run these bash commands to +catch artifact writes (design docs, plans, retros) that skipped the writer +shims, plus drain any still-pending queue entries: + +\`\`\`bash +"${ctx.paths.binDir}/gstack-brain-sync" --discover-new 2>/dev/null || true +"${ctx.paths.binDir}/gstack-brain-sync" --once 2>/dev/null || true +\`\`\` +`; +}