diff --git a/SKILL.md b/SKILL.md index cf72cf7a9..0b3feb89b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -27,121 +27,22 @@ skill, or ask "which gstack skill fits this?". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"gstack","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"gstack","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "gstack" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -334,110 +235,10 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -452,20 +253,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -520,36 +313,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "gstack" --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/autoplan/SKILL.md b/autoplan/SKILL.md index 66cd1a62a..f2dfc955e 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -36,121 +36,22 @@ Voice triggers (speech-to-text aliases): "auto plan", "automatic review". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"autoplan","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"autoplan","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "autoplan" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -468,110 +369,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -586,20 +387,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -796,36 +589,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "autoplan" --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/benchmark-models/SKILL.md b/benchmark-models/SKILL.md index 5c39c2302..356f88f87 100644 --- a/benchmark-models/SKILL.md +++ b/benchmark-models/SKILL.md @@ -31,121 +31,22 @@ Voice triggers (speech-to-text aliases): "compare models", "model shootout", "wh ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"benchmark-models","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"benchmark-models","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "benchmark-models" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -338,110 +239,10 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -456,20 +257,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -524,36 +317,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "benchmark-models" --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/benchmark/SKILL.md b/benchmark/SKILL.md index ae893b05e..eb519b827 100644 --- a/benchmark/SKILL.md +++ b/benchmark/SKILL.md @@ -31,121 +31,22 @@ Voice triggers (speech-to-text aliases): "speed test", "check performance". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"benchmark","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"benchmark","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "benchmark" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -338,110 +239,10 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -456,20 +257,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -524,36 +317,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "benchmark" --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/browse/SKILL.md b/browse/SKILL.md index d8f70c813..e357d2b04 100644 --- a/browse/SKILL.md +++ b/browse/SKILL.md @@ -29,121 +29,22 @@ site", "take a screenshot", or "dogfood this". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"browse","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"browse","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "browse" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -336,110 +237,10 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -454,20 +255,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -522,36 +315,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "browse" --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/canary/SKILL.md b/canary/SKILL.md index bacb1335e..a39dc9d25 100644 --- a/canary/SKILL.md +++ b/canary/SKILL.md @@ -29,121 +29,22 @@ on anomalies. Use when: "monitor deploy", "canary", "post-deploy check", ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"canary","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"canary","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "canary" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -461,110 +362,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -579,20 +380,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -771,36 +564,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "canary" --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/codex/SKILL.md b/codex/SKILL.md index 40cd0cab7..0959aef9f 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -32,121 +32,22 @@ Voice triggers (speech-to-text aliases): "code x", "code ex", "get another opini ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"codex","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"codex","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "codex" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -464,110 +365,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -582,20 +383,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -792,36 +585,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "codex" --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/context-restore/SKILL.md b/context-restore/SKILL.md index 31db0e8f8..fc1da99ae 100644 --- a/context-restore/SKILL.md +++ b/context-restore/SKILL.md @@ -33,121 +33,22 @@ as a native rewind alias in current environments. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"context-restore","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"context-restore","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "context-restore" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -465,110 +366,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -583,20 +384,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -775,36 +568,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "context-restore" --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/context-save/SKILL.md b/context-save/SKILL.md index 4ef9f249d..f186a1d98 100644 --- a/context-save/SKILL.md +++ b/context-save/SKILL.md @@ -32,121 +32,22 @@ native rewind alias in current environments, which was shadowing this skill. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"context-save","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"context-save","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "context-save" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -464,110 +365,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -582,20 +383,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -774,36 +567,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "context-save" --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/cso/SKILL.md b/cso/SKILL.md index 749c72d35..7a66ecf36 100644 --- a/cso/SKILL.md +++ b/cso/SKILL.md @@ -35,121 +35,22 @@ Voice triggers (speech-to-text aliases): "see-so", "see so", "security review", ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"cso","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"cso","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "cso" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -467,110 +368,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -585,20 +386,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -777,36 +570,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "cso" --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/design-consultation/SKILL.md b/design-consultation/SKILL.md index 7917a7de3..d4270351a 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -55,121 +55,22 @@ design system or DESIGN.md. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"design-consultation","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"design-consultation","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "design-consultation" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -487,110 +388,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -605,20 +406,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -815,36 +608,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "design-consultation" --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/design-html/SKILL.md b/design-html/SKILL.md index 72ebf5445..2bb91a641 100644 --- a/design-html/SKILL.md +++ b/design-html/SKILL.md @@ -36,121 +36,22 @@ Voice triggers (speech-to-text aliases): "build the design", "code the mockup", ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"design-html","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"design-html","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "design-html" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -468,110 +369,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -586,20 +387,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -778,36 +571,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "design-html" --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/design-review/SKILL.md b/design-review/SKILL.md index 41e30355a..a4bda32e9 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -33,121 +33,22 @@ wants to polish the look of a live site. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"design-review","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"design-review","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "design-review" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -465,110 +366,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -583,20 +384,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -793,36 +586,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "design-review" --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/design-shotgun/SKILL.md b/design-shotgun/SKILL.md index f43c0f738..55fda7bfa 100644 --- a/design-shotgun/SKILL.md +++ b/design-shotgun/SKILL.md @@ -50,121 +50,22 @@ what it could look like. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"design-shotgun","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"design-shotgun","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "design-shotgun" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -482,110 +383,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -600,20 +401,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -792,36 +585,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "design-shotgun" --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/devex-review/SKILL.md b/devex-review/SKILL.md index a5e1d5f94..d7629c1bb 100644 --- a/devex-review/SKILL.md +++ b/devex-review/SKILL.md @@ -35,121 +35,22 @@ Voice triggers (speech-to-text aliases): "dx audit", "test the developer experie ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"devex-review","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"devex-review","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "devex-review" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -467,110 +368,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -585,20 +386,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -795,36 +588,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "devex-review" --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/diagram/SKILL.md b/diagram/SKILL.md index 1bd399828..4392329fd 100644 --- a/diagram/SKILL.md +++ b/diagram/SKILL.md @@ -30,121 +30,22 @@ flowchart", "diagram this", or "visualize this flow". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"diagram","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"diagram","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "diagram" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -337,110 +238,10 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -455,20 +256,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -523,36 +316,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "diagram" --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/document-generate/SKILL.md b/document-generate/SKILL.md index 194b1ad59..3f3dabd18 100644 --- a/document-generate/SKILL.md +++ b/document-generate/SKILL.md @@ -35,121 +35,22 @@ complete, structured documentation. Can be invoked standalone or called by ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"document-generate","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"document-generate","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "document-generate" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -467,110 +368,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -585,20 +386,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -777,36 +570,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "document-generate" --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/document-release/SKILL.md b/document-release/SKILL.md index f763918b1..870e4c509 100644 --- a/document-release/SKILL.md +++ b/document-release/SKILL.md @@ -33,121 +33,22 @@ or "post-ship docs". Proactively suggest after a PR is merged or code is shipped ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"document-release","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"document-release","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "document-release" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -465,110 +366,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -583,20 +384,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -775,36 +568,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "document-release" --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/health/SKILL.md b/health/SKILL.md index bab29e888..480c052a5 100644 --- a/health/SKILL.md +++ b/health/SKILL.md @@ -31,121 +31,22 @@ test runner, dead code detector, shell linter), computes a weighted composite ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"health","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"health","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "health" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -463,110 +364,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -581,20 +382,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -773,36 +566,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "health" --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/investigate/SKILL.md b/investigate/SKILL.md index 31bb0c45a..ec905e876 100644 --- a/investigate/SKILL.md +++ b/investigate/SKILL.md @@ -70,121 +70,22 @@ yesterday", or is troubleshooting why something stopped working. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"investigate","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"investigate","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "investigate" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -502,110 +403,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -620,20 +421,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -812,36 +605,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "investigate" --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/ios-clean/SKILL.md b/ios-clean/SKILL.md index 8137f20cf..79e64c3c3 100644 --- a/ios-clean/SKILL.md +++ b/ios-clean/SKILL.md @@ -33,121 +33,22 @@ Voice triggers (speech-to-text aliases): "clean the iOS debug bridge", "remove D ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"ios-clean","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"ios-clean","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "ios-clean" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -465,110 +366,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -583,20 +384,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -793,36 +586,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "ios-clean" --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/ios-design-review/SKILL.md b/ios-design-review/SKILL.md index e638c3486..b7de8bbe3 100644 --- a/ios-design-review/SKILL.md +++ b/ios-design-review/SKILL.md @@ -35,121 +35,22 @@ Voice triggers (speech-to-text aliases): "review the iOS design", "audit the iPh ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"ios-design-review","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"ios-design-review","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "ios-design-review" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -467,110 +368,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -585,20 +386,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -795,36 +588,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "ios-design-review" --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/ios-fix/SKILL.md b/ios-fix/SKILL.md index 1142a218e..c1a8f4c8c 100644 --- a/ios-fix/SKILL.md +++ b/ios-fix/SKILL.md @@ -36,121 +36,22 @@ Voice triggers (speech-to-text aliases): "fix the iOS bug", "patch the iPhone ap ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"ios-fix","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"ios-fix","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "ios-fix" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -468,110 +369,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -586,20 +387,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -796,36 +589,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "ios-fix" --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/ios-qa/SKILL.md b/ios-qa/SKILL.md index 85813ca7e..3f488a8e7 100644 --- a/ios-qa/SKILL.md +++ b/ios-qa/SKILL.md @@ -39,121 +39,22 @@ Voice triggers (speech-to-text aliases): "iOS quality check", "test the iPhone a ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"ios-qa","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"ios-qa","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "ios-qa" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -471,110 +372,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -589,20 +390,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -799,36 +592,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "ios-qa" --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/ios-sync/SKILL.md b/ios-sync/SKILL.md index 9d0dcd3a1..d2effdbde 100644 --- a/ios-sync/SKILL.md +++ b/ios-sync/SKILL.md @@ -33,121 +33,22 @@ Voice triggers (speech-to-text aliases): "resync the iOS debug bridge", "regener ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"ios-sync","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"ios-sync","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "ios-sync" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -465,110 +366,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -583,20 +384,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -793,36 +586,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "ios-sync" --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/land-and-deploy/SKILL.md b/land-and-deploy/SKILL.md index a6079237d..c04799a21 100644 --- a/land-and-deploy/SKILL.md +++ b/land-and-deploy/SKILL.md @@ -28,121 +28,22 @@ creates the PR. Use when: "merge", "land", "deploy", "merge and verify", ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"land-and-deploy","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"land-and-deploy","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "land-and-deploy" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -460,110 +361,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -578,20 +379,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -788,36 +581,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "land-and-deploy" --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/landing-report/SKILL.md b/landing-report/SKILL.md index bf66b9a9e..e1782a697 100644 --- a/landing-report/SKILL.md +++ b/landing-report/SKILL.md @@ -30,121 +30,22 @@ the queue", "show me open PRs", or "which version do I claim next". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"landing-report","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"landing-report","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "landing-report" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -462,110 +363,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -580,20 +381,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -772,36 +565,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "landing-report" --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/learn/SKILL.md b/learn/SKILL.md index 8ec9554e3..dbd33a59d 100644 --- a/learn/SKILL.md +++ b/learn/SKILL.md @@ -31,121 +31,22 @@ Proactively suggest when the user asks about past patterns or wonders ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"learn","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"learn","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "learn" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -463,110 +364,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -581,20 +382,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -773,36 +566,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "learn" --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/make-pdf/SKILL.md b/make-pdf/SKILL.md index b0573679d..8d58f0380 100644 --- a/make-pdf/SKILL.md +++ b/make-pdf/SKILL.md @@ -30,121 +30,22 @@ Voice triggers (speech-to-text aliases): "make this a pdf", "make it a pdf", "ex ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"make-pdf","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"make-pdf","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "make-pdf" --model "claude" --parent-pid "$PPID" \ + || 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. + ## MAKE-PDF SETUP (run this check BEFORE any make-pdf command) ```bash @@ -373,110 +274,10 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -491,20 +292,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -559,36 +352,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "make-pdf" --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/office-hours/SKILL.md b/office-hours/SKILL.md index 4861146f9..d4eb3c41c 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -66,121 +66,22 @@ Use before /plan-ceo-review or /plan-eng-review. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"office-hours","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"office-hours","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "office-hours" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -498,110 +399,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -616,20 +417,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -826,36 +619,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "office-hours" --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/open-gstack-browser/SKILL.md b/open-gstack-browser/SKILL.md index ba9058796..dc8aad5c0 100644 --- a/open-gstack-browser/SKILL.md +++ b/open-gstack-browser/SKILL.md @@ -29,121 +29,22 @@ Voice triggers (speech-to-text aliases): "show me the browser". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"open-gstack-browser","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"open-gstack-browser","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "open-gstack-browser" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -336,110 +237,10 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -454,20 +255,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -522,36 +315,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "open-gstack-browser" --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/pair-agent/SKILL.md b/pair-agent/SKILL.md index 2f8246068..ec37f0117 100644 --- a/pair-agent/SKILL.md +++ b/pair-agent/SKILL.md @@ -32,121 +32,22 @@ Voice triggers (speech-to-text aliases): "pair agent", "connect agent", "share m ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"pair-agent","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"pair-agent","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "pair-agent" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -464,110 +365,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -582,20 +383,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -774,36 +567,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "pair-agent" --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/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index ecd3970fe..310cd8bc2 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -58,121 +58,22 @@ or when the plan feels like it could be thinking bigger. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"plan-ceo-review","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"plan-ceo-review","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "plan-ceo-review" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -490,110 +391,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -608,20 +409,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -818,36 +611,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "plan-ceo-review" --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/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index bbd971136..5d12b446e 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -31,121 +31,22 @@ should be reviewed before implementation. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"plan-design-review","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"plan-design-review","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "plan-design-review" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -463,110 +364,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -581,20 +382,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -791,36 +584,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "plan-design-review" --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/plan-devex-review/SKILL.md b/plan-devex-review/SKILL.md index d5ced6bb8..2048e6333 100644 --- a/plan-devex-review/SKILL.md +++ b/plan-devex-review/SKILL.md @@ -36,121 +36,22 @@ Voice triggers (speech-to-text aliases): "dx review", "developer experience revi ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"plan-devex-review","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"plan-devex-review","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "plan-devex-review" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -468,110 +369,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -586,20 +387,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -796,36 +589,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "plan-devex-review" --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/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md index 4c19c4da1..b70c08f9b 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -34,121 +34,22 @@ Voice triggers (speech-to-text aliases): "tech review", "technical review", "pla ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"plan-eng-review","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"plan-eng-review","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "plan-eng-review" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -466,110 +367,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -584,20 +385,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -794,36 +587,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "plan-eng-review" --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/plan-tune/SKILL.md b/plan-tune/SKILL.md index f79dd1b35..d0735f98e 100644 --- a/plan-tune/SKILL.md +++ b/plan-tune/SKILL.md @@ -41,121 +41,22 @@ or when they explicitly override a recommendation for the Nth time. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"plan-tune","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"plan-tune","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "plan-tune" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -473,110 +374,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -591,20 +392,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -783,36 +576,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "plan-tune" --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/qa-only/SKILL.md b/qa-only/SKILL.md index 48194186d..774e205a7 100644 --- a/qa-only/SKILL.md +++ b/qa-only/SKILL.md @@ -31,121 +31,22 @@ Voice triggers (speech-to-text aliases): "bug report", "just check for bugs". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"qa-only","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"qa-only","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "qa-only" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -463,110 +364,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -581,20 +382,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -791,36 +584,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "qa-only" --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/qa/SKILL.md b/qa/SKILL.md index 5e7018911..09f36bded 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -37,121 +37,22 @@ Voice triggers (speech-to-text aliases): "quality check", "test the app", "run Q ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"qa","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"qa","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "qa" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -469,110 +370,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -587,20 +388,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -797,36 +590,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "qa" --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/retro/SKILL.md b/retro/SKILL.md index db308fc02..6c23a7b9a 100644 --- a/retro/SKILL.md +++ b/retro/SKILL.md @@ -51,121 +51,22 @@ Proactively suggest at the end of a work week or sprint. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"retro","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"retro","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "retro" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -483,110 +384,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -601,20 +402,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -793,36 +586,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "retro" --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/review/SKILL.md b/review/SKILL.md index e658574c9..52a060d73 100644 --- a/review/SKILL.md +++ b/review/SKILL.md @@ -33,121 +33,22 @@ Proactively suggest when the user is about to merge or land code changes. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"review","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"review","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "review" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -465,110 +366,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -583,20 +384,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -793,36 +586,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "review" --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/scrape/SKILL.md b/scrape/SKILL.md index fa2cf4f37..0b23e1044 100644 --- a/scrape/SKILL.md +++ b/scrape/SKILL.md @@ -30,121 +30,22 @@ Use when asked to "scrape", "get data from", "pull", "extract from", or ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"scrape","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"scrape","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "scrape" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -337,110 +238,10 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -455,20 +256,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -523,36 +316,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "scrape" --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/setup-browser-cookies/SKILL.md b/setup-browser-cookies/SKILL.md index d1fe69ebc..c9a03780c 100644 --- a/setup-browser-cookies/SKILL.md +++ b/setup-browser-cookies/SKILL.md @@ -25,121 +25,22 @@ Use before QA testing authenticated pages. Use when asked to "import cookies", ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"setup-browser-cookies","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"setup-browser-cookies","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "setup-browser-cookies" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -332,110 +233,10 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -450,20 +251,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -518,36 +311,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "setup-browser-cookies" --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/setup-deploy/SKILL.md b/setup-deploy/SKILL.md index 9e061383c..b2a52d2af 100644 --- a/setup-deploy/SKILL.md +++ b/setup-deploy/SKILL.md @@ -32,121 +32,22 @@ Use when: "setup deploy", "configure deployment", "set up land-and-deploy", ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"setup-deploy","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"setup-deploy","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "setup-deploy" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -464,110 +365,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -582,20 +383,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -774,36 +567,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "setup-deploy" --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/setup-gbrain/SKILL.md b/setup-gbrain/SKILL.md index 327184b08..9c3aedb70 100644 --- a/setup-gbrain/SKILL.md +++ b/setup-gbrain/SKILL.md @@ -31,121 +31,22 @@ gbrain", "install gbrain", "configure gbrain for this machine". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"setup-gbrain","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"setup-gbrain","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "setup-gbrain" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -463,110 +364,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -581,20 +382,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -773,36 +566,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "setup-gbrain" --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/ship/SKILL.md b/ship/SKILL.md index 6c61f37ed..2173697c4 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -33,121 +33,22 @@ is ready, asks about deploying, wants to push code up, or asks to create a PR. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"ship","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"ship","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "ship" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -465,110 +366,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -583,20 +384,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -793,36 +586,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "ship" --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/skillify/SKILL.md b/skillify/SKILL.md index a1cdde2c6..5d90f3612 100644 --- a/skillify/SKILL.md +++ b/skillify/SKILL.md @@ -30,121 +30,22 @@ Use when asked to "skillify", "codify", "save this scrape", or ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"skillify","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"skillify","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "skillify" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -462,110 +363,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -580,20 +381,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -772,36 +565,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "skillify" --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/spec/SKILL.md b/spec/SKILL.md index 6d8d71187..bc8a87969 100644 --- a/spec/SKILL.md +++ b/spec/SKILL.md @@ -31,121 +31,22 @@ the source issue on merge. Use when asked to "spec this out", "file an issue", ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"spec","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"spec","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "spec" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -463,110 +364,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -581,20 +382,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -791,36 +584,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "spec" --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/sync-gbrain/SKILL.md b/sync-gbrain/SKILL.md index 09a46e1df..9d036339b 100644 --- a/sync-gbrain/SKILL.md +++ b/sync-gbrain/SKILL.md @@ -32,121 +32,22 @@ things". ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"sync-gbrain","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"sync-gbrain","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "sync-gbrain" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -464,110 +365,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -582,20 +383,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -774,36 +567,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "sync-gbrain" --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/test/fixtures/golden/claude-ship-SKILL.md b/test/fixtures/golden/claude-ship-SKILL.md index 6c61f37ed..2173697c4 100644 --- a/test/fixtures/golden/claude-ship-SKILL.md +++ b/test/fixtures/golden/claude-ship-SKILL.md @@ -33,121 +33,22 @@ is ready, asks about deploying, wants to push code up, or asks to create a PR. ## Preamble (run first) ```bash -_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$(~/.claude/skills/gstack/bin/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":"ship","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 "$HOME/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then - ~/.claude/skills/gstack/bin/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 "$(~/.claude/skills/gstack/bin/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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"ship","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=$(~/.claude/skills/gstack/bin/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: claude" -_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/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 +_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" +[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "ship" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -465,110 +366,10 @@ Before calling AskUserQuestion, verify: ## 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="$HOME/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -583,20 +384,12 @@ 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 +~/.claude/skills/gstack/bin/gstack-config set artifacts_sync_mode +~/.claude/skills/gstack/bin/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 -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"$HOME/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -793,36 +586,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 +~/.claude/skills/gstack/bin/gstack-skill-end --skill "ship" --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/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index ba77c8d78..8c9307b20 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -19,121 +19,22 @@ GSTACK_ROOT="$HOME/.codex/skills/gstack" GSTACK_BIN="$GSTACK_ROOT/bin" GSTACK_BROWSE="$GSTACK_ROOT/browse/dist" GSTACK_DESIGN="$GSTACK_ROOT/design/dist" -_UPD=$($GSTACK_BIN/gstack-update-check 2>/dev/null || .agents/skills/gstack/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=$($GSTACK_BIN/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=$($GSTACK_BIN/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <($GSTACK_BIN/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$($GSTACK_BIN/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=$($GSTACK_BIN/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=$($GSTACK_BIN/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=$($GSTACK_BIN/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=$($GSTACK_BIN/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$($GSTACK_BIN/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":"ship","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 "$GSTACK_BIN/gstack-telemetry-log" ]; then - $GSTACK_BIN/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 "$($GSTACK_BIN/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 - $GSTACK_BIN/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -$GSTACK_BIN/gstack-timeline-log '{"skill":"ship","event":"started","branch":"'"$_BRANCH"'","session":"'"$_SESSION_ID"'"}' 2>/dev/null & -_HAS_ROUTING="no" -for _RF in AGENTS.md AGENTS.md; do - if [ -f "$_RF" ] && grep -q "## Skill routing" "$_RF" 2>/dev/null; then - _HAS_ROUTING="yes" - fi -done -_ROUTING_DECLINED=$($GSTACK_BIN/gstack-config get routing_declined 2>/dev/null || echo "false") -echo "HAS_ROUTING: $_HAS_ROUTING" -echo "ROUTING_DECLINED: $_ROUTING_DECLINED" -_VENDORED="no" -if [ -d ".agents/skills/gstack" ] && [ ! -L ".agents/skills/gstack" ]; then - if [ -f ".agents/skills/gstack/VERSION" ] || [ -d ".agents/skills/gstack/.git" ]; then - _VENDORED="yes" - fi -fi -echo "VENDORED_GSTACK: $_VENDORED" -echo "MODEL_OVERLAY: gpt" -_CHECKPOINT_MODE=$($GSTACK_BIN/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$($GSTACK_BIN/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 +_SS="$GSTACK_BIN/gstack-skill-start" +[ -x "$_SS" ] || _SS=".agents/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "ship" --model "gpt" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -451,110 +352,10 @@ Before calling AskUserQuestion, verify: ## 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="$GSTACK_BIN/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$GSTACK_BIN/gstack-config" - -# /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 AGENTS.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) - -# 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" '((.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' "$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 -``` - - +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`). 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: @@ -569,20 +370,12 @@ 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 +$GSTACK_BIN/gstack-config set artifacts_sync_mode +$GSTACK_BIN/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 -"$GSTACK_BIN/gstack-brain-sync" --discover-new 2>/dev/null || true -"$GSTACK_BIN/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (gpt) The following nudges are tuned for the gpt model family. They are @@ -801,36 +594,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) -$GSTACK_ROOT/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 $GSTACK_ROOT/bin/gstack-telemetry-log ]; then - $GSTACK_ROOT/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 +$GSTACK_BIN/gstack-skill-end --skill "ship" --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/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md index 6f0a3d0d4..cdfd3f274 100644 --- a/test/fixtures/golden/factory-ship-SKILL.md +++ b/test/fixtures/golden/factory-ship-SKILL.md @@ -21,121 +21,22 @@ GSTACK_ROOT="$HOME/.factory/skills/gstack" GSTACK_BIN="$GSTACK_ROOT/bin" GSTACK_BROWSE="$GSTACK_ROOT/browse/dist" GSTACK_DESIGN="$GSTACK_ROOT/design/dist" -_UPD=$($GSTACK_BIN/gstack-update-check 2>/dev/null || .factory/skills/gstack/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=$($GSTACK_BIN/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=$($GSTACK_BIN/gstack-config get skill_prefix 2>/dev/null || echo "false") -echo "PROACTIVE: $_PROACTIVE" -echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED" -echo "SKILL_PREFIX: $_SKILL_PREFIX" -source <($GSTACK_BIN/gstack-repo-mode 2>/dev/null) || true -REPO_MODE=${REPO_MODE:-unknown} -echo "REPO_MODE: $REPO_MODE" -_SESSION_KIND=$($GSTACK_BIN/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=$($GSTACK_BIN/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=$($GSTACK_BIN/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=$($GSTACK_BIN/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=$($GSTACK_BIN/gstack-config get question_tuning 2>/dev/null || echo "false") -echo "QUESTION_TUNING: $_QUESTION_TUNING" -_UPDATE_CHECK=$($GSTACK_BIN/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":"ship","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 "$GSTACK_BIN/gstack-telemetry-log" ]; then - $GSTACK_BIN/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 "$($GSTACK_BIN/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 - $GSTACK_BIN/gstack-learnings-search --limit 3 2>/dev/null || true - fi -else - echo "LEARNINGS: 0" -fi -$GSTACK_BIN/gstack-timeline-log '{"skill":"ship","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=$($GSTACK_BIN/gstack-config get routing_declined 2>/dev/null || echo "false") -echo "HAS_ROUTING: $_HAS_ROUTING" -echo "ROUTING_DECLINED: $_ROUTING_DECLINED" -_VENDORED="no" -if [ -d ".factory/skills/gstack" ] && [ ! -L ".factory/skills/gstack" ]; then - if [ -f ".factory/skills/gstack/VERSION" ] || [ -d ".factory/skills/gstack/.git" ]; then - _VENDORED="yes" - fi -fi -echo "VENDORED_GSTACK: $_VENDORED" -echo "MODEL_OVERLAY: claude" -_CHECKPOINT_MODE=$($GSTACK_BIN/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit") -_CHECKPOINT_PUSH=$($GSTACK_BIN/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 +_SS="$GSTACK_BIN/gstack-skill-start" +[ -x "$_SS" ] || _SS=".factory/skills/gstack/bin/gstack-skill-start" +"$_SS" --skill "ship" --model "claude" --parent-pid "$PPID" \ + || 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. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -453,110 +354,10 @@ Before calling AskUserQuestion, verify: ## 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="$GSTACK_BIN/gstack-brain-sync" -_BRAIN_CONFIG_BIN="$GSTACK_BIN/gstack-config" - -# /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 - -_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" '((.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' "$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 -``` - - +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`). 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: @@ -571,20 +372,12 @@ 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 +$GSTACK_BIN/gstack-config set artifacts_sync_mode +$GSTACK_BIN/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 -"$GSTACK_BIN/gstack-brain-sync" --discover-new 2>/dev/null || true -"$GSTACK_BIN/gstack-brain-sync" --once 2>/dev/null || true -``` - - ## Model-Specific Behavioral Patch (claude) The following nudges are tuned for the claude model family. They are @@ -781,36 +574,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) -$GSTACK_ROOT/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 $GSTACK_ROOT/bin/gstack-telemetry-log ]; then - $GSTACK_ROOT/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 +$GSTACK_BIN/gstack-skill-end --skill "ship" --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