diff --git a/bin/gstack-codex-probe b/bin/gstack-codex-probe index 940dacf842..b77f1e6822 100755 --- a/bin/gstack-codex-probe +++ b/bin/gstack-codex-probe @@ -55,12 +55,16 @@ _gstack_codex_timeout_wrapper() { # Resolve wrapper binary: prefer gtimeout (Homebrew coreutils on macOS), # fall back to timeout (Linux), else run unwrapped. Arguments: $1 is the # duration in seconds; rest is the command to run. + # + # --kill-after=10: timeout sends SIGTERM at $_duration, then SIGKILL 10s later + # if the child is still alive. A plain `timeout $_duration` can't reap a child + # that traps/ignores SIGTERM; the escalation guarantees the wrapper fires. local _duration="$1" shift local _to _to=$(command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || echo "") if [ -n "$_to" ]; then - "$_to" "$_duration" "$@" + "$_to" --kill-after=10 "$_duration" "$@" else "$@" fi diff --git a/bin/gstack-gemini-probe b/bin/gstack-gemini-probe new file mode 100755 index 0000000000..72a9fb2e19 --- /dev/null +++ b/bin/gstack-gemini-probe @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# gstack-gemini-probe: shared helper for /gemini and outside-voice skills. +# Sourced from template bash blocks; never execute directly. +# +# Functions (all prefixed with _gstack_gemini_ for namespace hygiene): +# _gstack_gemini_auth_probe — auth check (login state + ~/.gemini/ files) +# _gstack_gemini_version_check — warn on known-bad gemini CLI versions +# _gstack_gemini_timeout_wrapper — gtimeout -> timeout -> unwrapped fallback +# _gstack_gemini_log_event — telemetry emission to ~/.gstack/analytics/ +# +# Hygiene rules (mirror gstack-codex-probe): +# - Never set -e / set -u / trap / IFS= / PATH= in this file. +# - All internal vars prefix with _GSTACK_GEMINI_. +# - All functions prefix with _gstack_gemini_. +# - No command execution at source time (only function defs). + +# --- Auth probe ------------------------------------------------------------- + +_gstack_gemini_auth_probe() { + # Gemini CLI authenticates via Google OAuth (interactive browser flow on + # first run) and stores tokens under one of: + # ~/.gemini/ (newer CLI default) + # ~/.config/google-gemini/ (alternate location) + # ~/.config/gemini/ (older alternate) + # Or via env var GEMINI_API_KEY for headless / CI use. + # + # We probe in order: env var, then any of the credential file locations. + # Multi-signal so we don't false-negative for users authenticated via env. + local _k1 + _k1=$(printf '%s' "${GEMINI_API_KEY:-}" | tr -d '[:space:]') + if [ -n "$_k1" ]; then + echo "AUTH_OK" + return 0 + fi + if [ -d "$HOME/.gemini" ] && [ -n "$(ls -A "$HOME/.gemini" 2>/dev/null)" ]; then + echo "AUTH_OK" + return 0 + fi + if [ -d "$HOME/.config/google-gemini" ] && [ -n "$(ls -A "$HOME/.config/google-gemini" 2>/dev/null)" ]; then + echo "AUTH_OK" + return 0 + fi + if [ -d "$HOME/.config/gemini" ] && [ -n "$(ls -A "$HOME/.config/gemini" 2>/dev/null)" ]; then + echo "AUTH_OK" + return 0 + fi + echo "AUTH_FAILED" + return 1 +} + +# --- Version check ---------------------------------------------------------- + +_gstack_gemini_version_check() { + # Warn on known-bad gemini CLI versions. Anchored regex prevents false + # positives. Update this list when a new gemini CLI version regresses. + # + # Currently no known-bad releases; entries should be added when issues + # are discovered (e.g., stdin deadlocks, auth regressions, breaking + # output-format changes). + local _ver + _ver=$(gemini --version 2>/dev/null | head -1) + [ -z "$_ver" ] && return 0 + # Example pattern (commented out — fill in when needed): + # if echo "$_ver" | grep -Eq '(^|[^0-9.])0\.38\.(0)([^0-9.]|$)'; then + # echo "WARN: gemini CLI $_ver has known issues. Upgrade: npm install -g @google/gemini-cli@latest" + # _gstack_gemini_log_event "gemini_version_warning" + # fi + return 0 +} + +# --- Timeout wrapper -------------------------------------------------------- + +_gstack_gemini_timeout_wrapper() { + # Resolve wrapper binary: prefer gtimeout (Homebrew coreutils on macOS), + # fall back to timeout (Linux), else run unwrapped. Arguments: $1 is the + # duration in seconds; rest is the command to run. + # + # --kill-after=10: at $_duration, timeout sends SIGTERM; if the child is still + # alive 10s later it sends SIGKILL. The gemini node CLI IGNORES SIGTERM, so a + # plain `timeout $_duration` never reaps a hung review (observed: ran 553s past + # a 330s cap). The escalation to SIGKILL guarantees the wrapper actually fires. + local _duration="$1" + shift + local _to + _to=$(command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || echo "") + if [ -n "$_to" ]; then + "$_to" --kill-after=10 "$_duration" "$@" + else + "$@" + fi +} + +# --- Telemetry event -------------------------------------------------------- + +_gstack_gemini_log_event() { + # Emit a telemetry event to ~/.gstack/analytics/skill-usage.jsonl. + # Gated on $_TEL != "off" (caller sets this from gstack-config). + # Event types: gemini_timeout, gemini_auth_failed, gemini_cli_missing, + # gemini_version_warning. + # Payload schema: {skill, event, duration_s, ts}. NEVER includes prompt + # content, env var values, or auth tokens. + local _event="$1" + local _duration="${2:-0}" + [ "${_TEL:-off}" = "off" ] && return 0 + local _ts + _ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) + mkdir -p "$HOME/.gstack/analytics" 2>/dev/null + printf '{"skill":"gemini","event":"%s","duration_s":"%s","ts":"%s"}\n' \ + "$_event" "$_duration" "$_ts" \ + >> "$HOME/.gstack/analytics/skill-usage.jsonl" 2>/dev/null || true +} + +# --- Hang detection helper -------------------------------------------------- + +_gstack_gemini_log_hang() { + # Called when timeout fires. Emits a structured event for postmortem. + local _mode="$1" # review | challenge | consult + local _stderr_bytes="$2" # bytes captured before timeout + _gstack_gemini_log_event "gemini_hang_${_mode}" "$_stderr_bytes" +} diff --git a/bin/gstack-review-sandbox b/bin/gstack-review-sandbox new file mode 100755 index 0000000000..13c0d7103b --- /dev/null +++ b/bin/gstack-review-sandbox @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# gstack-review-sandbox — sourceable helpers for full-context, sandboxed, +# timeout-robust cross-model reviews (codex / gemini). +# +# WHY: a diff is too small a lens — reviewers must read the whole repo (and the +# web) or they manufacture confident-wrong "breaking change / field missing" +# findings (the proving context is in unchanged code, absent from the diff). +# Codex gets this via `-s read-only`; gemini has no read-only-with-tools mode +# (its `--approval-mode plan` disables tools entirely), so gemini must run +# tool-enabled inside a throwaway git worktree off a clean, pushed branch — the +# worktree + hard timeout + guaranteed cleanup is what makes the unsafe mode +# safe. The headless tool path is also flaky, so every call is timeout-bounded +# with a stall watchdog and one bounded retry. +# +# Public API: +# gstack_review_setup_worktree -> sets GR_WT GR_TIP GR_OUTDIR GR_PGIDFILE +# gstack_run_reviewer -- +# -> prints ":" +# findings in $GR_OUTDIR/.out +# gstack_review_cleanup -> trap handler (idempotent) +# +# STATUS: OK (output) | CLEAN (exit 0, no output) | TIMEOUT (hung/stalled/killed) +# | CRASH (nonzero exit / OOM) +# +# Robustness: each reviewer runs in its OWN session (setsid) so the whole +# process tree is killable; `timeout --kill-after` converts a lockup into a +# bounded exit (TERM, then KILL); a stall watchdog kills early when output +# stops growing. PGIDs are tracked in a FILE (flock-appended) so tracking +# survives command-substitution, backgrounding and concurrency — cleanup reads +# that file and kills every surviving group on EVERY exit path, then removes +# the worktree. A reviewer that deliberately `setsid`-escapes its own group is +# an accepted edge (codex/gemini don't in normal use); the worktree removal is +# the file-level backstop. +# +# USAGE (self-contained orchestrator — run both reviewers concurrently in ONE +# process so the EXIT trap reaps everything): +# source gstack-review-sandbox +# trap gstack_review_cleanup EXIT INT TERM +# gstack_review_setup_worktree "$REPO" "$BRANCH" || exit 1 +# gstack_run_reviewer codex 480 180 1 -- codex exec "$P" -C "$GR_WT" -s read-only ... >"$GR_OUTDIR/codex.status" & +# gstack_run_reviewer gemini 480 180 1 -- gemini -y -p "$P" ...(cwd=$GR_WT)... >"$GR_OUTDIR/gemini.status" & +# wait +# # read $GR_OUTDIR/{codex,gemini}.{status,out}; synthesize. Trap cleans up. + +GR_WT="" +GR_TIP="" +GR_REPO="" +GR_OUTDIR="" +GR_PGIDFILE="" + +_gstack_review_timeout_bin() { + command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || true +} + +_gstack_review_track_pgid() { + # Append a PGID to the shared file under an flock so concurrent reviewers + # don't interleave. The file (not a shell array) is the source of truth so + # tracking survives $(...) subshells and backgrounding. + local pgid="$1" + [ -n "$pgid" ] && [ -n "$GR_PGIDFILE" ] || return 0 + ( flock 9; printf '%s\n' "$pgid" >>"$GR_PGIDFILE" ) 9>"${GR_PGIDFILE}.lock" 2>/dev/null || \ + printf '%s\n' "$pgid" >>"$GR_PGIDFILE" 2>/dev/null || true +} + +gstack_review_setup_worktree() { + local repo="$1" branch="$2" + if ! git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "gstack-review-sandbox: '$repo' is not a git repo" >&2; return 1 + fi + if [ -n "$(git -C "$repo" status --porcelain 2>/dev/null)" ]; then + echo "gstack-review-sandbox: '$repo' has uncommitted changes — commit/push first" >&2; return 1 + fi + GR_TIP="$(git -C "$repo" rev-parse "$branch" 2>/dev/null)" || { + echo "gstack-review-sandbox: cannot resolve branch '$branch'" >&2; return 1; } + if git -C "$repo" rev-parse "$branch@{upstream}" >/dev/null 2>&1; then + local ahead; ahead=$(git -C "$repo" rev-list --count "$branch@{upstream}..$branch" 2>/dev/null || echo 0) + [ "${ahead:-0}" != "0" ] && echo "gstack-review-sandbox: WARN '$branch' is $ahead commit(s) ahead of upstream (not pushed)" >&2 + else + echo "gstack-review-sandbox: WARN '$branch' has no upstream (not pushed)" >&2 + fi + GR_REPO="$repo" + GR_OUTDIR="$(mktemp -d "${TMPDIR:-/tmp}/gstack-review-out-XXXXXX")" + GR_PGIDFILE="$GR_OUTDIR/.pgids"; : > "$GR_PGIDFILE" + GR_WT="$(mktemp -d "${TMPDIR:-/tmp}/gstack-review-wt-XXXXXX")/wt" + if ! git -C "$repo" worktree add --detach "$GR_WT" "$GR_TIP" >/dev/null 2>&1; then + echo "gstack-review-sandbox: failed to create review worktree" >&2; GR_WT=""; return 1 + fi + printf '%s\n' "$GR_WT" +} + +# Internal: run the command once, fully bounded. Echoes the STATUS token. +_gstack_review_run_once() { + local name="$1" hard="$2" stall="$3" out="$4" err="$5"; shift 5 + local pidf rcf to launch pgid rc + pidf="$(mktemp)"; rcf="$(mktemp)" + : > "$out"; : > "$err" + to="$(_gstack_review_timeout_bin)" + + # Launch in a NEW session (setsid) so the whole tree shares one PGID. The + # bash wrapper records its own PID (== PGID after setsid) and the real exit + # code, so classification is correct whether setsid forks or execs. + if [ -n "$to" ]; then + setsid bash -c 'echo "$$" >"$1"; "$4" --kill-after=10 "$3" "${@:5}"; echo "$?" >"$2"' \ + _ "$pidf" "$rcf" "$hard" "$to" "$@" >"$out" 2>"$err" & + else + setsid bash -c 'echo "$$" >"$1"; "${@:3}"; echo "$?" >"$2"' \ + _ "$pidf" "$rcf" "$@" >"$out" 2>"$err" & + fi + launch=$! + + pgid=""; local i=0 + while [ -z "$pgid" ] && [ "$i" -lt 40 ]; do + pgid="$(cat "$pidf" 2>/dev/null)"; [ -n "$pgid" ] && break + sleep 0.05; i=$((i+1)) + done + _gstack_review_track_pgid "$pgid" + + local killed_stall=0 last_size=-1 stall_acc=0 tick=3 + while :; do + [ -s "$rcf" ] && break + if [ -n "$pgid" ]; then + kill -0 "-$pgid" 2>/dev/null || break + else + kill -0 "$launch" 2>/dev/null || break + fi + sleep "$tick" + if [ "$stall" -gt 0 ]; then + local sz; sz="$(stat -c %s "$out" 2>/dev/null || wc -c <"$out" 2>/dev/null || echo 0)" + if [ "$sz" = "$last_size" ]; then + stall_acc=$((stall_acc + tick)) + if [ "$stall_acc" -ge "$stall" ]; then + [ -n "$pgid" ] && kill -KILL "-$pgid" 2>/dev/null + killed_stall=1; break + fi + else + last_size="$sz"; stall_acc=0 + fi + fi + done + wait "$launch" 2>/dev/null || true + rc="$(cat "$rcf" 2>/dev/null || echo "")" + rm -f "$pidf" "$rcf" + + if [ "$killed_stall" = 1 ]; then echo "TIMEOUT"; return 0; fi + case "$rc" in + 0) if [ -s "$out" ]; then echo "OK"; else echo "CLEAN"; fi ;; + 124|137) echo "TIMEOUT" ;; + "") echo "TIMEOUT" ;; + *) echo "CRASH" ;; + esac +} + +gstack_run_reviewer() { + local name="$1" hard="$2" stall="$3" retries="$4"; shift 4 + [ "$1" = "--" ] && shift + local out="$GR_OUTDIR/$name.out" err="$GR_OUTDIR/$name.err" + local attempt=0 status="" + while :; do + attempt=$((attempt + 1)) + status="$(_gstack_review_run_once "$name" "$hard" "$stall" "$out" "$err" "$@")" + case "$status" in + OK|CLEAN) break ;; + TIMEOUT|CRASH) + [ "$attempt" -gt "$retries" ] && break + echo "gstack-review-sandbox: $name $status (attempt $attempt) — retrying" >&2 + ;; + esac + done + printf '%s:%s\n' "$name" "$status" +} + +gstack_review_cleanup() { + # Kill every tracked process group FIRST (read from the file — survives + # subshells/concurrency), so nothing holds the worktree busy, then discard + + # remove the worktree. Idempotent. + if [ -n "$GR_PGIDFILE" ] && [ -f "$GR_PGIDFILE" ]; then + local g + while IFS= read -r g; do + [ -n "$g" ] && kill -KILL "-$g" 2>/dev/null || true + done < "$GR_PGIDFILE" + : > "$GR_PGIDFILE" + fi + if [ -n "$GR_WT" ] && [ -d "$GR_WT" ]; then + local dirty; dirty="$(git -C "$GR_WT" status --porcelain 2>/dev/null)" + if [ -n "$dirty" ]; then + echo "gstack-review-sandbox: reviewer wrote to the worktree (discarding):" >&2 + printf '%s\n' "$dirty" | sed 's/^/ /' >&2 + fi + if [ -n "$GR_REPO" ]; then + git -C "$GR_REPO" worktree remove --force "$GR_WT" >/dev/null 2>&1 || rm -rf "$(dirname "$GR_WT")" + else + rm -rf "$(dirname "$GR_WT")" + fi + GR_WT="" + fi +} diff --git a/bin/gstack-update-check b/bin/gstack-update-check index d0486cb4c6..7814aa9449 100755 --- a/bin/gstack-update-check +++ b/bin/gstack-update-check @@ -19,8 +19,8 @@ CACHE_FILE="$STATE_DIR/last-update-check" MARKER_FILE="$STATE_DIR/just-upgraded-from" SNOOZE_FILE="$STATE_DIR/update-snoozed" VERSION_FILE="$GSTACK_DIR/VERSION" -REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}" -REMOTE_REPO="${GSTACK_REMOTE_REPO:-https://github.com/garrytan/gstack.git}" +REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/swxtchio/gstack/swxtch/VERSION}" +REMOTE_REPO="${GSTACK_REMOTE_REPO:-https://github.com/swxtchio/gstack.git}" # ─── Force flag (busts cache + snooze for standalone /gstack-upgrade) ── if [ "${1:-}" = "--force" ]; then @@ -194,10 +194,10 @@ if [ -z "${GSTACK_REMOTE_URL:-}" ]; then # Disable credential prompts and apply a 5-second low-speed timeout so a # flaky network or captive portal can't hang every skill preamble. _LSR_LINE="$(GIT_TERMINAL_PROMPT=0 GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 \ - git ls-remote "$REMOTE_REPO" refs/heads/main 2>/dev/null || true)" + git ls-remote "$REMOTE_REPO" refs/heads/swxtch 2>/dev/null || true)" _REMOTE_SHA="$(echo "$_LSR_LINE" | awk '{print $1}')" if echo "$_REMOTE_SHA" | grep -qE '^[0-9a-f]{40}$'; then - _SHA_URL="https://raw.githubusercontent.com/garrytan/gstack/${_REMOTE_SHA}/VERSION" + _SHA_URL="https://raw.githubusercontent.com/swxtchio/gstack/${_REMOTE_SHA}/VERSION" REMOTE="$(curl -sf --max-time 5 "$_SHA_URL" 2>/dev/null || true)" fi fi diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl index 333de7d8d5..4b551f2fdf 100644 --- a/codex/SKILL.md.tmpl +++ b/codex/SKILL.md.tmpl @@ -156,6 +156,17 @@ mode (persona prompt). Reference this section as "the filesystem boundary" below Run Codex code review against the current branch diff. +**Note — full-context is already Codex's strength.** Unlike gemini, `codex exec` +/`codex review` run in a real read-only-but-tool-enabled sandbox (`-s read-only`), +so Codex already reads the whole repo + web, not just the diff. For +timeout-robust, clean-snapshot review (review a clean+pushed worktree instead of +a dirty working tree, with a hard timeout + stall watchdog + auto-cleanup), wrap +the invocation in the shared harness — see `~/.claude/skills/gstack/bin/gstack-review-sandbox` +and the orchestrator example in the fix-and-ship skill: +`gstack_run_reviewer codex 480 180 1 -- codex exec "$PROMPT" -C "$GR_WT" -s read-only …`. +The default path below is the standalone (non-orchestrated) invocation; it keeps +its own `_gstack_codex_timeout_wrapper`. + 1. Create temp files for output capture: ```bash TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt") diff --git a/gemini/SKILL.md b/gemini/SKILL.md new file mode 100644 index 0000000000..99e1a71b67 --- /dev/null +++ b/gemini/SKILL.md @@ -0,0 +1,1470 @@ +--- +name: gemini +preamble-tier: 3 +version: 1.0.0 +description: | + Google Gemini CLI wrapper — three modes mirroring /codex. + Code review: independent diff review with pass/fail gate. + Challenge: adversarial mode that tries to break your code. + Consult: ask gemini anything with session continuity for follow-ups. + Architecturally divergent from Claude (different training paradigm), so + agreement = stronger signal and disagreement = better blind-spot coverage. + Use when asked to "gemini review", "gemini challenge", "ask gemini", + "second opinion", or "consult gemini". (gstack) + Voice triggers (speech-to-text aliases): "ask gemini", "google second opinion", "gem in eye". +triggers: + - gemini review + - gemini challenge + - ask gemini + - consult gemini + - second opinion gemini +allowed-tools: + - Bash + - Read + - Write + - Glob + - Grep + - AskUserQuestion +--- + + + +## 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" +_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" +mkdir -p ~/.gstack/analytics +if [ "$_TEL" != "off" ]; then +echo '{"skill":"gemini","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "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 "~/.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":"gemini","event":"started","branch":"'"$_BRANCH"'","session":"'"$_SESSION_ID"'"}' 2>/dev/null & +_HAS_ROUTING="no" +if [ -f CLAUDE.md ] && grep -q "## Skill routing" CLAUDE.md 2>/dev/null; then + _HAS_ROUTING="yes" +fi +_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" +[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true +``` + +## 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. + +## Skill Invocation During Plan Mode + +If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no variant is callable, fall back to writing the decision brief into the plan file as a `## Decisions to confirm` section + ExitPlanMode — never silently auto-decide. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode. + +If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?" + +If `SKILL_PREFIX` is `"true"`, suggest/invoke `/gstack-*` names. Disk paths stay `~/.claude/skills/gstack/[skill-name]/SKILL.md`. + +If output shows `UPGRADE_AVAILABLE `: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). + +If output shows `JUST_UPGRADED `: print "Running gstack v{to} (just updated!)". If `SPAWNED_SESSION` is true, skip feature discovery. + +Feature discovery, max one prompt per session: +- Missing `~/.claude/skills/gstack/.feature-prompted-continuous-checkpoint`: AskUserQuestion for Continuous checkpoint auto-commits. If accepted, run `~/.claude/skills/gstack/bin/gstack-config set checkpoint_mode continuous`. Always touch marker. +- Missing `~/.claude/skills/gstack/.feature-prompted-model-overlay`: inform "Model overlays are active. MODEL_OVERLAY shows the patch." Always touch marker. + +After upgrade prompts, continue workflow. + +If `WRITING_STYLE_PENDING` is `yes`: ask once about writing style: + +> v1 prompts are simpler: first-use jargon glosses, outcome-framed questions, shorter prose. Keep default or restore terse? + +Options: +- A) Keep the new default (recommended — good writing helps everyone) +- B) Restore V0 prose — set `explain_level: terse` + +If A: leave `explain_level` unset (defaults to `default`). +If B: run `~/.claude/skills/gstack/bin/gstack-config set explain_level terse`. + +Always run (regardless of choice): +```bash +rm -f ~/.gstack/.writing-style-prompt-pending +touch ~/.gstack/.writing-style-prompted +``` + +Skip if `WRITING_STYLE_PENDING` is `no`. + +If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open: + +```bash +open https://garryslist.org/posts/boil-the-ocean +touch ~/.gstack/.completeness-intro-seen +``` + +Only run `open` if yes. Always run `touch`. + +If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: ask telemetry once via AskUserQuestion: + +> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code, file paths, or repo names. + +Options: +- A) Help gstack get better! (recommended) +- B) No thanks + +If A: run `~/.claude/skills/gstack/bin/gstack-config set telemetry community` + +If B: ask follow-up: + +> Anonymous mode sends only aggregate usage, no unique ID. + +Options: +- A) Sure, anonymous is fine +- B) No thanks, fully off + +If B→A: run `~/.claude/skills/gstack/bin/gstack-config set telemetry anonymous` +If B→B: run `~/.claude/skills/gstack/bin/gstack-config set telemetry off` + +Always run: +```bash +touch ~/.gstack/.telemetry-prompted +``` + +Skip if `TEL_PROMPTED` is `yes`. + +If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: ask once: + +> Let gstack proactively suggest skills, like /qa for "does this work?" or /investigate for bugs? + +Options: +- A) Keep it on (recommended) +- B) Turn it off — I'll type /commands myself + +If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true` +If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false` + +Always run: +```bash +touch ~/.gstack/.proactive-prompted +``` + +Skip if `PROACTIVE_PROMPTED` is `yes`. + +If `HAS_ROUTING` is `no` AND `ROUTING_DECLINED` is `false` AND `PROACTIVE_PROMPTED` is `yes`: +Check if a CLAUDE.md file exists in the project root. If it does not exist, create it. + +Use AskUserQuestion: + +> gstack works best when your project's CLAUDE.md includes skill routing rules. + +Options: +- A) Add routing rules to CLAUDE.md (recommended) +- B) No thanks, I'll invoke skills manually + +If A: Append this section to the end of CLAUDE.md: + +```markdown + +## Skill routing + +When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill. + +Key routing rules: +- Product ideas/brainstorming → invoke /office-hours +- Strategy/scope → invoke /plan-ceo-review +- Architecture → invoke /plan-eng-review +- Design system/plan review → invoke /design-consultation or /plan-design-review +- Full review pipeline → invoke /autoplan +- Bugs/errors → invoke /investigate +- QA/testing site behavior → invoke /qa or /qa-only +- Code review/diff check → invoke /review +- Visual polish → invoke /design-review +- Ship/deploy/PR → invoke /ship or /land-and-deploy +- Save progress → invoke /context-save +- Resume context → invoke /context-restore +``` + +Then commit the change: `git add CLAUDE.md && git commit -m "chore: add gstack skill routing rules to CLAUDE.md"` + +If B: run `~/.claude/skills/gstack/bin/gstack-config set routing_declined true` and say they can re-enable with `gstack-config set routing_declined false`. + +This only happens once per project. Skip if `HAS_ROUTING` is `yes` or `ROUTING_DECLINED` is `true`. + +If `VENDORED_GSTACK` is `yes`, warn once via AskUserQuestion unless `~/.gstack/.vendoring-warned-$SLUG` exists: + +> This project has gstack vendored in `.claude/skills/gstack/`. Vendoring is deprecated. +> Migrate to team mode? + +Options: +- A) Yes, migrate to team mode now +- B) No, I'll handle it myself + +If A: +1. Run `git rm -r .claude/skills/gstack/` +2. Run `echo '.claude/skills/gstack/' >> .gitignore` +3. Run `~/.claude/skills/gstack/bin/gstack-team-init required` (or `optional`) +4. Run `git add .claude/ .gitignore CLAUDE.md && git commit -m "chore: migrate gstack from vendored to team mode"` +5. Tell the user: "Done. Each developer now runs: `cd ~/.claude/skills/gstack && ./setup --team`" + +If B: say "OK, you're on your own to keep the vendored copy up to date." + +Always run (regardless of choice): +```bash +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 2>/dev/null || true +touch ~/.gstack/.vendoring-warned-${SLUG:-unknown} +``` + +If marker exists, skip. + +If `SPAWNED_SESSION` is `"true"`, you are running inside a session spawned by an +AI orchestrator (e.g., OpenClaw). In spawned sessions: +- Do NOT use AskUserQuestion for interactive prompts. Auto-choose the recommended option. +- Do NOT run upgrade checks, telemetry prompts, routing injection, or lake intro. +- Focus on completing the task and reporting results via prose output. +- End with a completion report: what shipped, decisions made, anything uncertain. + +## AskUserQuestion Format + +### Tool resolution (read first) + +"AskUserQuestion" can resolve to two tools at runtime: the **host MCP variant** (e.g. `mcp__conductor__AskUserQuestion` — appears in your tool list when the host registers it) or the **native** Claude Code tool. + +**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies. + +**Fallback when neither variant is callable:** in plan mode, write the decision brief into the plan file as a `## Decisions to confirm` section + ExitPlanMode (the native "Ready to execute?" surfaces it). Outside plan mode, output the brief as prose and stop. **Never silently auto-decide** — only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking. + +### Format + +Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose. + +``` +D +Project/branch/task: <1 short grounding sentence using _BRANCH> +ELI10: +Stakes if we pick wrong: +Recommendation: because +Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score) +Pros / cons: +A)