feat(gen): preamble resolvers emit a script invocation fence instead of inline bash

generate-preamble-bash: ~6.3KB fence -> 4-line gstack-skill-start invocation
(quoted-tilde pitfall handled: leading ~ interpolates through $HOME; env-var
hosts keep $GSTACK_BIN) + degraded-mode prose (F1/EOV8: safe defaults, consent
gates deferred-never-lost; OV5: proto rule). generate-brain-sync-block: ~6.8KB
bash -> interpretation prose + the privacy stop-gate (stays inline until
Phase 2's gated emission). generate-completion-status: telemetry fence -> one
gstack-skill-end call with SESSION_ID/TEL_START handoff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-25 15:36:35 +00:00
co-authored by Claude Fable 5
parent b1199afc03
commit e9d85060ca
3 changed files with 69 additions and 321 deletions
@@ -1,176 +1,32 @@
/** /**
* artifacts-sync preamble block (renamed from gbrain-sync in v1.27.0.0). * Artifacts-sync interpretation rules (token-reduction Phase 1).
* *
* Emits bash that runs at every skill invocation: * The ~6.8KB of artifacts-sync bash this generator used to inline (gbrain
* 0. Live gbrain-availability hint (per /plan-eng-review): when gbrain is * availability hints, remote-MCP detection via claude.json, restore hint,
* configured, emit one of two variants (steady-state vs empty-corpus * daily pull, queue-depth status) now runs inside `bin/gstack-skill-start`
* emergency). Zero context cost when gbrain is not configured. * (invoked by the Preamble fence above) and reports through the same STATUS
* 1. If ~/.gstack-artifacts-remote.txt (or legacy ~/.gstack-brain-remote.txt * lines it always emitted: the GBrain hint text and the `ARTIFACTS_SYNC:`
* during the v1.27.0.0 migration window) exists AND ~/.gstack/.git is * line. What remains here is the prose the model acts on — including the
* missing, surface a restore-available hint (does NOT auto-run restore). * one-time privacy stop-gate, which stays inline until the Phase 2
* 2. If sync is on, run `gstack-brain-sync --once` (drain + push). The * instruction-emission layer moves it behind its runtime gate.
* 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 * Skill-END sync is no longer a separate fence: `bin/gstack-skill-end`
* stop-gate via AskUserQuestion when artifacts_sync_mode is unset and gbrain * (invoked by the Telemetry step) drains the queue before logging.
* 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'; import type { TemplateContext } from '../types';
import { quoteSafePath } from '../types';
/**
* Shared jq sub-expression resolving the gbrain MCP registration entry
* (#2499). Claude Code registers MCP servers at TWO scopes in
* ~/.claude.json: user scope (.mcpServers.gbrain) and project scope
* (.projects["/abs/path"].mcpServers.gbrain — what `claude mcp add`
* WITHOUT --scope user writes). Reading only user scope makes a correctly
* configured project-scoped brain invisible: brain-aware blocks suppress,
* remote-mode sync is never recognised, nothing errors.
*
* Resolution order: user scope first, then the nearest-ancestor project
* entry for $PWD that actually carries a gbrain server (longest matching
* key with a path-boundary check, so nested repos pick their own
* registration, /a/repo never matches /a/repo2, and a nested project
* WITHOUT gbrain doesn't shadow its parent's registration). The emitted
* bash resolves the entry ONCE into _GBRAIN_MCP_ENTRY (compact JSON) and
* extracts fields from that variable — one jq parse of claude.json per
* skill start, and the long expression appears once per rendered SKILL.md.
*/
// Project-local scope BEATS user scope — verified empirically against claude
// 2.1.233 with hermetic fixtures ('claude mcp get gbrain' reports Scope:
// Local config when both scopes define the server). The operand order below
// (nearest-ancestor project first, user-scope fallback) mirrors that; the
// pre-wave user-first order mis-resolved whenever the scopes disagreed.
// The ancestor match accepts BOTH separators: project keys and $PWD are
// backslash-formed on Windows, so a "/"-only startswith never matched there
// and project-scoped brains were invisible. `"\\\\"` in this TS source is a
// jq string containing ONE backslash (TS halves it, jq halves it again) —
// mirrors the path-boundary handling in the TS scope resolvers.
const GBRAIN_MCP_ENTRY_JQ =
'((.projects // {}) | to_entries | map(select((.key as $k | $cwd == $k or ($cwd | startswith($k + "/")) or ($cwd | startswith($k + "\\\\"))) and ((try .value.mcpServers.gbrain catch null) != null))) | sort_by(.key | length) | last | .value.mcpServers.gbrain) // .mcpServers.gbrain // empty';
export function generateBrainSyncBlock(ctx: TemplateContext): string { export function generateBrainSyncBlock(ctx: TemplateContext): string {
const isBrainHost = ctx.host === 'gbrain' || ctx.host === 'hermes'; const isBrainHost = ctx.host === 'gbrain' || ctx.host === 'hermes';
return `## Artifacts Sync (skill start) return `## Artifacts Sync (skill start)
\`\`\`bash The skill-start output above already ran artifacts sync. Act on its lines:
_GSTACK_HOME="\${GSTACK_HOME:-$HOME/.gstack}" GBrain hint text (if present) tells you when to prefer \`gbrain\` over Grep;
# Prefer the v1.27.0.0 artifacts file; fall back to brain file for users \`ARTIFACTS_SYNC:\` reports sync health (\`off\`, \`mode=... | queue=N\`,
# upgrading mid-stream before the migration script runs. \`remote-mode\`, or a restore hint naming \`gstack-brain-restore\`).
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="${quoteSafePath(ctx.paths.binDir)}/gstack-brain-sync"
_BRAIN_CONFIG_BIN="${quoteSafePath(ctx.paths.binDir)}/gstack-config"
# /sync-gbrain context-load: teach the agent to use gbrain when it's available. ${isBrainHost ? `If output shows \`ARTIFACTS_SYNC: artifacts repo detected\`, offer \`gstack-brain-restore\` via AskUserQuestion; otherwise continue.
# Per-worktree pin: post-spike redesign uses kubectl-style \`.gbrain-source\` in the
# git toplevel to scope queries. Look for the pin in the worktree (not a global
# state file) so that opening worktree B without a pin doesn't claim "indexed"
# just because worktree A was synced. 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
_GBRAIN_PIN_PATH=""
_REPO_TOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
if [ -n "$_REPO_TOP" ] && [ -f "$_REPO_TOP/.gbrain-source" ]; then
_GBRAIN_PIN_PATH="$_REPO_TOP/.gbrain-source"
fi
if [ -n "$_GBRAIN_PIN_PATH" ]; 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 worktree isn't pinned yet. Run \\\`/sync-gbrain --full\\\`"
echo "before relying on \\\`gbrain search\\\` for code questions in this worktree."
echo "Falls back to Grep until pinned."
fi
fi
fi
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get artifacts_sync_mode 2>/dev/null || echo off) ` : ''}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:
# 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). Both registration scopes
# are read (#2499): user scope, then the nearest-ancestor project scope.
_GBRAIN_MCP_MODE="none"
_GBRAIN_MCP_ENTRY=""
if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.claude.json" ]; then
_GBRAIN_MCP_ENTRY=$(jq -c --arg cwd "$PWD" '${GBRAIN_MCP_ENTRY_JQ}' "$HOME/.claude.json" 2>/dev/null)
_GBRAIN_MCP_TYPE=$(printf '%s' "$_GBRAIN_MCP_ENTRY" | jq -r '.type // .transport // empty' 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)
case "$_BRAIN_LAST" in ''|*[!0-9]*) _BRAIN_LAST=0 ;; esac
_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=$(printf '%s' "\${_GBRAIN_MCP_ENTRY:-}" | jq -r '.url // empty' 2>/dev/null | sed -E 's|^https?://([^/:]+).*|\\1|' | head -1 | tr -cd 'A-Za-z0-9._-')
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
# Spool-dir queue (one file per record); legacy .brain-queue.jsonl lines are
# counted too until the drain migrates them.
[ -d "$_GSTACK_HOME/.brain-queue.d" ] && _BRAIN_QUEUE_DEPTH=$(find "$_GSTACK_HOME/.brain-queue.d" -maxdepth 1 -name '*.json' 2>/dev/null | wc -l | tr -d ' ')
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(( _BRAIN_QUEUE_DEPTH + $(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ') ))
[ -f "$_GSTACK_HOME/.brain-queue.jsonl.migrating" ] && _BRAIN_QUEUE_DEPTH=$(( _BRAIN_QUEUE_DEPTH + $(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl.migrating" | 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? > gstack can publish your artifacts (CEO plans, designs, reports) to a private GitHub repo that GBrain indexes across machines. How much should sync?
@@ -183,17 +39,9 @@ After answer:
\`\`\`bash \`\`\`bash
# Chosen mode: full | artifacts-only | off # Chosen mode: full | artifacts-only | off
"$_BRAIN_CONFIG_BIN" set artifacts_sync_mode <choice> ${ctx.paths.binDir}/gstack-config set artifacts_sync_mode <choice>
"$_BRAIN_CONFIG_BIN" set artifacts_sync_mode_prompted true ${ctx.paths.binDir}/gstack-config 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. 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
"${quoteSafePath(ctx.paths.binDir)}/gstack-brain-sync" --discover-new 2>/dev/null || true
"${quoteSafePath(ctx.paths.binDir)}/gstack-brain-sync" --once 2>/dev/null || true
\`\`\`
`;
} }
@@ -58,36 +58,24 @@ Do not log obvious facts or one-time transient errors.
## Telemetry (run last) ## Telemetry (run last)
After workflow completion, log telemetry. Use skill \`name:\` from frontmatter. OUTCOME is success/error/abort/unknown. After workflow completion, log telemetry with ONE command. OUTCOME is
success/error/abort/unknown; \`SESSION_ID\` and \`TEL_START\` are the values the
preamble's skill-start output echoed. It also drains the artifacts-sync queue
(the former skill-end sync step — do not run gstack-brain-sync separately).
**PLAN MODE EXCEPTION — ALWAYS RUN:** This command writes telemetry to **PLAN MODE EXCEPTION — ALWAYS RUN:** This writes telemetry to
\`~/.gstack/analytics/\`, matching preamble analytics writes. \`~/.gstack/analytics/\`, matching preamble analytics writes.
Run this bash:
\`\`\`bash \`\`\`bash
_TEL_END=$(date +%s) ${ctx.paths.binDir}/gstack-skill-end --skill "${ctx.skillName}" --outcome OUTCOME \\
_TEL_DUR=$(( _TEL_END - _TEL_START )) --session-id "SESSION_ID" --tel-start "TEL_START" --used-browse USED_BROWSE \\
rm -f ~/.gstack/analytics/.pending-"$_SESSION_ID" 2>/dev/null || true --error-message "ERROR_MESSAGE" --failed-step "FAILED_STEP" 2>/dev/null || true
# Session timeline: record skill completion (local-only, never sent anywhere)
~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"SKILL_NAME","event":"completed","branch":"'$(git branch --show-current 2>/dev/null || echo unknown)'","outcome":"OUTCOME","duration_s":"'"$_TEL_DUR"'","session":"'"$_SESSION_ID"'"}' 2>/dev/null || true
# Local analytics (gated on telemetry setting)
if [ "$_TEL" != "off" ]; then
echo '{"skill":"SKILL_NAME","duration_s":"'"$_TEL_DUR"'","outcome":"OUTCOME","browse":"USED_BROWSE","session":"'"$_SESSION_ID"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
fi
# Remote telemetry (opt-in, requires binary)
if [ "$_TEL" != "off" ] && [ -x ~/.claude/skills/gstack/bin/gstack-telemetry-log ]; then
~/.claude/skills/gstack/bin/gstack-telemetry-log \\
--skill "SKILL_NAME" --duration "$_TEL_DUR" --outcome "OUTCOME" \\
--used-browse "USED_BROWSE" --session-id "$_SESSION_ID" \\
--error-message "ERROR_MESSAGE" --failed-step "FAILED_STEP" 2>/dev/null &
fi
\`\`\` \`\`\`
Replace \`SKILL_NAME\`, \`OUTCOME\`, and \`USED_BROWSE\` before running. Replace \`OUTCOME\` and \`USED_BROWSE\` (yes/no) before running; substitute
Replace \`ERROR_MESSAGE\` with a short description of the error (if outcome is error, \`SESSION_ID\`/\`TEL_START\` from the skill-start echoes. \`ERROR_MESSAGE\`/\`FAILED_STEP\`
otherwise use empty string ""), and \`FAILED_STEP\` with the step name or number where are "" unless outcome is error. If the command is missing (stale install), skip
the failure occurred (if outcome is error, otherwise use empty string ""). telemetry — it never blocks the workflow.
## Plan Status Footer ## Plan Status Footer
@@ -1,7 +1,23 @@
import type { TemplateContext } from '../types'; import type { TemplateContext } from '../types';
import { quoteSafePath } from '../types';
import { getHostConfig } from '../../../hosts/index'; import { getHostConfig } from '../../../hosts/index';
/**
* Preamble bootstrap (token-reduction Phase 1).
*
* The ~6.3KB inline bash this generator used to emit (session bookkeeping,
* config reads, ~20 STATUS echoes) now lives in `bin/gstack-skill-start`,
* alongside the artifacts-sync bash that generate-brain-sync-block used to
* inline (~6.8KB). The render carries a short invocation fence plus the
* interpretation rules the model actually needs. The STATUS-line contract
* between this prose and the script is pinned by
* test/gstack-skill-start.test.ts (every KEY the prose references must be
* emitted by the script, for every host render).
*
* Divergences from the old inline bash are deliberate and enumerated in the
* script header (EOV5): $0-relative paths, --parent-pid for session identity,
* GSTACK_HOME normalization, SKILL_START_PROTO handshake, passthrough
* sanitization.
*/
export function generatePreambleBash(ctx: TemplateContext): string { export function generatePreambleBash(ctx: TemplateContext): string {
const hostConfig = getHostConfig(ctx.host); const hostConfig = getHostConfig(ctx.host);
const runtimeRoot = hostConfig.usesEnvVars const runtimeRoot = hostConfig.usesEnvVars
@@ -13,132 +29,28 @@ GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"
GSTACK_DESIGN="$GSTACK_ROOT/design/dist" GSTACK_DESIGN="$GSTACK_ROOT/design/dist"
` `
: ''; : '';
const brainHealthFlag = ctx.host === 'gbrain' || ctx.host === 'hermes' ? ' --brain-health' : '';
// A leading ~ inside double quotes never expands in bash — the primary path
// would silently fail -x and every run would take the fallback. Interpolate
// through $HOME instead (env-var hosts already use $GSTACK_BIN).
const shellPath = (p: string) => p.replace(/^~\//, '$HOME/');
return `## Preamble (run first) return `## Preamble (run first)
\`\`\`bash \`\`\`bash
${runtimeRoot}_UPD=$(${ctx.paths.binDir}/gstack-update-check 2>/dev/null || ${ctx.paths.localSkillRoot}/bin/gstack-update-check 2>/dev/null || true) ${runtimeRoot}_SS="${shellPath(ctx.paths.binDir)}/gstack-skill-start"
[ -n "$_UPD" ] && echo "$_UPD" || true [ -x "$_SS" ] || _SS="${shellPath(ctx.paths.localSkillRoot)}/bin/gstack-skill-start"
mkdir -p ~/.gstack/sessions "$_SS" --skill "${ctx.skillName}" --model "${ctx.model ?? 'none'}" --parent-pid "$PPID"${brainHealthFlag} \\
touch ~/.gstack/sessions/"$PPID" || echo "SKILL_START: unavailable — stale install; run ./setup or /gstack-upgrade (preamble degraded, continue the user's task)"
_SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr -d ' ') \`\`\`
find ~/.gstack/sessions -mmin +120 -type f -exec rm {} + 2>/dev/null || true
_PROACTIVE=$(${ctx.paths.binDir}/gstack-config get proactive 2>/dev/null || echo "true") Read the echoed \`KEY: value\` STATUS lines — they drive every preamble rule
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no") below. **Degraded mode:** if \`SKILL_START_PROTO: 1\` is missing from the output
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown") (script absent, stale install, or a different protocol number), apply safe
echo "BRANCH: $_BRANCH" defaults: treat \`SESSION_KIND\` as \`interactive\`, do NOT assume Conductor,
_SKILL_PREFIX=$(${ctx.paths.binDir}/gstack-config get skill_prefix 2>/dev/null || echo "false") skip onboarding/telemetry steps (their gates are marker-based, so consent and
echo "PROACTIVE: $_PROACTIVE" onboarding prompts are DEFERRED to the next healthy run — never lost), tell
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" the user to run \`./setup\` or \`/gstack-upgrade\`, and proceed with their task.
echo "SKILL_PREFIX: $_SKILL_PREFIX" Note \`SESSION_ID\` and \`TEL_START\` from the output — the Telemetry step needs
source <(${ctx.paths.binDir}/gstack-repo-mode 2>/dev/null) || true them at skill end.`;
REPO_MODE=\${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(${ctx.paths.binDir}/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
# Conductor host: AskUserQuestion is unreliable here (native disabled, MCP
# variant flaky), so skills render decisions as prose instead of calling the
# tool. Gated on !headless so an eval/CI run INSIDE Conductor (GSTACK_HEADLESS)
# still BLOCKs rather than rendering prose to nobody.
if [ "$_SESSION_KIND" != "headless" ] && { [ -n "\${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "\${CONDUCTOR_PORT:-}" ]; }; then
echo "CONDUCTOR_SESSION: true"
fi
_ACTIVATED=$([ -f ~/.gstack/.activated ] && echo "yes" || echo "no")
_FIRST_LOOP_SHOWN=$([ -f ~/.gstack/.first-loop-tip-shown ] && echo "yes" || echo "no")
echo "ACTIVATED: $_ACTIVATED"
echo "FIRST_LOOP_SHOWN: $_FIRST_LOOP_SHOWN"
# First-run project detection: run the detector ONLY on the first-ever skill run
# (ACTIVATED=no, interactive) so it stays off the hot path for every run after.
_FIRST_TASK=""
if [ "$_ACTIVATED" = "no" ] && [ "$_SESSION_KIND" != "headless" ]; then
_FIRST_TASK=$(${ctx.paths.binDir}/gstack-first-task-detect 2>/dev/null || true)
fi
echo "FIRST_TASK: $_FIRST_TASK"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(${ctx.paths.binDir}/gstack-config get telemetry 2>/dev/null || true)
_TEL_PROMPTED=$([ -f ~/.gstack/.telemetry-prompted ] && echo "yes" || echo "no")
_TEL_START=$(date +%s)
_SESSION_ID="$$-$(date +%s)"
echo "TELEMETRY: \${_TEL:-off}"
echo "TEL_PROMPTED: $_TEL_PROMPTED"
_EXPLAIN_LEVEL=$(${ctx.paths.binDir}/gstack-config get explain_level 2>/dev/null || echo "default")
if [ "$_EXPLAIN_LEVEL" != "default" ] && [ "$_EXPLAIN_LEVEL" != "terse" ]; then _EXPLAIN_LEVEL="default"; fi
echo "EXPLAIN_LEVEL: $_EXPLAIN_LEVEL"
_QUESTION_TUNING=$(${ctx.paths.binDir}/gstack-config get question_tuning 2>/dev/null || echo "false")
echo "QUESTION_TUNING: $_QUESTION_TUNING"
_UPDATE_CHECK=$(${ctx.paths.binDir}/gstack-config get update_check 2>/dev/null || echo "true")
echo "UPDATE_CHECK: $_UPDATE_CHECK"
mkdir -p ~/.gstack/analytics
if [ "$_TEL" != "off" ]; then
echo '{"skill":"${ctx.skillName}","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null | tr -cd 'a-zA-Z0-9._-'); echo "\${_repo:-unknown}")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
fi
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
if [ -f "$_PF" ]; then
if [ "$_TEL" != "off" ] && [ -x "${quoteSafePath(ctx.paths.binDir)}/gstack-telemetry-log" ]; then
${ctx.paths.binDir}/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true
fi
rm -f "$_PF" 2>/dev/null || true
fi
break
done
eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)" 2>/dev/null || true
_LEARN_FILE="\${GSTACK_HOME:-$HOME/.gstack}/projects/\${SLUG:-unknown}/learnings.jsonl"
if [ -f "$_LEARN_FILE" ]; then
_LEARN_COUNT=$(wc -l < "$_LEARN_FILE" 2>/dev/null | tr -d ' ')
echo "LEARNINGS: $_LEARN_COUNT entries loaded"
if [ "$_LEARN_COUNT" -gt 5 ] 2>/dev/null; then
${ctx.paths.binDir}/gstack-learnings-search --limit 3 2>/dev/null || true
fi
else
echo "LEARNINGS: 0"
fi
${ctx.paths.binDir}/gstack-timeline-log '{"skill":"${ctx.skillName}","event":"started","branch":"'"$_BRANCH"'","session":"'"$_SESSION_ID"'"}' 2>/dev/null &
_HAS_ROUTING="no"
for _RF in CLAUDE.md AGENTS.md; do
if [ -f "$_RF" ] && grep -q "## Skill routing" "$_RF" 2>/dev/null; then
_HAS_ROUTING="yes"
fi
done
_ROUTING_DECLINED=$(${ctx.paths.binDir}/gstack-config get routing_declined 2>/dev/null || echo "false")
echo "HAS_ROUTING: $_HAS_ROUTING"
echo "ROUTING_DECLINED: $_ROUTING_DECLINED"
_VENDORED="no"
if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then
if [ -f ".claude/skills/gstack/VERSION" ] || [ -d ".claude/skills/gstack/.git" ]; then
_VENDORED="yes"
fi
fi
echo "VENDORED_GSTACK: $_VENDORED"
echo "MODEL_OVERLAY: ${ctx.model ?? 'none'}"
_CHECKPOINT_MODE=$(${ctx.paths.binDir}/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit")
_CHECKPOINT_PUSH=$(${ctx.paths.binDir}/gstack-config get checkpoint_push 2>/dev/null || echo "false")
echo "CHECKPOINT_MODE: $_CHECKPOINT_MODE"
echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH"
# Plan-mode hint for skills like /spec that branch behavior on plan-mode state.
# Claude Code exposes plan mode via system reminders; we detect best-effort
# from CLAUDE_PLAN_FILE (set by the harness when plan mode is active) and
# fall back to "inactive". Codex hosts and Claude execution mode both end up
# inactive, which is the safe default (defaults to file+execute pipeline).
if [ -n "\${CLAUDE_PLAN_FILE:-}\${GSTACK_PLAN_MODE_FORCE:-}" ]; then
export GSTACK_PLAN_MODE="active"
elif [ "\${GSTACK_PLAN_MODE:-}" = "active" ]; then
export GSTACK_PLAN_MODE="active"
else
export GSTACK_PLAN_MODE="inactive"
fi
echo "GSTACK_PLAN_MODE: $GSTACK_PLAN_MODE"
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true${ctx.host === 'gbrain' || ctx.host === 'hermes' ? `
if command -v gbrain &>/dev/null; then
_BRAIN_JSON=$(gbrain doctor --fast --json 2>/dev/null || echo '{}')
_BRAIN_SCORE=$(echo "$_BRAIN_JSON" | grep -o '"health_score":[0-9]*' | cut -d: -f2)
_BRAIN_FAILS=$(echo "$_BRAIN_JSON" | grep -o '"status":"fail"' | wc -l | tr -d ' ')
_BRAIN_WARNS=$(echo "$_BRAIN_JSON" | grep -o '"status":"warn"' | wc -l | tr -d ' ')
echo "BRAIN_HEALTH: \${_BRAIN_SCORE:-unknown} (\${_BRAIN_FAILS:-0} failures, \${_BRAIN_WARNS:-0} warnings)"
if [ "\${_BRAIN_SCORE:-100}" -lt 50 ] 2>/dev/null; then
echo "$_BRAIN_JSON" | grep -o '"name":"[^"]*","status":"[^"]*","message":"[^"]*"' || true
fi
fi` : ''}
\`\`\``;
} }