Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## [1.60.2.0] - 2026-08-05

## **Conductor decisions are clickable popups again.**

gstack now routes questions through Conductor's working host tool instead of
blocking it and replacing the decision with a long reply-by-letter message. A
successful choice resumes the skill once, cancellation stays cancelled, and
prose appears only when a genuine transport failure cannot be recovered.

### Fixed

- Conductor sessions prefer `mcp__conductor__AskUserQuestion`, redirect native
calls to that host tool, and validate its string-option contract before the
popup renders. Auto-decisions still run first, while destructive and one-way
decisions still require a person.
- Retry behavior now distinguishes completed responses, cancellation, the
Claude SDK's ambiguous missing-result placeholder, retryable delivery/answer
errors, and non-retryable session failures. The same question retries at most
once even if JSON key order changes.
- Upgrades normalize duplicate AskUserQuestion hooks left behind when Claude
removed gstack's source tags, preserving unrelated hooks and taking a backup
before changing `settings.json`. A failed normalization leaves the prior setup
version in place so the next setup run retries it.

### Changed

- Generated skill guidance documents the host-MCP string payload and keeps
prose as an unavailable-tool or exhausted-transport fallback. The old
Conductor-prose E2E now tests that genuine fallback instead.

## [1.60.1.0] - 2026-07-09

## **The /autoplan dual-voice eval is back on the board, catching real regressions.**
Expand Down Expand Up @@ -240,12 +270,11 @@ real `~/.claude` in the loop.
scripts: detached, SIGTERM-proof, `caffeinate`-wrapped eval runs with a machine-wide
lock, per-run logs under `~/.gstack-dev/eval-runs/`, a watchdog, and an `EXIT=`
sentinel.
- **Conductor prose AskUserQuestion**: when a Conductor session is detected, every
decision renders as a prose brief (labeled question, recommendation, per-option
completeness, reply-with-a-letter), enforced by a PreToolUse hook that denies the
tool and redirects. Auto-decide preferences still apply first; destructive
confirmations require an explicit typed answer. Installed for Conductor even in
non-interactive setup, with an upgrade migration for existing installs.
- **Historical AskUserQuestion workaround (superseded in v1.60.2.0)**: v1.58
temporarily bypassed Conductor's host question tool and rendered decision briefs
in chat. Auto-decide preferences still applied first, and destructive confirmations
required an explicit typed answer. The later popup restoration replaces this
workaround while retaining its safety gates.

