Skip to content

fix: auto-resolve question tool in non-interactive contexts#937

Merged
sahrizvi merged 4 commits into
mainfrom
fix/question-non-interactive-936
Jun 18, 2026
Merged

fix: auto-resolve question tool in non-interactive contexts#937
sahrizvi merged 4 commits into
mainfrom
fix/question-non-interactive-936

Conversation

@sahrizvi

@sahrizvi sahrizvi commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Closes #936

Summary

packages/opencode/src/tool/question.ts calls Question.ask(), which awaits an Effect Deferred that only resolves on a TUI click. When altimate-code run is invoked as a subprocess (Claude Code's Bash tool, CI, subprocess.run, plugin host) and a skill that uses the question tool fires, nobody can ever click — the deferred awaits forever and the parent eventually TaskStops the subprocess. Symptom: 0% CPU, no log activity, no error, indistinguishable from a hang.

This PR short-circuits the question tool in non-interactive contexts with a conservative-by-default auto-answer policy.

Resolution policy

  • Non-interactive detection: !process.stdin.isTTY. Overrides:
    • ALTIMATE_FORCE_INTERACTIVE=1 — keep the original interactive Deferred path even when isTTY is false.
    • ALTIMATE_NON_INTERACTIVE=1 — force non-interactive even when isTTY is true (useful for tests + CI assertions).
  • Default in non-TTY (ALTIMATE_AUTO_ANSWER=last): 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).
  • Explicit overrides:
    • ALTIMATE_AUTO_ANSWER=first — always pick first option.
    • ALTIMATE_AUTO_ANSWER=skip — return Unanswered for all questions.
    • ALTIMATE_AUTO_ANSWER="<exact label>" — exact-match an option's label (case-insensitive).
  • Tool result prefix reflects mode"Running in non-interactive mode (no TTY). Auto-answered with safe defaults: ..." vs the original "User has answered your questions: ..." — so the agent knows the choice was not a real user answer and can adapt strategy.

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 lives

Two reasonable choices:

  1. At the tool boundary (this PR)packages/opencode/src/tool/question.ts short-circuits before calling Question.ask().
  2. At the Effect layer — push the non-interactive detection into Question.ask() itself, so any caller of Question.ask (not just the tool) benefits.

Option 1 is what this PR ships; the scope is contained and the diff is reviewable. Option 2 is the deeper, more invasive change and may be the right long-term home. Happy to refactor if reviewers prefer.

Test plan

  • bun test test/tool/question.test.ts8 pass, 0 fail, 12 expect() calls. Verified locally before pushing.
    • 2 pre-existing legacy tests retained (gated with ALTIMATE_FORCE_INTERACTIVE=1 in beforeEach so they preserve their original intent under non-TTY CI).
    • 6 new tests covering: safe-keyword selection, last-option fallback, ALTIMATE_AUTO_ANSWER=first, =skip, =<exact label>, non-interactive prefix wording.
  • Reviewer smoke: trigger any skill using question from Claude Code's Bash tool — expect completion in seconds with auto-answered output, not a hang.

Diff size

packages/opencode/src/tool/question.ts       |  81 ++++++++++++++++--
packages/opencode/test/tool/question.test.ts | 123 +++++++++++++++++++++++++++
2 files changed, 198 insertions(+), 6 deletions(-)

The patch is roughly 70 lines of source change plus tests. Larger than #935 (the stdin-wedge guard) because the resolution policy has real branching to implement; still bounded to one tool file.

Risk

Low. Default behavior under TTY is unchanged (the new branch only fires when !isTTY or ALTIMATE_NON_INTERACTIVE=1). The non-TTY auto-answer surfaces explicitly in the tool result, so a downstream agent treating the choice as a real user click is impossible. ALTIMATE_FORCE_INTERACTIVE=1 provides an escape hatch for any consumer that wants the original behavior even in non-TTY.

Links


Summary by cubic

Fixes hangs in the question tool for headless runs by treating altimate-code run as non-interactive, defaulting to Unanswered, and clearly labeling the mode. Interactive server flows remain unchanged; addresses #936.

  • Bug Fixes
    • Env-driven detection: ALTIMATE_NON_INTERACTIVE=1 enables non-interactive; ALTIMATE_FORCE_INTERACTIVE=1 forces interactive; ALTIMATE_NON_INTERACTIVE=0 opts out. run sets ALTIMATE_NON_INTERACTIVE=1 by default (skipped with --attach).
    • Non-interactive behavior: return Unanswered for all questions; removed label heuristics. Optional auto-answer via ALTIMATE_AUTO_ANSWER=first, =last, or ="<exact label>".
    • Output: prefix states “non-interactive mode (no answer channel available)”, removes the interactive trailer, and points to ALTIMATE_AUTO_ANSWER.
    • bash tool strips ALTIMATE_NON_INTERACTIVE from child env to avoid breaking nested server reply paths.
    • run stdin read is now fully null-safe: use process.stdin && !process.stdin.isTTY and skip the read when stdin is undefined to prevent stalls.
    • Tests cover env-var detection, precedence (FORCE_INTERACTIVE over NON_INTERACTIVE), explicit opt-out (NON_INTERACTIVE=0), non-interactive prefix, and suite isolation fixes.

