diff --git a/AGENTS.md b/AGENTS.md index 4df7eec35f..b75a142537 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,8 @@ Invoke them by name (e.g., `/office-hours`). | `/qa` | Open a real browser, find bugs, fix them, re-verify. | | `/qa-only` | Same methodology as /qa but report only — no code changes. | | `/scrape` | Pull data from a web page. First call prototypes; codified call runs in ~200ms. | -| `/skillify` | Codify the most recent successful `/scrape` flow into a permanent browser-skill. | +| `/automate` | Automate multi-step mutating flows on a page. First call prototypes with strict gates; codified call runs in ~200ms. | +| `/skillify` | Codify the most recent successful `/scrape` or `/automate` flow into a permanent browser-skill. | ### Release + deploy diff --git a/CLAUDE.md b/CLAUDE.md index 9848449020..7e8af9440e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,7 +64,7 @@ variants to force all tests. Run `eval:select` to preview which tests would run. **Two-tier system:** Tests are classified as `gate` or `periodic` in `E2E_TIERS` (in `test/helpers/touchfiles.ts`). CI runs only gate tests (`EVALS_TIER=gate`); -periodic tests run weekly via cron or manually. Use `EVALS_TIER=gate` or +a curated subset of periodic tests run weekly via cron to manage API spend, while the rest run manually. Use `EVALS_TIER=gate` or `EVALS_TIER=periodic` to filter. When adding new E2E tests, classify them: 1. Safety guardrail or deterministic functional test? -> `gate` 2. Quality benchmark, Opus model test, or non-deterministic? -> `periodic` diff --git a/SKILL.md b/SKILL.md index aaa5612dd4..cc926e4d6b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -27,24 +27,30 @@ 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) +_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) +GSTACK_ROOT="$HOME/.claude/skills/gstack" +[ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ] && GSTACK_ROOT="$_ROOT/.claude/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 || .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=$($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") +_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 <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true +source <($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") +_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 @@ -62,21 +68,21 @@ echo "FIRST_LOOP_SHOWN: $_FIRST_LOOP_SHOWN" # (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) + _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=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true) +_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=$(~/.claude/skills/gstack/bin/gstack-config get explain_level 2>/dev/null || echo "default") +_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=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false") +_QUESTION_TUNING=$($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 @@ -84,30 +90,30 @@ echo '{"skill":"gstack","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo 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 + 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 "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 2>/dev/null || true +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 - ~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true + $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 & +$GSTACK_BIN/gstack-timeline-log '{"skill":"gstack","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") +_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" @@ -118,8 +124,8 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then 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") +_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. @@ -148,15 +154,15 @@ If the user invokes a skill in plan mode, the skill takes precedence over generi 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 `SKILL_PREFIX` is `"true"`, suggest/invoke `/gstack-*` names. Disk paths stay `$GSTACK_ROOT/[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 `UPGRADE_AVAILABLE `: read `$GSTACK_ROOT/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. +- Missing `$GSTACK_ROOT/.feature-prompted-continuous-checkpoint`: AskUserQuestion for Continuous checkpoint auto-commits. If accepted, run `$GSTACK_BIN/gstack-config set checkpoint_mode continuous`. Always touch marker. +- Missing `$GSTACK_ROOT/.feature-prompted-model-overlay`: inform "Model overlays are active. MODEL_OVERLAY shows the patch." Always touch marker. After upgrade prompts, continue workflow. @@ -169,7 +175,7 @@ Options: - 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`. +If B: run `$GSTACK_BIN/gstack-config set explain_level terse`. Always run (regardless of choice): ```bash @@ -196,7 +202,7 @@ Options: - A) Help gstack get better! (recommended) - B) No thanks -If A: run `~/.claude/skills/gstack/bin/gstack-config set telemetry community` +If A: run `$GSTACK_BIN/gstack-config set telemetry community` If B: ask follow-up: @@ -206,8 +212,8 @@ 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` +If B→A: run `$GSTACK_BIN/gstack-config set telemetry anonymous` +If B→B: run `$GSTACK_BIN/gstack-config set telemetry off` Always run: ```bash @@ -224,8 +230,8 @@ 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` +If A: run `$GSTACK_BIN/gstack-config set proactive true` +If B: run `$GSTACK_BIN/gstack-config set proactive false` Always run: ```bash @@ -238,7 +244,7 @@ Skip if `PROACTIVE_PROMPTED` is `yes`. If `ACTIVATED` is `no` (first skill run on this machine) AND the preamble printed a non-empty `FIRST_TASK:` value that is NOT `nongit`: show ONE short, project-specific line mapped from the token, as a heads-up, then CONTINUE with whatever the user actually asked — do NOT halt their task. Map the token: `greenfield` → "Fresh repo — shape it first with `/spec` or `/office-hours`." `code_node`/`code_python`/`code_rust`/`code_go`/`code_ruby`/`code_ios` → "There's code here — `/qa` to see it work, or `/investigate` if something's off." `branch_ahead` → "Unshipped work on this branch — `/review` then `/ship`." `dirty_default` → "Uncommitted changes — `/review` before committing." `clean_default` → "Pick one: `/spec`, `/investigate`, or `/qa`." Then substitute the token you saw for TASK_TOKEN and run (best-effort), and mark activated: ```bash -~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type first_task_scaffold_shown --skill "TASK_TOKEN" --outcome shown 2>/dev/null || true +$GSTACK_BIN/gstack-telemetry-log --event-type first_task_scaffold_shown --skill "TASK_TOKEN" --outcome shown 2>/dev/null || true touch ~/.gstack/.activated 2>/dev/null || true ``` @@ -289,7 +295,7 @@ Key routing rules: 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`. +If B: run `$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`. @@ -305,15 +311,15 @@ Options: 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`) +3. Run `$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`" +5. Tell the user: "Done. Each developer now runs: `cd $GSTACK_ROOT && ./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 +eval "$($GSTACK_BIN/gstack-slug 2>/dev/null)" 2>/dev/null || true touch ~/.gstack/.vendoring-warned-${SLUG:-unknown} ``` @@ -337,8 +343,8 @@ if [ -f "$HOME/.gstack-artifacts-remote.txt" ]; then else _BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt" fi -_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync" -_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config" +_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 @@ -447,8 +453,8 @@ If A/B and `~/.gstack/.git` is missing, ask whether to run `gstack-artifacts-ini At skill END before telemetry: ```bash -"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true -"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true +"$GSTACK_BIN/gstack-brain-sync" --discover-new 2>/dev/null || true +"$GSTACK_BIN/gstack-brain-sync" --once 2>/dev/null || true ``` @@ -493,7 +499,7 @@ Escalate after 3 failed attempts, uncertain security-sensitive changes, or scope Before completing, if you discovered a durable project quirk or command fix that would save 5+ minutes next time, log it: ```bash -~/.claude/skills/gstack/bin/gstack-learnings-log '{"skill":"SKILL_NAME","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":N,"source":"observed"}' +$GSTACK_BIN/gstack-learnings-log '{"skill":"SKILL_NAME","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":N,"source":"observed"}' ``` Do not log obvious facts or one-time transient errors. @@ -512,14 +518,14 @@ _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 +$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 \ +if [ "$_TEL" != "off" ] && [ -x $GSTACK_BIN/gstack-telemetry-log ]; then + $GSTACK_BIN/gstack-telemetry-log \ --skill "SKILL_NAME" --duration "$_TEL_DUR" --outcome "OUTCOME" \ --used-browse "USED_BROWSE" --session-id "$_SESSION_ID" 2>/dev/null & fi diff --git a/TODOS.md b/TODOS.md index 0b27e403a5..7f7aefa312 100644 --- a/TODOS.md +++ b/TODOS.md @@ -2,177 +2,8 @@ ## NEXT PRIORITY -### P1: #1882 — portable skill-install prefix (non-`gstack` install dirs break silently) - -**What:** Every generated SKILL.md hardcodes the literal `~/.claude/skills/gstack/...` -for its `bin/`/asset calls (the per-invocation telemetry/config preamble plus ~9 -resolvers). `setup` wires the top-level skill symlinks for any directory name, so -installing at `~/.claude/skills/` leaves every internal `bin` reference -pointing at a non-existent `~/.claude/skills/gstack/` path — failing **silently, at -skill-invocation time**. Make the emitted references portable: resolve the install -root at runtime (the preamble already defines `GSTACK_ROOT`/`GSTACK_BIN` in -`scripts/resolvers/preamble/generate-preamble-bash.ts` but the literals don't use -them) and emit `$GSTACK_BIN`-relative paths instead of the hardcoded prefix. - -**Why:** Filed as #1882. Split out of the June 2026 fix wave (decision A) once -implementation showed it is a host-config/design change, not a fix-wave patch. The -urgent half — the guard/freeze/careful frontmatter hooks broken on CC 2.1.162 — was -already fixed in that wave (#1871) with a literal `$HOME`-anchored path, because -frontmatter hooks run before any runtime variable exists and cannot use `$GSTACK_BIN`. -So #1882 is now purely the body-preamble portability work. - -**Pros:** Unblocks installs at any directory name; removes a whole class of silent -invocation-time failures. -**Cons:** Touches the most load-bearing bash in the repo (every skill's preamble); -a silent mistake breaks all 52 skills. High blast radius — needs its own focused PR. - -**Context / where to start:** -- Rewire `ctx.paths.binDir` (and browse/design dir paths) + the ~9 resolvers that - emit the literal (`testing.ts`, `review.ts`, `design.ts`, `browse.ts`, - `redact-doc.ts`, `tasks-section.ts`, `preamble/generate-*.ts`) to use the - preamble-defined `$GSTACK_ROOT`/`$GSTACK_BIN`. -- Ensure `GSTACK_ROOT`/`GSTACK_BIN` are defined before first use in EVERY skill's - preamble (verify the telemetry preamble's first bin call is after the definition). -- **Test conflict (verified):** `test/gen-skill-docs.test.ts:1942` and the sibling - ship assertion currently *assert* generated Claude output `.toContain('~/.claude/skills/gstack')` - as a guardrail that Codex-host paths don't leak. These must be rewritten to match - the new portable scheme. -- Regenerate all 52 SKILL.md (`bun run scripts/gen-skill-docs.ts --host all`); never - hand-edit generated files. Bisect: resolver/host-config change commit, then the - 52-file regen commit. -- Smoke-test a skill invocation from a non-`gstack` install dir to prove the fix. -- Sibling of #349 (the `$CLAUDE_CONFIG_DIR` / `~/.claude` path issue). - ## Test infrastructure -### P2: Wire `design/test/` into CI (all 8 files are invisible to every runner) - -**What:** Add `design/test/` to the `bun test` glob (`package.json:21`) and -`TEST_ROOTS` (`scripts/test-free-shards.ts:32`) after auditing its 8 files for -server-spawning/flakiness (they were plausibly excluded on purpose). While in -there, fix the known timing flake: `variants-retry-after.test.ts` "HTTP-date: -honors a future date with no extra leading exponential" fails ~1-2 in 9 runs -under parallel suite load (verified pre-existing on v1.58.5.0 during the -June 2026 fix wave — wall-clock assertion with a ~2s window). - -**Why:** Every test in `design/test/` runs only when someone types the path by -hand — a silent coverage hole, the fix wave's theme at meta-level. The wave's -own design tests went into `test/design-flag-utils.test.ts` to dodge this. - -**Pros:** design binary gets CI coverage; kills a latent "we have tests" illusion. -**Cons:** unaudited files may spawn servers or flake; audit first, wire second. - -**Context:** Filed from the June 2026 fix-wave eng review (issue 11 + flake -receipts). Start with the audit: which of the 8 files are hermetic? Wire the -hermetic ones, quarantine or fix the rest. - -**Effort:** S-M (human ~1d, CC ~30min). **Depends on:** None. - -### P2: /context-save worktree-identity hardening (the #2052 residual) - -**What:** Persist a stable worktree identity (path hash or worktree name) into -checkpoint frontmatter at save time; `/context-restore` prefers identity match -over branch-name match. PR #2054 (@jbetala7, absorbed in the June 2026 wave) -fixed restore ORDERING (current-branch first), but branch frontmatter is not a -stable worktree identity: same-name branches across clones/remotes, renamed -branches, and detached HEAD can still restore the wrong checkpoint. - -**Why:** Closes the residual wrong-checkpoint class entirely instead of the -common case. Codex outside-voice concurred during the wave's eng review. - -**Pros:** Eliminates cross-clone checkpoint collisions. -**Cons:** Frontmatter schema change; needs a migration story for old -checkpoints (no-identity checkpoints rank as fallback, like #2054's -no-branch handling). - -**Context:** Filed from the June 2026 fix-wave eng review (NOT-in-scope item). -Start at `context-restore/SKILL.md.tmpl` Step 1 + `/context-save`'s frontmatter -writer; mirror #2054's partition logic with identity as the first key. - -**Effort:** S (human ~4h, CC ~20min). **Depends on:** #2054 (landed in the wave). - -### P3: gbrain reindex-in-place on perpetual drift (conditional — check the drift log first) - -**What:** IF the `[gbrain-sources] drift:` stderr line (added in the June 2026 -wave) shows drift firing on every sync for some environment, implement #1985's -reporter design: refresh an existing source in place with `gbrain reindex-code` -instead of remove+add (which drops and re-embeds the full index — 768 pages / -6,786 embeddings in the reporter's case). - -**Why:** Perpetual drift means paying full re-embed cost every sync. The wave's -`realpathSync` normalization (symlink aliases are a match, not drift) may have -eliminated the drift class entirely — that's why this is conditional. - -**Pros:** Avoids repeated embedding spend for affected environments. -**Cons:** Speculative until the drift log produces evidence; reindex-in-place -has its own consistency questions (stale chunks for deleted files). - -**Context:** Filed from the June 2026 fix-wave eng review (4A observability). -Trigger condition documented in `lib/gbrain-sources.ts` at the drift log line. - -**Effort:** M (human ~1d, CC ~45min). **Depends on:** drift-log evidence from -the wave's `ensureSourceRegistered` logging. -### P1: Free suite exit code is untrustworthy — in-process force-exits mask failures - -**Priority:** P1 - -**What:** At least five browse test files end with `setTimeout(() => process.exit(0), 500)` -(browse/test/commands.test.ts:101, snapshot.test.ts:36, batch.test.ts:47, -handoff.test.ts:31, content-security.test.ts:465). The timer fires inside the SHARED -`bun test` process, exiting 0 before bun prints its final summary — so `bun test` can -report exit 0 while real test failures scrolled by earlier. Remove the force-exits and -fix the underlying handle leaks they paper over (lingering Playwright/daemon handles -that once made the suite hang), or scope the exit to a spawned child process. - -**Why:** Observed 2026-08-07: three genuinely failing tests (eval-list-cli, -benchmark-cli, observability check 11) rode green `bun test` exit codes across -multiple runs; the failures only surfaced by grepping logs for "(fail)" lines. A test -suite that exits 0 on failure is worse than no suite — it manufactures false -confidence at commit time and in any CI job that trusts the exit code. - -**Pros:** Restores the one contract everything (CI, /ship, humans) relies on: exit -code == truth. Also un-hides the missing final summary block. -**Cons:** The force-exits exist because the suite once hung on leaked handles; -removing them without fixing the leaks trades silent failure for hangs. Needs a -focused pass: find each leaked handle (daemon children, PTY, Playwright contexts), -close them in afterAll, then delete the exits one file at a time. - -**Context / where to start:** `grep -rn "process.exit(0)" browse/test/` — the -setTimeout variants are the offenders (server-no-import-side-effects.test.ts:62 is a -spawned-child probe, fine). Repro: run the full free suite and note the log ends at -the browse files with no "Ran N tests" summary. Receipts: -~/.gstack-dev/logs/free-suite-main-check.log (3 masked fails, exit 0). - -### P2: Periodic CI matrix covers 9 of ~66 e2e files — decide the coverage contract - -**Priority:** P2 - -**What:** `evals-periodic.yml` (weekly cron, `EVALS_TIER=periodic EVALS_ALL=1`) runs a -hard-coded 9-file matrix; `evals.yml` gate shards cover 14 files. ~57 `test/skill-e2e-*` -files run in NEITHER workflow — they execute only when a local diff happens to select -them via touchfiles. CLAUDE.md says "periodic tests run weekly via cron," which the -matrix doesn't deliver. Decide: (a) expand the periodic matrix (or glob it) to all -periodic-tier files with a budget cap, (b) shrink the claim in CLAUDE.md and mark the -uncovered files as local-only, or (c) tier the orphans explicitly. - -**Why:** The autoplan-dual-voice E2E was silently broken for months (claude >= 2.x -changed unregistered-slash-command handling) and nothing noticed until a docs PR's -touchfiles happened to select it locally (2026-07-09). Tests that never run anywhere -rot invisibly; each one found broken later costs a full /investigate session. - -**Pros:** Kills the silent-rot class for ~57 test files; makes the CLAUDE.md tiering -claim true. -**Cons:** Full periodic coverage costs real money weekly (rough order: ~$1/file/run); -some orphans are deliberately manual (ios-device, opus-47 overlay harness), so a plain -glob is wrong — needs a curated exclude list. - -**Context / where to start:** `.github/workflows/evals-periodic.yml:71` (matrix), -`test/helpers/touchfiles.ts` E2E_TIERS (tier labels already exist per test), orphan -list generated via `comm -23` between `ls test/skill-e2e-*.test.ts` and the file lists -in `.github/workflows/evals*.yml`. Receipts from the autoplan incident: -`~/.gstack/projects/garrytan-gstack/e2e-runs/2026-07-10-0154/` (0-turn "Unknown command" -transcripts). - ### Eval harness: live progress + incremental result persistence (kill the silent hour) **Priority:** P1 @@ -270,65 +101,6 @@ the `$B memory` diagnostic + the four leak fixes. They were deliberately deferred from that PR (already 14 commits / ~12 files); each stands alone and any one could ship independently. -### P2: MV3 extension service worker memory profile - -**What:** The `/memory` endpoint snapshot enumerates pages but does -not enumerate the gstack baked-in extension's service-worker target. -A long-running MV3 service worker can leak through retained DOM -snapshots, message ports that never close, alarms that re-arm, and -caches that grow without bound. The diagnostic should call -`Target.getTargets` with a filter for `service_worker` and include -each one in `tabs[]` (or a sibling `serviceWorkers[]` array) with the -same `Performance.getMetrics` data. - -**Why:** Codex's outside-voice review on the eng-review surfaced this -class of leak (the extension is part of the gbrowser process tree but -invisible to today's snapshot). Until we surface it, a SW leak shows -up only in the parent process RSS with no per-target attribution. - -**Pros:** Closes the per-target attribution gap for the -single-most-likely future leak source (our own extension). -**Cons:** Extension SW lifecycle is asymmetric vs page lifecycle; -auto-attach + filter is one more piece of CDP plumbing. - -**Context:** Codex finding #4 on the eng-review outside voice. Not -in scope of the v1.49 PR; deliberately deferred to keep the PR to -the four highest-confidence leak fixes. - -**Priority:** P2. **Effort:** M. - ---- - -### P2: Native + GPU memory breakdown in `$B memory` - -**What:** `$B memory` shows Bun RSS + per-tab JS heap + Chromium -process tree (PIDs + types + CPU time) but the per-process RSS is -absent — `SystemInfo.getProcessInfo` doesn't expose RSS and the eng -review (D2 USE_CDP) explicitly chose CDP over shelling to `ps`. The -honest next step is to surface what CDP DOES give for the other -memory categories: `Memory.getDOMCounters` per target (node + listener -counts), `SystemInfo.getInfo` for GPU memory, `Memory.getAllTimeSamplingProfile` -for a sampled native estimate. - -**Why:** Codex's outside-voice review flagged that -`Performance.getMetrics` misses native memory, GPU memory, video -buffers, Skia, network cache, extension process RSS, and -browser-process RSS — all the categories where a 160 GB leak would -actually live. A diagnostic that misses the categories where the -leak class lives undersells itself. - -**Pros:** Per-process category breakdown closes the gap between -"Activity Monitor says 160 GB" and what the diagnostic shows. -**Cons:** Each CDP method has its own quirks; this is a real -implementation pass, not a one-line addition. - -**Context:** Codex finding #5 on the eng-review outside voice. Not -in scope of the v1.49 PR; deliberately deferred. - -**Priority:** P2. **Effort:** M. - ---- - ### P3: Single-context CDP listener for Network.loadingFinished **What:** `wirePageEvents` attaches a `page.on('requestfinished')` @@ -586,24 +358,6 @@ made opt-in. Lower priority than the gbrain-side perf issue above. ## Browser-skills follow-on (Phases 2-4) -### P1: Browser-skills Phase 2 — `/scrape` and `/skillify` skill templates - -**What:** Phase 2a of the browser-skills design (`docs/designs/BROWSER_SKILLS_V1.md`). Two new gstack skills: `/scrape ` (read-only) is the single entry point for pulling page data — first call prototypes via `$B` primitives, subsequent calls on a matching intent route to a codified browser-skill in ~200ms. `/skillify` codifies the most recent successful prototype into a permanent browser-skill on disk: synthesizes `script.ts` + `script.test.ts` + fixture from the agent's own context (final-attempt $B calls only), runs the test in a temp dir, asks before committing, atomic rename to `~/.gstack/browser-skills//`. The mutating-flow sibling `/automate` is split out as its own P0 (below) — same skillify pattern, different trust profile. - -**Why:** Phase 1 shipped the runtime — humans can hand-write deterministic browser scripts that gstack runs. Phase 2a unlocks the productivity gain: an agent that gets a flow right once via 20+ `$B` commands says `/skillify` and the script becomes a 200ms call forever after. Same skillify pattern Garry's articles describe, applied to the read-only browser activity (scraping) most amenable to deterministic compression. Mutating actions ship next as `/automate` because the failure mode (unintended writes) needs stronger gates. - -**Pros:** The 100x productivity gain lives here. Closes the loop: agents prototype, codify, then reach for the codified skill in future sessions instead of re-exploring. Replaces the original "self-authoring `$B` commands" P1 — same user-visible goal, no in-daemon isolation problem (skill scripts run as standalone Bun processes, never imported into the daemon). Synthesis question (Codex finding #6) is resolved by re-prompting from the agent's own conversation context (option b in the design doc), bounded to final-attempt `$B` calls per `/plan-eng-review` D2. - -**Cons:** **Bun runtime distribution** (Codex finding #7). Phase 1 sidesteps this because the bundled reference skill ships inside the gstack install. User-authored skills land on machines without Bun unless we ship a runtime alongside, compile to a self-contained binary, or use Node + the existing `cli.ts` pattern. Deferred to Phase 4 — `/skillify` documents the assumption that gstack is installed (which means Bun is on PATH). - -**Context:** The Phase 1 architecture (3-tier lookup, scoped tokens, sibling SDK, frontmatter contract) is locked and exercised by the bundled `hackernews-frontpage` reference skill. Phase 2a plugs `/scrape` and `/skillify` into that runtime via two skill templates plus one new helper (`browse/src/browser-skill-write.ts` for atomic temp-dir-then-rename per `/plan-eng-review` D3) — no new storage primitives. - -**Effort:** M (human: ~1 week / CC: ~1 day) -**Priority:** P1 (this branch — `garrytan/browserharness` shipping as v1.19.0.0) -**Depends on:** Phase 1 shipped (this branch). - ---- - ### P2: Browser-skills Phase 3 — resolver injection at session start **What:** Mirror the domain-skill resolver at `browse/src/server.ts:722-743`. When a sidebar-agent session starts on a host with matching browser-skills, inject a list block telling the agent which skills exist for that host and how to invoke them (`$B skill run --arg ...`). UNTRUSTED-wrapped via the existing L1-L6 security stack. Add `gstack-config browser_skillify_prompts` knob (default `off`) controlling end-of-task nudges in `/qa`, `/design-review`, etc. when activity feed shows ≥N commands on a single host AND no skill exists yet for that host+intent. @@ -654,24 +408,6 @@ made opt-in. Lower priority than the gbrain-side perf issue above. --- -### P2: Remove plan-mode handshake from `/plan-devex-review` SKILL.md.tmpl - -**What:** `/plan-devex-review` has a "Plan Mode Handshake" section at the top that contradicts the preamble's "Skill Invocation During Plan Mode" contract (which says AskUserQuestion satisfies plan mode's end-of-turn requirement). The handshake forces an extra exit-plan-mode step that no other interactive review skill needs. `/plan-ceo-review`, `/plan-eng-review`, `/plan-design-review` all run fine in plan mode without it. - -**Why:** Found during the v1.8.0.0 DevEx review. The inconsistency cost a turn and confused the flow. Either remove the handshake from `plan-devex-review` (clean fix, recommended) OR add it to every interactive skill for consistency. - -**Pros:** Fixes a real DX bug for anyone running `/plan-devex-review` in plan mode. Five-minute change. - -**Cons:** Need to think about WHY it was added in the first place — there may be context this TODO is missing. - -**Context:** The handshake section in `plan-devex-review/SKILL.md.tmpl` says it's needed because plan mode's "this supersedes any other instructions" warning could otherwise bypass the skill's per-finding STOP gates. But the same warning exists for the other review skills, and they all work fine because AskUserQuestion satisfies the end-of-turn contract. - -**Effort:** S (human: ~15 min / CC: ~5 min) -**Priority:** P2 -**Depends on:** Nothing. - ---- - ### P2: Bump gbrain install-pin in lockstep with gstack memory-feature releases (#1305 part 2) **What:** `bin/gstack-gbrain-install` pins gbrain to commit `08b3698` (v0.18.2). When gstack ships features that depend on newer gbrain ops or schema (e.g. v1.26.0 manifests + `code-def`/`code-refs`/`reindex-code`), the pin doesn't move with it. Fresh `/setup-gbrain` installs an old gbrain that fails `gbrain doctor` schema_version checks (24 vs latest 32+) until the user manually upgrades. diff --git a/automate/SKILL.md b/automate/SKILL.md new file mode 100644 index 0000000000..a8a4afb82e --- /dev/null +++ b/automate/SKILL.md @@ -0,0 +1,917 @@ +--- +name: automate +version: 1.0.0 +description: Codify form fills, click sequences, and multi-step interactions into permanent browser-skills. (gstack) +allowed-tools: + - Bash + - Read + - AskUserQuestion +triggers: + - automate this flow + - fill the form + - log in to + - click through + - submit this +--- + + + + +## When to invoke this skill + +First call on a new intent prototypes the flow via $B primitives +with strict confirmation gates. Subsequent calls on a matching intent route +to a codified browser-skill and return in ~200ms. +Use when asked to "automate", "fill the form", "log in", "click X", "submit", "create", or +perform multi-step mutating actions on a page. + +## Preamble (run first) + +```bash +_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) +GSTACK_ROOT="$HOME/.claude/skills/gstack" +[ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ] && GSTACK_ROOT="$_ROOT/.claude/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 || .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=$($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" +mkdir -p ~/.gstack/analytics +if [ "$_TEL" != "off" ]; then +echo '{"skill":"automate","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":"automate","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=$($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=$($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 +``` + +## 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 AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). 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 `$GSTACK_ROOT/[skill-name]/SKILL.md`. + +If output shows `UPGRADE_AVAILABLE `: read `$GSTACK_ROOT/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 `$GSTACK_ROOT/.feature-prompted-continuous-checkpoint`: AskUserQuestion for Continuous checkpoint auto-commits. If accepted, run `$GSTACK_BIN/gstack-config set checkpoint_mode continuous`. Always touch marker. +- Missing `$GSTACK_ROOT/.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 `$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 Ocean** 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 or file paths. Your repo name is recorded locally only and stripped before any upload. + +Options: +- A) Help gstack get better! (recommended) +- B) No thanks + +If A: run `$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 `$GSTACK_BIN/gstack-config set telemetry anonymous` +If B→B: run `$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 `$GSTACK_BIN/gstack-config set proactive true` +If B: run `$GSTACK_BIN/gstack-config set proactive false` + +Always run: +```bash +touch ~/.gstack/.proactive-prompted +``` + +Skip if `PROACTIVE_PROMPTED` is `yes`. + +## First-run guidance (one-time) + +If `ACTIVATED` is `no` (first skill run on this machine) AND the preamble printed a non-empty `FIRST_TASK:` value that is NOT `nongit`: show ONE short, project-specific line mapped from the token, as a heads-up, then CONTINUE with whatever the user actually asked — do NOT halt their task. Map the token: `greenfield` → "Fresh repo — shape it first with `/spec` or `/office-hours`." `code_node`/`code_python`/`code_rust`/`code_go`/`code_ruby`/`code_ios` → "There's code here — `/qa` to see it work, or `/investigate` if something's off." `branch_ahead` → "Unshipped work on this branch — `/review` then `/ship`." `dirty_default` → "Uncommitted changes — `/review` before committing." `clean_default` → "Pick one: `/spec`, `/investigate`, or `/qa`." Then substitute the token you saw for TASK_TOKEN and run (best-effort), and mark activated: +```bash +$GSTACK_BIN/gstack-telemetry-log --event-type first_task_scaffold_shown --skill "TASK_TOKEN" --outcome shown 2>/dev/null || true +touch ~/.gstack/.activated 2>/dev/null || true +``` + +If `ACTIVATED` is `no` but `FIRST_TASK:` is empty or `nongit` (headless, non-git, or nothing actionable): show nothing, just run `touch ~/.gstack/.activated 2>/dev/null || true`. + +Else if `ACTIVATED` is `yes` AND `FIRST_LOOP_SHOWN` is `no`: say once as a heads-up (then continue): + +> Tip: gstack pays off when you complete one loop — **plan → review → ship**. A common first loop: `/office-hours` or `/spec` to shape it, `/plan-eng-review` to lock it, then `/ship`. + +Then run `touch ~/.gstack/.first-loop-tip-shown 2>/dev/null || true`. + +Skip this section if `ACTIVATED` and `FIRST_LOOP_SHOWN` are both `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 +- Author a backlog-ready spec/issue → invoke /spec +``` + +Then commit the change: `git add CLAUDE.md && git commit -m "chore: add gstack skill routing rules to CLAUDE.md"` + +If B: run `$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 `$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 $GSTACK_ROOT && ./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 "$($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. + +**Conductor rule (read before the MCP rule):** if `CONDUCTOR_SESSION: true` was echoed by the preamble, do NOT call AskUserQuestion at all — neither native nor any `mcp__*__AskUserQuestion` variant. Render EVERY decision brief as the **prose form** below and STOP. This is proactive, not a reaction to a failure: Conductor disables native AUQ and its MCP variant is flaky (it returns `[Tool result missing due to internal error]`), so prose is the reliable path. **Auto-decide preferences still apply first:** if a `[plan-tune auto-decide]