#### Changed
- All five E2E runners (`session-runner`, `claude-pty-runner`, `agent-sdk-runner`,
Expand Down
9 changes: 5 additions & 4 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ 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.
# Conductor host: native AskUserQuestion is disabled (--disallowedTools), so
# route clickable questions through mcp__conductor__AskUserQuestion with plain
# STRING options (see the AskUserQuestion Format section). Signal it here so
# skills pick the right tool + payload shape. Gated on !headless so an eval/CI
# run INSIDE Conductor (GSTACK_HEADLESS) still BLOCKs rather than prompting nobody.
if [ "$_SESSION_KIND" != "headless" ] && { [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "${CONDUCTOR_PORT:-}" ]; }; then
echo "CONDUCTOR_SESSION: true"
fi
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.60.1.0
1.60.2.0
37 changes: 24 additions & 13 deletions autoplan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ 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.
# Conductor host: native AskUserQuestion is disabled (--disallowedTools), so
# route clickable questions through mcp__conductor__AskUserQuestion with plain
# STRING options (see the AskUserQuestion Format section). Signal it here so
# skills pick the right tool + payload shape. Gated on !headless so an eval/CI
# run INSIDE Conductor (GSTACK_HEADLESS) still BLOCKs rather than prompting nobody.
if [ "$_SESSION_KIND" != "headless" ] && { [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "${CONDUCTOR_PORT:-}" ]; }; then
echo "CONDUCTOR_SESSION: true"
fi
Expand Down Expand Up @@ -342,20 +343,28 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:

"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] <id> → <option>` result has already surfaced for a question, proceed with that option (no prose). Because in Conductor you go straight to prose without ever calling the tool, this auto-decide-first ordering is enforced HERE, not only by the PreToolUse hook. When you render a Conductor prose brief, also capture it with `bin/gstack-question-log` (the PostToolUse capture hook never fires on a prose path, so `/plan-tune` history/learning depends on this call).
**Rule — always prefer the MCP variant when present.** If any `mcp__*__AskUserQuestion` variant is in your tool list, CALL IT. This includes Conductor's `mcp__conductor__AskUserQuestion` — it renders clickable questions and works. Hosts disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant, so calling native there silently fails. Do NOT render prose instead — the MCP tool is the clickable path.

**Rule (non-Conductor):** 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.
**Payload shape (this is the one thing that must be right):** the MCP variant takes `options` as an array of plain **strings** — put the option letter, label, the `(recommended)` marker, and short tradeoff text all inside each string (e.g. `"A) Site + app foundation (recommended) — ship the site now and add the DB/app skeleton underneath"`). It does **NOT** accept `{label, description}` option objects — those render as blank/unclickable choices. The full `D<N>` decision brief (ELI10, Recommendation, etc.) goes in the `question` field exactly as in the Format section below; long multi-line `question` text is fine. Native Claude AUQ — only when no MCP variant exists — uses option **objects** with `label` + optional `description` instead.

If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
**Conductor MCP contract:** send 1-4 `questions`. Each entry is `{ question: string, options: string[<=4], multiSelect?: boolean }`. Do not include an `Other` option; Conductor adds it automatically.

**Auto-decide applies first:** if a `[plan-tune auto-decide] <id> → <option>` result has already surfaced for a question, proceed with that option — do not re-call the tool.

If AskUserQuestion is unavailable (no variant in your tool list) OR a call returns an explicit error, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.

### When AskUserQuestion is unavailable or a call fails

Tell three outcomes apart:
Tell these outcomes apart:

1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
2. **Success.** A Conductor result beginning `User responses:` is the completed human answer. Apply it and resume the workflow exactly once. Do NOT retry and do NOT emit a prose duplicate.
3. **Cancellation.** `CONDUCTOR_ASK_USER_QUESTION_USER_CANCELLED` means the human cancelled. Do NOT retry or replace it with prose; remain stopped for new user direction.
4. **Explicitly retryable Conductor failure.** Retry the SAME call exactly **once** only when its error code is `CONDUCTOR_ASK_USER_QUESTION_DELIVERY_FAILED`, `CONDUCTOR_ASK_USER_QUESTION_MALFORMED_ANSWERS`, or `CONDUCTOR_ASK_USER_QUESTION_ANSWER_COUNT_MISMATCH` and the result says `Retryable: yes`. Preserve the same questions and plain-string options. If that one retry fails, use the session-kind fallback below.
5. **Non-retryable failure or unavailable tool.** `CONDUCTOR_ASK_USER_QUESTION_SESSION_UNAVAILABLE`, any other explicit error, or no available AskUserQuestion variant goes directly to the existing session-kind fallback. Do not retry.
6. **Ambiguous SDK placeholder.** `[Tool result missing due to internal error]` is emitted by the Claude Code SDK, not Conductor, and is not proof that popup delivery failed. Treat the question as pending: do NOT retry and do NOT emit prose from this placeholder alone.

For outcomes 4 (after retry exhaustion) and 5, branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless` → `BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive` → **prose fallback** (below).
Expand All @@ -366,7 +375,7 @@ Tell three outcomes apart:
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.

Layout: a `D<N>` title + a one-line note to reply with a letter (in Conductor this is the normal path; elsewhere it means AskUserQuestion was unavailable or errored); the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
Layout: a `D<N>` title + a one-line note to reply with a letter (this fallback means AskUserQuestion was unavailable or errored); the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.

**Continuation — mapping a typed reply back to a brief.** Each brief carries a stable label (`D<N>`, or `D<N>.k` in a split chain). The user references it (e.g. "3.2: B"). A bare letter maps to the single most-recent UNANSWERED brief; if more than one is open (a split chain), do NOT guess — ask which `D<N>.k` it answers. Never apply a bare letter ambiguously across a chain.

Expand Down Expand Up @@ -454,7 +463,9 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool, not writing prose — unless `CONDUCTOR_SESSION: true` (then prose is the DEFAULT, not the tool) OR the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Options use the right shape for the tool you're calling: MCP variant (incl. Conductor) → plain **strings**; native Claude → `{label, description}` objects
- [ ] Conductor MCP call has 1-4 questions, ≤4 options each, and no explicit `Other` option
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
Expand Down
9 changes: 5 additions & 4 deletions benchmark-models/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ 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.
# Conductor host: native AskUserQuestion is disabled (--disallowedTools), so
# route clickable questions through mcp__conductor__AskUserQuestion with plain
# STRING options (see the AskUserQuestion Format section). Signal it here so
# skills pick the right tool + payload shape. Gated on !headless so an eval/CI
# run INSIDE Conductor (GSTACK_HEADLESS) still BLOCKs rather than prompting nobody.
if [ "$_SESSION_KIND" != "headless" ] && { [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "${CONDUCTOR_PORT:-}" ]; }; then
echo "CONDUCTOR_SESSION: true"
fi
Expand Down
9 changes: 5 additions & 4 deletions benchmark/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ 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.
# Conductor host: native AskUserQuestion is disabled (--disallowedTools), so
# route clickable questions through mcp__conductor__AskUserQuestion with plain
# STRING options (see the AskUserQuestion Format section). Signal it here so
# skills pick the right tool + payload shape. Gated on !headless so an eval/CI
# run INSIDE Conductor (GSTACK_HEADLESS) still BLOCKs rather than prompting nobody.
if [ "$_SESSION_KIND" != "headless" ] && { [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "${CONDUCTOR_PORT:-}" ]; }; then
echo "CONDUCTOR_SESSION: true"
fi
Expand Down
Loading
Loading