mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-12 23:43:05 +08:00
Hard rename, no dual-read alias (codex Finding D4). The on-disk migration script (Phase C, separate commit) renames the config key in users' ~/.gstack/config.yaml and any CLAUDE.md blocks. Touched call sites: - bin/gstack-config defaults + validation + list/defaults output - bin/gstack-gbrain-detect (gstack_brain_sync_mode field still emitted with the same name for downstream-tool compat; reads new key) - bin/gstack-brain-sync, bin/gstack-brain-enqueue, bin/gstack-brain-uninstall - bin/gstack-timeline-log (comment ref) - scripts/resolvers/preamble/generate-brain-sync-block.ts: renames key, branches on gbrain_mcp_mode=remote-http to emit "ARTIFACTS_SYNC: remote-mode (managed by brain server <host>)" instead of the local mode/queue/last_push line (codex Finding #11) - bin/gstack-brain-restore + bin/gstack-gbrain-source-wireup: read ~/.gstack-artifacts-remote.txt with ~/.gstack-brain-remote.txt fallback during the migration window - bin/gstack-artifacts-init: tolerant of unrecognized URL forms (local paths, file://, self-hosted gitea) so test infrastructure and unusual remotes work without canonicalization - test/brain-sync.test.ts: gstack-brain-init → gstack-artifacts-init - test/skill-e2e-brain-privacy-gate.test.ts: artifacts_sync_mode keys - test/gen-skill-docs.test.ts: budget 35K → 36.5K for the new MCP-mode probe in the preamble resolver - health/SKILL.md.tmpl, sync-gbrain/SKILL.md.tmpl: comment + verdict line Hard delete: - bin/gstack-brain-init (replaced by bin/gstack-artifacts-init in v1.27.0.0) - test/gstack-brain-init-gh-mock.test.ts (replaced by gstack-artifacts-init.test.ts) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
161 lines
7.6 KiB
TypeScript
161 lines
7.6 KiB
TypeScript
/**
|
|
* artifacts-sync preamble block (renamed from gbrain-sync in v1.27.0.0).
|
|
*
|
|
* Emits bash that runs at every skill invocation:
|
|
* 0. Live gbrain-availability hint (per /plan-eng-review): when gbrain is
|
|
* configured, emit one of two variants (steady-state vs empty-corpus
|
|
* emergency). Zero context cost when gbrain is not configured.
|
|
* 1. If ~/.gstack-artifacts-remote.txt (or legacy ~/.gstack-brain-remote.txt
|
|
* during the v1.27.0.0 migration window) 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). The
|
|
* script keeps its old name; only the config-key + state-file names flip.
|
|
* 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 an `ARTIFACTS_SYNC:` status line so every skill surfaces health.
|
|
* In remote-MCP mode, the line reads `ARTIFACTS_SYNC: remote-mode
|
|
* (managed by brain server <host>)` since this machine doesn't sync
|
|
* anything locally — the brain admin's server pulls from GitHub/GitLab.
|
|
*
|
|
* Also emits prose instructions for the host LLM to fire a one-time privacy
|
|
* stop-gate via AskUserQuestion when artifacts_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 `## Artifacts Sync (skill start)
|
|
|
|
\`\`\`bash
|
|
_GSTACK_HOME="\${GSTACK_HOME:-$HOME/.gstack}"
|
|
# Prefer the v1.27.0.0 artifacts file; fall back to brain file for users
|
|
# upgrading mid-stream before the migration script runs.
|
|
if [ -f "$HOME/.gstack-artifacts-remote.txt" ]; then
|
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-artifacts-remote.txt"
|
|
else
|
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
|
fi
|
|
_BRAIN_SYNC_BIN="${ctx.paths.binDir}/gstack-brain-sync"
|
|
_BRAIN_CONFIG_BIN="${ctx.paths.binDir}/gstack-config"
|
|
|
|
# /sync-gbrain context-load: teach the agent to use gbrain when it's available.
|
|
# Mutually exclusive variants per /plan-eng-review §4. Empty string when gbrain
|
|
# is not configured (zero context cost for non-gbrain users).
|
|
_GBRAIN_CONFIG="$HOME/.gbrain/config.json"
|
|
if [ -f "$_GBRAIN_CONFIG" ] && command -v gbrain >/dev/null 2>&1; then
|
|
_GBRAIN_VERSION_OK=$(gbrain --version 2>/dev/null | grep -c '^gbrain ' || echo 0)
|
|
if [ "$_GBRAIN_VERSION_OK" -gt 0 ] 2>/dev/null; then
|
|
_SYNC_STATE="$_GSTACK_HOME/.gbrain-sync-state.json"
|
|
_CWD_PAGES=0
|
|
if [ -f "$_SYNC_STATE" ]; then
|
|
# Flatten newlines so the regex works against pretty-printed JSON too.
|
|
_CWD_PAGES=$(tr -d '\\n' < "$_SYNC_STATE" 2>/dev/null \\
|
|
| grep -o '"name": *"code"[^}]*"detail": *{[^}]*"page_count": *[0-9]*' \\
|
|
| grep -o '"page_count": *[0-9]*' | grep -o '[0-9]\\+' | head -1)
|
|
_CWD_PAGES=\${_CWD_PAGES:-0}
|
|
fi
|
|
if [ "$_CWD_PAGES" -gt 0 ] 2>/dev/null; then
|
|
echo "GBrain configured. Prefer \\\`gbrain search\\\`/\\\`gbrain query\\\` over Grep for"
|
|
echo "semantic questions; use \\\`gbrain code-def\\\`/\\\`code-refs\\\`/\\\`code-callers\\\` for"
|
|
echo "symbol-aware code lookup. See \\"## GBrain Search Guidance\\" in CLAUDE.md."
|
|
echo "Run /sync-gbrain to refresh."
|
|
else
|
|
echo "GBrain configured but this repo isn't indexed yet. Run \\\`/sync-gbrain --full\\\`"
|
|
echo "before relying on \\\`gbrain search\\\` for code questions in this repo."
|
|
echo "Falls back to Grep until indexed."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get artifacts_sync_mode 2>/dev/null || echo off)
|
|
|
|
# Detect remote-MCP mode (Path 4 of /setup-gbrain). Local artifacts sync is
|
|
# a no-op in remote mode; the brain server pulls from GitHub/GitLab on its
|
|
# own cadence. Read claude.json directly to keep this preamble fast (no
|
|
# subprocess to claude CLI on every skill start).
|
|
_GBRAIN_MCP_MODE="none"
|
|
if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.claude.json" ]; then
|
|
_GBRAIN_MCP_TYPE=$(jq -r '.mcpServers.gbrain.type // .mcpServers.gbrain.transport // empty' "$HOME/.claude.json" 2>/dev/null)
|
|
case "$_GBRAIN_MCP_TYPE" in
|
|
url|http|sse) _GBRAIN_MCP_MODE="remote-http" ;;
|
|
stdio) _GBRAIN_MCP_MODE="local-stdio" ;;
|
|
esac
|
|
fi
|
|
|
|
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 "ARTIFACTS_SYNC: artifacts repo detected: $_BRAIN_NEW_URL"
|
|
echo "ARTIFACTS_SYNC: run 'gstack-brain-restore' to pull your cross-machine artifacts (or 'gstack-config set artifacts_sync_mode off' to dismiss forever)"
|
|
fi
|
|
fi
|
|
|
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
|
_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
|
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
|
fi
|
|
|
|
if [ "$_GBRAIN_MCP_MODE" = "remote-http" ]; then
|
|
# Remote-MCP mode: local artifacts sync is a no-op (brain admin's server
|
|
# pulls from GitHub/GitLab). Show the user this is by design, not broken.
|
|
_GBRAIN_HOST=$(jq -r '.mcpServers.gbrain.url // empty' "$HOME/.claude.json" 2>/dev/null | sed -E 's|^https?://([^/:]+).*|\\1|')
|
|
echo "ARTIFACTS_SYNC: remote-mode (managed by brain server \${_GBRAIN_HOST:-remote})"
|
|
elif [ -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 "ARTIFACTS_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
|
else
|
|
echo "ARTIFACTS_SYNC: off"
|
|
fi
|
|
\`\`\`
|
|
|
|
${isBrainHost ? `If output shows \`ARTIFACTS_SYNC: artifacts repo detected\`, offer \`gstack-brain-restore\` via AskUserQuestion; otherwise continue.` : ''}
|
|
|
|
Privacy stop-gate: if output shows \`ARTIFACTS_SYNC: off\`, \`artifacts_sync_mode_prompted\` is \`false\`, and gbrain is on PATH or \`gbrain doctor --fast --json\` works, ask once:
|
|
|
|
> gstack can publish your artifacts (CEO plans, designs, reports) to a private GitHub repo that GBrain indexes across machines. How much should sync?
|
|
|
|
Options:
|
|
- A) Everything allowlisted (recommended)
|
|
- B) Only artifacts
|
|
- C) Decline, keep everything local
|
|
|
|
After answer:
|
|
|
|
\`\`\`bash
|
|
# Chosen mode: full | artifacts-only | off
|
|
"$_BRAIN_CONFIG_BIN" set artifacts_sync_mode <choice>
|
|
"$_BRAIN_CONFIG_BIN" set artifacts_sync_mode_prompted true
|
|
\`\`\`
|
|
|
|
If A/B and \`~/.gstack/.git\` is missing, ask whether to run \`gstack-artifacts-init\`. Do not block the skill.
|
|
|
|
At skill END before telemetry:
|
|
|
|
\`\`\`bash
|
|
"${ctx.paths.binDir}/gstack-brain-sync" --discover-new 2>/dev/null || true
|
|
"${ctx.paths.binDir}/gstack-brain-sync" --once 2>/dev/null || true
|
|
\`\`\`
|
|
`;
|
|
}
|