Problem
When altimate-code run "<task>" --yolo is invoked as a subprocess (Claude Code's Bash tool, CI, subprocess.run(..., stdin=None), agent harnesses), and a skill that uses the question tool fires during the run, the process hangs at 0% CPU forever. The harness eventually TaskStops the subprocess, looking identical to a deadlock.
Concretely surfaced during the data-parity skill's run when it detected PII columns in DimCustomer/DimEmployee and (per its safety rules) asked the user "May I run row-level hashdiff comparisons where sample diff rows may appear if mismatches exist?" with options "Approve row diff" / "Profile only". 70 seconds later, Claude Code TaskStop'd the subprocess.
Root cause
packages/opencode/src/tool/question.ts calls Question.ask(), which awaits an Effect Deferred. In TUI mode the deferred resolves when the user clicks an option. In claude --print / CI / non-interactive contexts, there is no TUI and no user — the deferred awaits forever, while the parent host waits for the subprocess to finish. Neither side has a path to give up.
Proposed fix
In non-interactive contexts (no TTY, or explicit env-var opt-in), auto-resolve question with a conservative-by-default policy and surface the choice clearly in the tool result so the calling LLM can adapt.
Resolution policy (env-var controlled, default safe):
- Detect non-interactive:
!process.stdin.isTTY, with overrides ALTIMATE_FORCE_INTERACTIVE=1 (use the original interactive Deferred path) and ALTIMATE_NON_INTERACTIVE=1 (force non-interactive even when isTTY is true).
ALTIMATE_AUTO_ANSWER=last (default in non-TTY) — pick the option whose label/description contains a safe keyword (skip, cancel, no, abort, profile only, decline, deny, stop); fall back to the last option (UX convention: safer/cancel typically sits at the end).
ALTIMATE_AUTO_ANSWER=first — always first option.
ALTIMATE_AUTO_ANSWER=skip — return Unanswered for all questions.
ALTIMATE_AUTO_ANSWER="<exact label>" — exact-match an option's label.
The tool result text explicitly flags non-interactive auto-answer ("Running in non-interactive mode (no TTY). Auto-answered with safe defaults: …"), so downstream agents can adjust strategy if needed.
Why not just always pick "cancel"?
Picking cancel/abort blindly fails open in the opposite direction: skills that ask permission to do reasonable work would always get a no, breaking the user's actual intent. The safe-keyword scan tries to match the question author's intent (these are typically "may I do destructive thing X?" prompts) without blocking legitimate flows.
Where this should live
Two choices:
- At the tool boundary (this fix) —
packages/opencode/src/tool/question.ts short-circuits before calling Question.ask().
- At the Effect layer — push the non-interactive detection into
Question.ask() itself.
Option 1 is the proposed PR. Option 2 is a deeper, more invasive change and may be the right long-term home — happy to revisit if reviewers prefer that path.
Test plan
- Unit test (mocked TTY): non-interactive mode + 2-option question → assert tool result names the auto-answered option and tags it as non-interactive.
- Unit test (
ALTIMATE_FORCE_INTERACTIVE=1 + non-TTY): assert original Question.ask() path is taken (defers to interactive Deferred).
- Unit test (
ALTIMATE_AUTO_ANSWER=skip): assert Unanswered returned without calling Question.ask().
- Smoke: trigger any skill that uses
question from a Claude Code Bash tool call; expect completion in seconds, not a hang.
Context
Surfaced during the same multi-week experiment series that produced #934. Patched locally in altimate-code-preview (packages/opencode/src/tool/question.ts, ~70 lines + accompanying test/tool/question.test.ts); running clean against real workloads for ~9 days. Full writeup: plugin-skill-experiments/03-issues-and-fixes.md Issue #5.
PR incoming.
Problem
When
altimate-code run "<task>" --yolois invoked as a subprocess (Claude Code's Bash tool, CI,subprocess.run(..., stdin=None), agent harnesses), and a skill that uses thequestiontool fires during the run, the process hangs at 0% CPU forever. The harness eventuallyTaskStops the subprocess, looking identical to a deadlock.Concretely surfaced during the
data-parityskill's run when it detected PII columns inDimCustomer/DimEmployeeand (per its safety rules) asked the user "May I run row-level hashdiff comparisons where sample diff rows may appear if mismatches exist?" with options "Approve row diff" / "Profile only". 70 seconds later, Claude Code TaskStop'd the subprocess.Root cause
packages/opencode/src/tool/question.tscallsQuestion.ask(), which awaits an EffectDeferred. In TUI mode the deferred resolves when the user clicks an option. Inclaude --print/ CI / non-interactive contexts, there is no TUI and no user — the deferred awaits forever, while the parent host waits for the subprocess to finish. Neither side has a path to give up.Proposed fix
In non-interactive contexts (no TTY, or explicit env-var opt-in), auto-resolve
questionwith a conservative-by-default policy and surface the choice clearly in the tool result so the calling LLM can adapt.Resolution policy (env-var controlled, default safe):
!process.stdin.isTTY, with overridesALTIMATE_FORCE_INTERACTIVE=1(use the original interactiveDeferredpath) andALTIMATE_NON_INTERACTIVE=1(force non-interactive even when isTTY is true).ALTIMATE_AUTO_ANSWER=last(default in non-TTY) — pick the option whose label/description contains a safe keyword (skip,cancel,no,abort,profile only,decline,deny,stop); fall back to the last option (UX convention: safer/cancel typically sits at the end).ALTIMATE_AUTO_ANSWER=first— always first option.ALTIMATE_AUTO_ANSWER=skip— returnUnansweredfor all questions.ALTIMATE_AUTO_ANSWER="<exact label>"— exact-match an option's label.The tool result text explicitly flags non-interactive auto-answer ("Running in non-interactive mode (no TTY). Auto-answered with safe defaults: …"), so downstream agents can adjust strategy if needed.
Why not just always pick "cancel"?
Picking cancel/abort blindly fails open in the opposite direction: skills that ask permission to do reasonable work would always get a no, breaking the user's actual intent. The safe-keyword scan tries to match the question author's intent (these are typically "may I do destructive thing X?" prompts) without blocking legitimate flows.
Where this should live
Two choices:
packages/opencode/src/tool/question.tsshort-circuits before callingQuestion.ask().Question.ask()itself.Option 1 is the proposed PR. Option 2 is a deeper, more invasive change and may be the right long-term home — happy to revisit if reviewers prefer that path.
Test plan
ALTIMATE_FORCE_INTERACTIVE=1+ non-TTY): assert originalQuestion.ask()path is taken (defers to interactive Deferred).ALTIMATE_AUTO_ANSWER=skip): assertUnansweredreturned without callingQuestion.ask().questionfrom a Claude Code Bash tool call; expect completion in seconds, not a hang.Context
Surfaced during the same multi-week experiment series that produced #934. Patched locally in
altimate-code-preview(packages/opencode/src/tool/question.ts, ~70 lines + accompanyingtest/tool/question.test.ts); running clean against real workloads for ~9 days. Full writeup:plugin-skill-experiments/03-issues-and-fixes.mdIssue #5.PR incoming.