From e9d85060cada234d1a152120272401d126b3cada Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 25 Aug 2026 15:36:35 +0000 Subject: [PATCH] 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 --- .../preamble/generate-brain-sync-block.ts | 192 ++---------------- .../preamble/generate-completion-status.ts | 36 ++-- .../preamble/generate-preamble-bash.ts | 162 ++++----------- 3 files changed, 69 insertions(+), 321 deletions(-) diff --git a/scripts/resolvers/preamble/generate-brain-sync-block.ts b/scripts/resolvers/preamble/generate-brain-sync-block.ts index 2501747d9..a0a63c346 100644 --- a/scripts/resolvers/preamble/generate-brain-sync-block.ts +++ b/scripts/resolvers/preamble/generate-brain-sync-block.ts @@ -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: - * 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 )` since this machine doesn't sync - * anything locally — the brain admin's server pulls from GitHub/GitLab. + * The ~6.8KB of artifacts-sync bash this generator used to inline (gbrain + * availability hints, remote-MCP detection via claude.json, restore hint, + * daily pull, queue-depth status) now runs inside `bin/gstack-skill-start` + * (invoked by the Preamble fence above) and reports through the same STATUS + * lines it always emitted: the GBrain hint text and the `ARTIFACTS_SYNC:` + * line. What remains here is the prose the model acts on — including the + * one-time privacy stop-gate, which stays inline until the Phase 2 + * instruction-emission layer moves it behind its runtime gate. * - * 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`. + * Skill-END sync is no longer a separate fence: `bin/gstack-skill-end` + * (invoked by the Telemetry step) drains the queue before logging. */ 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 { 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="${quoteSafePath(ctx.paths.binDir)}/gstack-brain-sync" -_BRAIN_CONFIG_BIN="${quoteSafePath(ctx.paths.binDir)}/gstack-config" +The skill-start output above already ran artifacts sync. Act on its lines: +GBrain hint text (if present) tells you when to prefer \`gbrain\` over Grep; +\`ARTIFACTS_SYNC:\` reports sync health (\`off\`, \`mode=... | queue=N\`, +\`remote-mode\`, or a restore hint naming \`gstack-brain-restore\`). -# /sync-gbrain context-load: teach the agent to use gbrain when it's available. -# 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 +${isBrainHost ? `If output shows \`ARTIFACTS_SYNC: artifacts repo detected\`, offer \`gstack-brain-restore\` via AskUserQuestion; otherwise continue. -_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). 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: +` : ''}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? @@ -183,17 +39,9 @@ After answer: \`\`\`bash # Chosen mode: full | artifacts-only | off -"$_BRAIN_CONFIG_BIN" set artifacts_sync_mode -"$_BRAIN_CONFIG_BIN" set artifacts_sync_mode_prompted true +${ctx.paths.binDir}/gstack-config set artifacts_sync_mode +${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. - -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 -\`\`\` -`; +If A/B and \`~/.gstack/.git\` is missing, ask whether to run \`gstack-artifacts-init\`. Do not block the skill.`; } diff --git a/scripts/resolvers/preamble/generate-completion-status.ts b/scripts/resolvers/preamble/generate-completion-status.ts index eb73656d4..03091bdca 100644 --- a/scripts/resolvers/preamble/generate-completion-status.ts +++ b/scripts/resolvers/preamble/generate-completion-status.ts @@ -58,36 +58,24 @@ Do not log obvious facts or one-time transient errors. ## 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. -Run this bash: - \`\`\`bash -_TEL_END=$(date +%s) -_TEL_DUR=$(( _TEL_END - _TEL_START )) -rm -f ~/.gstack/analytics/.pending-"$_SESSION_ID" 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 +${ctx.paths.binDir}/gstack-skill-end --skill "${ctx.skillName}" --outcome OUTCOME \\ + --session-id "SESSION_ID" --tel-start "TEL_START" --used-browse USED_BROWSE \\ + --error-message "ERROR_MESSAGE" --failed-step "FAILED_STEP" 2>/dev/null || true \`\`\` -Replace \`SKILL_NAME\`, \`OUTCOME\`, and \`USED_BROWSE\` before running. -Replace \`ERROR_MESSAGE\` with a short description of the error (if outcome is error, -otherwise use empty string ""), and \`FAILED_STEP\` with the step name or number where -the failure occurred (if outcome is error, otherwise use empty string ""). +Replace \`OUTCOME\` and \`USED_BROWSE\` (yes/no) before running; substitute +\`SESSION_ID\`/\`TEL_START\` from the skill-start echoes. \`ERROR_MESSAGE\`/\`FAILED_STEP\` +are "" unless outcome is error. If the command is missing (stale install), skip +telemetry — it never blocks the workflow. ## Plan Status Footer diff --git a/scripts/resolvers/preamble/generate-preamble-bash.ts b/scripts/resolvers/preamble/generate-preamble-bash.ts index e133b5d76..8a2253b7b 100644 --- a/scripts/resolvers/preamble/generate-preamble-bash.ts +++ b/scripts/resolvers/preamble/generate-preamble-bash.ts @@ -1,7 +1,23 @@ import type { TemplateContext } from '../types'; -import { quoteSafePath } from '../types'; 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 { const hostConfig = getHostConfig(ctx.host); const runtimeRoot = hostConfig.usesEnvVars @@ -13,132 +29,28 @@ GSTACK_BROWSE="$GSTACK_ROOT/browse/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) \`\`\`bash -${runtimeRoot}_UPD=$(${ctx.paths.binDir}/gstack-update-check 2>/dev/null || ${ctx.paths.localSkillRoot}/bin/gstack-update-check 2>/dev/null || true) -[ -n "$_UPD" ] && echo "$_UPD" || true -mkdir -p ~/.gstack/sessions -touch ~/.gstack/sessions/"$PPID" -_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") -_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no") -_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown") -echo "BRANCH: $_BRANCH" -_SKILL_PREFIX=$(${ctx.paths.binDir}/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(${ctx.paths.binDir}/gstack-repo-mode 2>/dev/null) || true -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` : ''} -\`\`\``; +${runtimeRoot}_SS="${shellPath(ctx.paths.binDir)}/gstack-skill-start" +[ -x "$_SS" ] || _SS="${shellPath(ctx.paths.localSkillRoot)}/bin/gstack-skill-start" +"$_SS" --skill "${ctx.skillName}" --model "${ctx.model ?? 'none'}" --parent-pid "$PPID"${brainHealthFlag} \\ + || echo "SKILL_START: unavailable — stale install; run ./setup or /gstack-upgrade (preamble degraded, continue the user's task)" +\`\`\` + +Read the echoed \`KEY: value\` STATUS lines — they drive every preamble rule +below. **Degraded mode:** if \`SKILL_START_PROTO: 1\` is missing from the output +(script absent, stale install, or a different protocol number), apply safe +defaults: treat \`SESSION_KIND\` as \`interactive\`, do NOT assume Conductor, +skip onboarding/telemetry steps (their gates are marker-based, so consent and +onboarding prompts are DEFERRED to the next healthy run — never lost), tell +the user to run \`./setup\` or \`/gstack-upgrade\`, and proceed with their task. +Note \`SESSION_ID\` and \`TEL_START\` from the output — the Telemetry step needs +them at skill end.`; }