Written for commit 764291c. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Improved non-interactive execution with deterministic question auto-answering (first, last, or exact option label) via configuration, or leaving items unanswered.
    • Mode-specific messaging now clearly indicates when the tool is running non-interactively and how to preconfigure answers.
  • Bug Fixes

    • Reduced risk of blocking in the run entrypoint by enabling non-interactive mode by default when not attaching.
    • Improved robustness when stdin is unavailable.
    • Prevented non-interactive settings from affecting nested executions.
  • Tests

    • Added coverage for auto-answer modes, option-label matching, unanswered fallback, and non-interactive vs interactive messaging.

`Question.ask()` awaits an Effect Deferred that only resolves on a
TUI click. When `altimate-code run` is invoked as a subprocess
(Claude Code's Bash tool, CI, plugin host) and a skill that uses
`question` fires, nobody can ever click — the deferred awaits
forever and the parent eventually TaskStops the subprocess. The
symptom is indistinguishable from a hang: 0% CPU, no log activity,
no error.

In non-interactive contexts (no TTY, or explicit env-var opt-in),
auto-resolve `question` with a conservative-by-default policy and
flag the auto-answer in the tool result so the calling LLM can
adapt instead of treating it as a real user choice.

Resolution policy (env-var controlled):
- Detect non-interactive: `!process.stdin.isTTY`. Overrides:
  ALTIMATE_FORCE_INTERACTIVE=1 — keep the original interactive
  Deferred path even when isTTY is false.
  ALTIMATE_NON_INTERACTIVE=1 — force non-interactive even when
  isTTY is true (useful for tests + CI assertions).
- Default in non-TTY (ALTIMATE_AUTO_ANSWER=last): 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 end).
- ALTIMATE_AUTO_ANSWER=first / =skip / =<exact label>: explicit
  overrides for callers who want a specific behavior.

Tool result prefix reflects mode — "Running in non-interactive
mode (no TTY). Auto-answered with safe defaults: ..." vs the
original "User has answered your questions: ..." — so the agent
knows the choice was not a real user answer.

Tests: 6 new bun:test cases covering safe-keyword selection,
last-option fallback, each ALTIMATE_AUTO_ANSWER mode, and the
prefix wording. Existing 2 legacy tests gated with
ALTIMATE_FORCE_INTERACTIVE=1 so they preserve their original
intent under non-TTY CI.

Closes #936
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3d3d6b2e-5810-4bad-bc7e-96f8b47f9007

📥 Commits

Reviewing files that changed from the base of the PR and between d2a8516 and 764291c.

📒 Files selected for processing (2)
  • packages/opencode/src/cli/cmd/run.ts
  • packages/opencode/test/tool/question.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/opencode/src/cli/cmd/run.ts
  • packages/opencode/test/tool/question.test.ts

📝 Walkthrough

Walkthrough

QuestionTool now detects non-interactive runs via environment variables (ALTIMATE_FORCE_INTERACTIVE, ALTIMATE_NON_INTERACTIVE) and TTY availability, computing deterministic answers via ALTIMATE_AUTO_ANSWER (first/last/exact label/skip) instead of awaiting user input when non-interactive. Run command enables non-interactive mode by default in subprocess contexts and safely handles stdin in runtimes where it may be undefined. Bash tool cleans up the flag to prevent inheritance by child processes.

Changes

Non-interactive auto-answer support

Layer / File(s) Summary
Question tool non-interactive auto-answer
packages/opencode/src/tool/question.ts
Adds isNonInteractive() and autoAnswer(...) helpers driven by ALTIMATE_AUTO_ANSWER (first/last/exact label/skip), bypasses Question.ask() when non-interactive, and updates output prefix to indicate non-interactive auto-answering with escape-hatch documentation.
Question tool test coverage
packages/opencode/test/tool/question.test.ts
Forces interactive path in legacy tests via ALTIMATE_FORCE_INTERACTIVE=1 setup. Adds non-interactive test suite validating auto-answer modes, Unanswered defaults, heuristic regression checks, and non-interactive output prefix. Adds default-detection suite validating automatic selection and env var override semantics.
Run command non-interactive initialization and stdin safety
packages/opencode/src/cli/cmd/run.ts
Sets ALTIMATE_NON_INTERACTIVE=1 by default for non-attach runs when unset, preventing question-tool deadlocks in subprocess contexts. Updates stdin ingestion with null-safe process.stdin?.isTTY check to handle runtimes where stdin may be undefined.
Bash tool environment cleanup
packages/opencode/src/tool/bash.ts
Strips ALTIMATE_NON_INTERACTIVE from merged environment before spawning child processes, preventing nested server-mode invocations from inheriting the non-interactive flag.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped where prompts once stalled the run,
No TTY? I choose—the safe path won.
First, last, or label, the rule is clear,
Auto-answers bloom, no hangs appear.
A rabbit's logic fixed the queue with cheer!

