mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
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:
co-authored by
Claude Fable 5
parent
b1199afc03
commit
e9d85060ca
@@ -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 <host>)` 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 <choice>
|
||||
"$_BRAIN_CONFIG_BIN" set artifacts_sync_mode_prompted true
|
||||
${ctx.paths.binDir}/gstack-config set artifacts_sync_mode <choice>
|
||||
${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.`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user