Suggested reviewers

  • anandgupta42
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive The PR includes helpful but technically out-of-scope fixes to run.ts stdin handling and bash.ts environment isolation. While justified as companion fixes, the PR description primarily commits to closing #936, not #935, creating unclear scope boundaries.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: handling non-interactive execution in the question tool by implementing auto-answer behavior.
Description check ✅ Passed The PR description follows the required template with Summary, Test Plan, and Checklist sections, comprehensively explaining the problem and solution.
Linked Issues check ✅ Passed The PR successfully addresses issue #936's requirements: non-TTY detection, environment-controlled auto-answer policy, explicit non-interactive result prefix, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/question-non-interactive-936

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/opencode/src/tool/question.ts (2)

39-57: 💤 Low value

Consider logging when ALTIMATE_AUTO_ANSWER doesn't match any known mode or option label.

When ALTIMATE_AUTO_ANSWER is set to a value that doesn't match "skip", "first", "last", or any option label (case-insensitive), the function silently returns empty arrays (Unanswered). This is safe but could make debugging harder if a user misspells a mode or label.

Consider logging a warning in this case to help users diagnose configuration issues.

📝 Optional: Add debug logging for unmatched modes
     // exact label match for explicit answers, e.g. ALTIMATE_AUTO_ANSWER="Profile only"
     const match = q.options.find((o) => o.label.toLowerCase() === mode)
+    if (!match && mode !== "skip" && mode !== "first" && mode !== "last") {
+      console.warn(`ALTIMATE_AUTO_ANSWER="${mode}" did not match any option label; returning Unanswered`)
+    }
     return match ? [match.label] : []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/tool/question.ts` around lines 39 - 57, The autoAnswer
function silently returns empty answers when ALTIMATE_AUTO_ANSWER contains an
unknown mode or when the explicit-label match fails; add a warning log to aid
debugging: detect when mode is not "skip"/"first"/"last" and no q.options label
matches the lowercase mode, and emit a single warning (e.g., console.warn or the
module's logger) including the provided ALTIMATE_AUTO_ANSWER value and question
id/label (use Question.Info properties) so users know the env value didn't match
any known mode or option; place this check inside autoAnswer just before
returning an empty array for unmatched cases and reference autoAnswer,
ALTIMATE_AUTO_ANSWER, and q.options in the change.

86-92: 💤 Low value

Consider mode-specific prefix wording for accuracy.

The prefix says "Auto-answered with safe defaults" but this is only accurate when ALTIMATE_AUTO_ANSWER is "last" (default). When the mode is "first" or an exact label match, the selection isn't necessarily using safe defaults—it's just picking the first option or the specified label.

While the key information ("Running in non-interactive mode") is accurate and sufficient, you might consider mode-specific wording for precision:

  • "last" → "Auto-answered with safe defaults"
  • "first" → "Auto-selected first option"
  • exact match → "Auto-selected option: {mode}"
  • "skip" → "Auto-skipped (Unanswered)"

This is a minor clarity improvement; the current wording is acceptable since the primary goal is signaling non-interactive execution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/tool/question.ts` around lines 86 - 92, The prefix
message built in question.ts (variable prefix) misstates the non-interactive
auto-answer behavior by always saying "Auto-answered with safe defaults"; update
the logic that sets prefix (referencing isNonInteractive() and the
ALTIMATE_AUTO_ANSWER mode) to choose mode-specific wording: "Auto-answered with
safe defaults" for "last", "Auto-selected first option" for "first",
"Auto-selected option: {mode}" for exact label matches, and "Auto-skipped
(Unanswered)" for "skip", while keeping the existing "Running in non-interactive
mode" text and preserving the interactive branch "User has answered your
questions".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/opencode/src/tool/question.ts`:
- Around line 39-57: The autoAnswer function silently returns empty answers when
ALTIMATE_AUTO_ANSWER contains an unknown mode or when the explicit-label match
fails; add a warning log to aid debugging: detect when mode is not
"skip"/"first"/"last" and no q.options label matches the lowercase mode, and
emit a single warning (e.g., console.warn or the module's logger) including the
provided ALTIMATE_AUTO_ANSWER value and question id/label (use Question.Info
properties) so users know the env value didn't match any known mode or option;
place this check inside autoAnswer just before returning an empty array for
unmatched cases and reference autoAnswer, ALTIMATE_AUTO_ANSWER, and q.options in
the change.
- Around line 86-92: The prefix message built in question.ts (variable prefix)
misstates the non-interactive auto-answer behavior by always saying
"Auto-answered with safe defaults"; update the logic that sets prefix
(referencing isNonInteractive() and the ALTIMATE_AUTO_ANSWER mode) to choose
mode-specific wording: "Auto-answered with safe defaults" for "last",
"Auto-selected first option" for "first", "Auto-selected option: {mode}" for
exact label matches, and "Auto-skipped (Unanswered)" for "skip", while keeping
the existing "Running in non-interactive mode" text and preserving the
interactive branch "User has answered your questions".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2e6c3623-6d27-4e80-adc6-f385f03948eb

📥 Commits

Reviewing files that changed from the base of the PR and between 146acea and a49442b.

📒 Files selected for processing (2)
  • packages/opencode/src/tool/question.ts
  • packages/opencode/test/tool/question.test.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/tool/question.ts Outdated
Comment thread packages/opencode/src/tool/question.ts Outdated
The earlier non-interactive policy in this branch scanned option label
text for "safe" keywords (skip/cancel/no/abort/...) and fell back to
the last option. That tried to recover semantics the LLM already knew
at construction time, and false-positived on substrings — "no"
matched inside "Snowflake", "Annotate", "Knowledge", "Honor".

The fix: don't guess. Return Unanswered for every question when no TTY
is present and let the agent decide. The agent has full context — it
knows what action it was about to take and why it asked. It can pick
a safe path from that context or report that user input is required.
Pretending a decision was made that wasn't is the worse failure mode.

Changes:
- Drop SAFE_KEYWORDS and the label-text scan entirely.
- Default non-interactive behavior returns [] (renders as "Unanswered"
  via the existing format()).
- Cache isNonInteractive() once at execute() entry so the result
  prefix can't disagree with the path that produced the answer.
- Non-interactive prefix tells the agent how to proceed AND names the
  escape hatch (ALTIMATE_AUTO_ANSWER=first|last|<label>) so it can
  surface it to the user when reporting back.
- Keep =first / =last / =<exact label> as explicit user opt-ins for
  callers who genuinely want a default. Drop =skip — it's the new
  default.

Addresses cubic-dev-ai review feedback on #937 by removing the
heuristic that needed the word-boundary fix in the first place.

Tests: 9 pass / 0 fail in packages/opencode/test/tool/question.test.ts.

Refs #936

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@sahrizvi

Copy link
Copy Markdown
Contributor Author

New commit: redesigned around the actual problem

Pushed f981199e5. Pivoted away from the cubic-fix-in-place approach (word-boundary on "no", cache isNonInteractive()) toward the cleaner answer:

Delete the label-text heuristic entirely. The LLM that constructed the question already knew at construction time which option meant "cancel" — the keyword scan was trying to recover information that was never lost, and false-positived on substrings ("no" matched inside "Snowflake", "Annotate", "Knowledge", "Honor").

What this commit does

  • Drops SAFE_KEYWORDS and the label-text scan.
  • Default non-interactive behavior: every question returns Unanswered. format() already renders empty-array answers as "Unanswered".
  • The non-interactive prefix tells the agent how to proceed AND surfaces the escape hatch:

    Running in non-interactive mode (no TTY). No user was available to answer. Either pick a safe path from the context of the action you were about to take, or report that user input is required to proceed — the user can set ALTIMATE_AUTO_ANSWER=first|last|<exact option label> to pre-answer questions in this mode. Result: ...

  • Keeps =first / =last / =<exact label> as explicit user opt-ins for callers who genuinely want a default. Drops =skip (it's now the default).
  • Caches isNonInteractive() once at execute() entry so the prefix can't disagree with the path that produced the answer.

Addresses cubic review feedback

  • P2 (substring "no"): resolved by deletion. No keyword scan, no substring problem.
  • P3 (mode recomputed post-await): cached at the top of execute().

Tests + verification

  • bun test test/tool/question.test.ts9 pass / 0 fail.
  • Dropped 2 safe-keyword tests; added =last opt-in test and =<unknown label> fall-through test.
  • Independent codex review: "consistently bypasses Question.ask in non-interactive mode, returns empty answers as Unanswered by default, and preserves explicit opt-in overrides. I did not find a discrete regression."

Honest tradeoff

The prior policy (auto-pick "last" or safe-keyword) tried to make forward progress when no human is present. The new policy says: forward progress on behalf of someone who didn't choose is the worse failure mode — let the agent (which has full context) decide. The escape hatch in the prefix gives the user explicit knobs if they disagree.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/tool/question.ts Outdated
@sahrizvi

Copy link
Copy Markdown
Contributor Author

Centralized-test bot failures — likely shared infra, not PR-caused

The dev-punia-altimate bot reported 15 TypeScript failures (connection_refused / timeout / parse_error / network_error / auth_failure / rate_limit / internal_error / empty_error / oom / permission_denied / ...). The identical 15-failure set appears on PRs #933 and #935, which touch unrelated code (packages/dbt-tools and packages/opencode/src/cli/cmd/run.ts respectively). This PR only modifies packages/opencode/src/tool/question.ts + its test file.

Strong signal these are shared fault-injection-harness failures, not regressions introduced by this PR. Flagging here so the pattern is on record across the three PRs; happy to dig into the harness config if the bot owner wants.

@suryaiyer95 suryaiyer95 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One workflow-affecting concern (verified against the codebase) — posting just this; the rest were quality/description nits.

Comment thread packages/opencode/src/tool/question.ts Outdated
function isNonInteractive(): boolean {
if (process.env["ALTIMATE_FORCE_INTERACTIVE"] === "1") return false
if (process.env["ALTIMATE_NON_INTERACTIVE"] === "1") return true
return !process.stdin.isTTY

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely regression for server / IDE (headless-but-interactive) mode — !process.stdin.isTTY is the wrong signal for "no human is listening."

Question.ask() does not only resolve on a TUI keypress. Answers also arrive over HTTP: Question.reply({ requestID, answers }) is exposed at POST /question/:requestID/reply (packages/opencode/src/server/routes/question.ts), with GET /question to list pending ones. So when altimate-code runs as a server with a frontend/IDE client (VS Code / JetBrains / web), process.stdin.isTTY is false, yet a real human can answer via the route.

With this guard, that existing interactive flow is misclassified as non-interactive: execute() short-circuits to autoAnswer() → returns Unanswered immediately and never publishes the question for the client to reply to. The UI user loses the ability to answer, out of the box — ALTIMATE_FORCE_INTERACTIVE=1 exists but server deployments won't have it set.

Suggestion: gate on whether an answer channel actually exists, not on TTY. E.g. treat it as interactive when the server/question clients are connected (a pending-question listener is registered), and reserve the auto-answer path for true headless runs (run subprocess / CI). At minimum, default server mode to interactive and require explicit ALTIMATE_NON_INTERACTIVE=1 to opt into auto-answer, so the altimate-code run-as-subprocess fix doesn't also silently disable the HTTP reply path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this — your point was the core insight that drove the entire redesign of this PR. Addressed across f981199, d2a85165, and 764291c0.

Specifically:

  • tool/question.ts no longer touches process.stdin.isTTY at all. Detection is now opt-in via ALTIMATE_NON_INTERACTIVE, and run is the only entrypoint that sets it. serve, web, acp, and workspace-serve deliberately leave it unset, so their POST /question/:requestID/reply path stays live for connected IDE/web clients exactly as you described.
  • cli/cmd/run.ts sets the env var only when args.attach is not set — attach-mode runs the agent on the remote server, so the local env var would just be noise and could mislead other host-local tools.
  • tool/bash.ts strips ALTIMATE_NON_INTERACTIVE from mergedEnv before spawning children, so a nested altimate-code serve (or any server entrypoint) spawned from a run session doesn't inherit the flag and silently disable its own HTTP reply path. This was a latent regression of the same class you flagged — it would have hit the same IDE/web users.

The block comment at tool/question.ts:6-38 captures the rationale and explicitly mentions PR #937 review for future maintainers. Tests at test/tool/question.test.ts lock in the default-interactive contract (the suryaiyer95-named regression class) plus FORCE_INTERACTIVE precedence and NON_INTERACTIVE=0 opt-out.

There's one remaining architectural item your review surfaced indirectly: session/llm.ts:299 only consults agent.permission, not the merged session.permission, so the question: deny rule in run.ts is intent-marker only. Tracked as a follow-up — out of scope here because the env-var path now handles the actual behavior correctly.

@dev-punia-altimate dev-punia-altimate left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Code Review — OpenCodeReview (Gemini) — 1 finding(s)

  • 1 anchored to a line (posted inline when the comment stream is on)
  • 0 without a line anchor
All findings (full text)

1. packages/opencode/src/tool/question.ts (L34)

[🟠 MEDIUM] There's a potential null pointer exception here. Depending on the runtime environment (e.g., embedded environments or child processes without standard streams), process.stdin could be undefined. Using optional chaining process.stdin?.isTTY is a safer approach and adheres to the null checks guideline.

Suggested change:

  return !process.stdin?.isTTY

Comment thread packages/opencode/src/tool/question.ts Outdated
function isNonInteractive(): boolean {
if (process.env["ALTIMATE_FORCE_INTERACTIVE"] === "1") return false
if (process.env["ALTIMATE_NON_INTERACTIVE"] === "1") return true
return !process.stdin.isTTY

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟠 MEDIUM] There's a potential null pointer exception here. Depending on the runtime environment (e.g., embedded environments or child processes without standard streams), process.stdin could be undefined. Using optional chaining process.stdin?.isTTY is a safer approach and adheres to the null checks guideline.

Suggested change:

Suggested change
return !process.stdin.isTTY
return !process.stdin?.isTTY

@dev-punia-altimate

Copy link
Copy Markdown
Contributor

🤖 Code Review — OpenCodeReview (Gemini) — No Issues Found

No supported files changed.

@anandgupta42

Copy link
Copy Markdown
Contributor

Code Review — /code-review (multi-model consensus)

Panel: Claude + 5 OpenRouter models (GPT-5.4, Gemini 3.1 Pro, Kimi K2.5, MiniMax M2.7, GLM-5). (qwen/deepseek/mimo unavailable on the provider, excluded.)

Verdict: Request changes — the fix is correct but incomplete. The design is right: returning Unanswered by default and letting the agent decide (rather than guessing from label text) is the safe call, and the Snowflake-contains-no regression is the right thing to have a test for. But the same hang still exists one file over.

MAJOR — the fix misses plan.ts, which has the identical wedge (Bug) — tool/plan.ts:25,99

Question.ask() is called from three places: tool/question.ts (fixed here) and tool/plan.ts:25 and :99 (not touched). Those two still await the TUI-only Deferred, so the plan tool reproduces the exact 0%-CPU non-interactive hang this PR exists to kill — any non-interactive run that hits the plan tool's question path wedges identically.

This is the concrete argument for the "Option 2" the description already floats: push isNonInteractive() / auto-answer into Question.ask() so all current and future callers are covered by construction. If you keep it at the tool boundary, replicate the guard in both plan.ts call sites in this PR — otherwise #936 is only half-closed.

MINOR — PR description no longer matches the code (Documentation) — consensus: GPT-5.4, Kimi, MiniMax, GLM-5

The body still documents the original policy ("default ALTIMATE_AUTO_ANSWER=last with safe-keyword scan: skip/cancel/no/abort…"), but the shipped code removed all label heuristics — default is now Unanswered, and =last simply takes the last option with no safe-keyword scan. The auto-summary already reflects the new behavior; please update the human-written description so reviewers/maintainers aren't misled.

MINOR — three new env vars, no central registration — GLM-5

ALTIMATE_FORCE_INTERACTIVE, ALTIMATE_NON_INTERACTIVE, ALTIMATE_AUTO_ANSWER are read directly via process.env. If there's a central env/flag registry (as with OPENCODE_* flags), register them there with docs; otherwise at least document the trio in one place so the precedence (FORCE_INTERACTIVE > NON_INTERACTIVE > isTTY) is discoverable.

Positive observations (consensus)

  • Returning Unanswered by default instead of inventing an answer is the correct, conservative design — every reviewer called this out. The companion test that asserts "Snowflake" is not auto-picked is exactly the regression guard this needs.
  • isNonInteractive() is evaluated once and reused for both the answer path and the result prefix — no double-eval / drift.
  • The explicit "Running in non-interactive mode (no TTY)…" prefix means a downstream agent can't mistake the auto-result for a real user choice — good.
  • ALTIMATE_FORCE_INTERACTIVE=1 escape hatch + legacy tests gated on it preserves prior behavior under non-TTY CI.

Bottom line: safe, well-tested fix for the question tool — but close the plan.ts sibling (ideally by moving the guard into Question.ask()) and refresh the PR description before merge.

Five fixes flowing from the multi-model consensus review on the prior
revision. Touches the `run` entrypoint, the bash tool, the question tool,
and its tests.

- `run.ts:447`: `process.stdin.isTTY` was still dereferenced without a
  guard. The prior commit closed null-safety in `question.ts` only;
  this closes the same risk in the `run` entrypoint. Addresses the
  dev-punia-altimate review comment in full.
- `run.ts` handler: gate the `ALTIMATE_NON_INTERACTIVE=1` assignment on
  `!args.attach`. In attach mode the agent runs on the remote server,
  so the local env var is a no-op and only pollutes the local process
  env for other tools that may consult it.
- `bash.ts`: strip `ALTIMATE_NON_INTERACTIVE` from `mergedEnv` before
  `spawn`. Without this, child processes spawned by the bash tool
  (e.g. nested `altimate-code serve`) would inherit the flag from the
  parent `run` process and silently disable their own HTTP question-
  reply path — the exact regression the prior commit is fixing for
  other surfaces.
- `question.test.ts`: regression tests for
  `ALTIMATE_FORCE_INTERACTIVE=1` precedence over `NON_INTERACTIVE=1`
  and for `ALTIMATE_NON_INTERACTIVE=0` honored as explicit opt-out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@sahrizvi

Copy link
Copy Markdown
Contributor Author

Update — addressing review findings (commit d2a8516)

Ran a multi-model consensus review (Claude + GPT 5.4 Codex + Gemini 3.1 Pro + Kimi K2.5 + MiniMax M2.7 + GLM-5.1, 1 convergence round). Resolved 5 findings in this push; 2 deferred to follow-ups.

Fixed in this commit

# Severity Location Change
1 CRITICAL cli/cmd/run.ts:447 !process.stdin.isTTY!process.stdin?.isTTY. Closes the dev-punia-altimate review item in full — the prior commit only resolved it in tool/question.ts.
4 MAJOR cli/cmd/run.ts:387 Gated the ALTIMATE_NON_INTERACTIVE=1 assignment on !args.attach. In attach mode the agent runs on the remote server, so the local env var was a no-op and only polluted the local process env.
3 MAJOR tool/bash.ts:170 delete mergedEnv["ALTIMATE_NON_INTERACTIVE"] before spawn. Without this, child processes (e.g. nested altimate-code serve) inherited the flag and silently disabled their own HTTP question-reply path — exactly the regression this PR fixes for other surfaces.
5 MINOR test/tool/question.test.ts Regression test: ALTIMATE_FORCE_INTERACTIVE=1 overrides ALTIMATE_NON_INTERACTIVE=1.
6 MINOR test/tool/question.test.ts Regression test: ALTIMATE_NON_INTERACTIVE=0 honored as explicit opt-out.

Test count: 11 → 13. All passing locally (bun test test/tool/question.test.ts).

Deferred to follow-ups

These were flagged in the consensus review but are pre-existing architectural concerns out of scope for this PR:

  • session/llm.ts:299 permission-layer mismatch. resolveTools filters tool advertisement using only agent.permission, not the merged session.permission. The permission: question, action: deny rule in run.ts therefore doesn't filter the tool out of the model's tool list (execution-time deny via prompt.ts:1494 still fires, but the question tool calls Question.ask() directly, bypassing ctx.ask). Will track as a separate ticket.
  • Integration test for run actually setting the env var on startup. Unit tests cover the question tool's branching; a spawn-based test would lock in the run.ts contract. Same follow-up.

Convergence review artifacts

Stored at /tmp/code-review-FSSIM4/ (local). Convergence outcome: 1 APPROVE (Gemini), 3 CHANGES NEEDED (GPT, MiniMax, GLM-5) with overlapping accuracy objections — all incorporated. Specifically:

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opencode/test/tool/question.test.ts (1)

119-127: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden env isolation for the non-interactive suite.

Line 119 sets ALTIMATE_NON_INTERACTIVE=1, but ALTIMATE_FORCE_INTERACTIVE is not cleared in this suite. If that var is pre-set by the parent environment, these tests can silently run the interactive path and lose non-interactive coverage.

Suggested fix
  beforeEach(() => {
+   delete process.env["ALTIMATE_FORCE_INTERACTIVE"]
    process.env["ALTIMATE_NON_INTERACTIVE"] = "1"
    askSpy = spyOn(QuestionModule.Question, "ask").mockImplementation(async () => [])
  })

  afterEach(() => {
+   delete process.env["ALTIMATE_FORCE_INTERACTIVE"]
    delete process.env["ALTIMATE_NON_INTERACTIVE"]
    delete process.env["ALTIMATE_AUTO_ANSWER"]
    askSpy.mockRestore()
  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/test/tool/question.test.ts` around lines 119 - 127, The
beforeEach hook sets ALTIMATE_NON_INTERACTIVE but does not clear
ALTIMATE_FORCE_INTERACTIVE, which could cause these non-interactive tests to
silently run in interactive mode if that variable is pre-set in the parent
environment. Add a line in the beforeEach function to explicitly delete
process.env["ALTIMATE_FORCE_INTERACTIVE"] to ensure proper test isolation, and
also add deletion of this variable to the afterEach function for complete
environment cleanup alongside the existing ALTIMATE_NON_INTERACTIVE and
ALTIMATE_AUTO_ANSWER deletions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/cli/cmd/run.ts`:
- Around line 457-460: The stdin-read condition at line 459 in the run.ts file
is too broad and attempts to read stdin whenever process.stdin is not a TTY (or
is undefined), even when a message has already been provided as a positional
argument. Modify the condition to check not only that process.stdin is available
and not a TTY, but also that no message was already provided by the user before
attempting to read from Bun.stdin.text(). This prevents the process from
blocking indefinitely on stdin when message content has already been supplied.

---

Outside diff comments:
In `@packages/opencode/test/tool/question.test.ts`:
- Around line 119-127: The beforeEach hook sets ALTIMATE_NON_INTERACTIVE but
does not clear ALTIMATE_FORCE_INTERACTIVE, which could cause these
non-interactive tests to silently run in interactive mode if that variable is
pre-set in the parent environment. Add a line in the beforeEach function to
explicitly delete process.env["ALTIMATE_FORCE_INTERACTIVE"] to ensure proper
test isolation, and also add deletion of this variable to the afterEach function
for complete environment cleanup alongside the existing ALTIMATE_NON_INTERACTIVE
and ALTIMATE_AUTO_ANSWER deletions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bf81a5f5-387e-436f-ba73-ba864cd4bc88

📥 Commits

Reviewing files that changed from the base of the PR and between f981199 and d2a8516.

📒 Files selected for processing (4)
  • packages/opencode/src/cli/cmd/run.ts
  • packages/opencode/src/tool/bash.ts
  • packages/opencode/src/tool/question.ts
  • packages/opencode/test/tool/question.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opencode/src/tool/question.ts

Comment thread packages/opencode/src/cli/cmd/run.ts Outdated
Comment on lines +457 to +460
// altimate_change start — null-safe stdin access. process.stdin can be
// undefined in embedded/child runtimes (see dev-punia review on PR #937).
if (!process.stdin?.isTTY) message += "\n" + (await Bun.stdin.text())
// altimate_change end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Stdin-read guard is too broad and can reintroduce subprocess hangs.

At Line 459, the condition reads stdin for any non-TTY (and even when process.stdin is undefined), which can block forever on inherited stdin despite a provided positional message. This is the same hang class this PR series is fixing.

Suggested fix
-    if (!process.stdin?.isTTY) message += "\n" + (await Bun.stdin.text())
+    if (process.stdin?.isTTY === false && message.trim().length === 0) {
+      message += "\n" + (await Bun.stdin.text())
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/cli/cmd/run.ts` around lines 457 - 460, The stdin-read
condition at line 459 in the run.ts file is too broad and attempts to read stdin
whenever process.stdin is not a TTY (or is undefined), even when a message has
already been provided as a positional argument. Modify the condition to check
not only that process.stdin is available and not a TTY, but also that no message
was already provided by the user before attempting to read from
Bun.stdin.text(). This prevents the process from blocking indefinitely on stdin
when message content has already been supplied.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/opencode/src/cli/cmd/run.ts Outdated
…view

Two small fixes flagged by coderabbit + cubic on the prior commit
(d2a8516).

- `run.ts:459`: the `!process.stdin?.isTTY` null-safety guard turned a
  crash (undefined stdin) into a stall (we still entered the branch
  and awaited `Bun.stdin.text()` on a stream that would never EOF).
  Use `process.stdin && !process.stdin.isTTY` so undefined stdin
  skips the read entirely — the only sensible behavior in an embedded
  runtime where there is no stdin to read from.
- `question.test.ts`: the `tool.question non-interactive auto-answer`
  describe block sets `ALTIMATE_NON_INTERACTIVE=1` in beforeEach but
  didn't clear `ALTIMATE_FORCE_INTERACTIVE`. A parent-shell export
  could silently flip the suite to the interactive path and lose
  non-interactive coverage. Delete in both hooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@sahrizvi

Copy link
Copy Markdown
Contributor Author

Follow-up — addressing coderabbit + cubic findings on d2a851657 (commit 764291c0c)

Both bots independently flagged the same regression I introduced with my null-safety patch: !process.stdin?.isTTY traded the original crash for a stall (undefined stdin satisfies the guard, then Bun.stdin.text() awaits a stream that never EOFs). Plus a related test-isolation gap.

Changes

Finding Location Change
coderabbit + cubic cli/cmd/run.ts:459 !process.stdin?.isTTYprocess.stdin && !process.stdin.isTTY. Undefined stdin now skips the read entirely — only sensible behavior when there is no stdin to read from.
coderabbit (outside-diff) test/tool/question.test.ts:119-127 tool.question non-interactive auto-answer block now deletes ALTIMATE_FORCE_INTERACTIVE in both beforeEach and afterEach. Without this, a parent-shell FORCE_INTERACTIVE=1 would silently flip the suite to the interactive path and lose all non-interactive coverage.

Not adopted from the suggestions

CodeRabbit additionally suggested that the stdin-read should be skipped when a positional message argument was already provided. That would change current semantics — altimate-code run "summarize" < data.txt is a supported pattern that intentionally combines positional + piped content. Out of scope for this fix.

Tests

13/13 pass on the updated file. All previously-passing question/bash tool tests still pass.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@dev-punia-altimate

Copy link
Copy Markdown
Contributor

❌ Tests — Failures Detected

TypeScript — 15 failure(s)

  • connection_refused
  • timeout
  • permission_denied
  • parse_error [1.00ms]
  • network_error
  • auth_failure
  • rate_limit
  • internal_error
  • empty_error
  • connection_refused
  • timeout
  • permission_denied [1.00ms]
  • parse_error
  • network_error
  • auth_failure

Next Step

Please address the failing cases above and re-run verification.

cc @sahrizvi

@sahrizvi sahrizvi merged commit 9644a00 into main Jun 18, 2026
21 checks passed
sahrizvi pushed a commit that referenced this pull request Jun 18, 2026
Resolved conflict on packages/opencode/src/cli/cmd/run.ts by keeping
the assembleStdinMessage call. PR #937's null-safety against undefined
process.stdin is folded into readStdinIfAvailable in a follow-up commit
on top of this merge.
sahrizvi pushed a commit that referenced this pull request Jun 18, 2026
Addresses suryaiyer95's review of `cc222db2a`:

- Default first-byte timeout bumped from 100ms to 500ms. The previous
  value was aggressive against realistic slow-to-first-byte producers
  (DB queries that need to plan, decompression headers, network calls
  with DNS+TLS handshake). 500ms is generous for those without sacri-
  ficing the inherited-idle wedge fix.

- New `ALTIMATE_STDIN_TIMEOUT_MS=N` env override so a user hitting an
  even slower pipeline can bump it without code changes.

- Stderr note when fd 0 looked like real input (FIFO / file / socket)
  but readStdin came back empty. Silent drop was the original UX
  failure that motivated this whole review thread. The note fires on
  the dominant inherited-idle path too; that's intentional since
  stderr is captured but not surfaced for most subprocess callers
  (Claude Code's Bash tool, CI), and a single line of noise per call
  is worth the explicitness for the slow-producer case.

- `process.stdin` null-safety folded into `readStdinIfAvailable`: in
  embedded / child runtimes (dev-punia review on PR #937) accessing
  `process.stdin.isTTY` could throw. Treat undefined stdin as "no
  input to read".

- 6 new injection tests: env override + invalid env fallback, warning
  fires/doesn't fire across each gate, undefined-stdin guard.

- E2E thresholds adjusted for the 500ms timeout (idle-exit assertion
  bumped to 1500ms, post-timeout sleep bumped to 1200ms).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

question tool blocks indefinitely in non-interactive contexts

4 participants