Skip to content

Non-interactive AskUserQuestion auto-reply and --json-stream#87

Open
kalil0321 wants to merge 1 commit into
mainfrom
cursor/non-interactive-ask-user-json-stream-8e64
Open

Non-interactive AskUserQuestion auto-reply and --json-stream#87
kalil0321 wants to merge 1 commit into
mainfrom
cursor/non-interactive-ask-user-json-stream-8e64

Conversation

@kalil0321
Copy link
Copy Markdown
Owner

@kalil0321 kalil0321 commented May 24, 2026

Summary

  • AskUserQuestion in scripted mode: When --json, --no-interactive, or --json-stream is used, AskUserQuestion no longer opens questionary prompts. Each question receives a fixed auto-reply telling the model to assume reasonable answers or stop and ask the caller to rerun with a clearer prompt.
  • --json-stream: New flag on agent and engineer emits NDJSON progress events on stdout (run_started, tool_start, tool_end, thinking, ask_user_skipped, etc.) and ends with {"event":"result",...} using the same schema as --json. Logs stay on stderr.
  • Cursor default model: Bumped default cursor_model from composer-2 to composer-2.5 (bridge fallback included).

SDK coverage

SDK AskUserQuestion
Claude / agent auto can_use_tool_ask_user_questions
Copilot custom ask_user_question tool → _ask_user_questions
Cursor emits ask_user_skipped when an ask-user-like tool starts (no bridge injection yet)
OpenCode N/A (no AskUserQuestion tool)

Tests

  • tests/test_engineer.py — non-interactive stub + permission handler
  • tests/test_cli_json_stream.py — NDJSON wiring for agent/engineer

Docs

  • Updated website/content/docs/cli/scripted-usage.mdx for --json-stream (no headless doc changes).
Open in Web Open in Cursor 

Summary by cubic

Make AskUserQuestion non-interactive by auto-replying when running with --json, --no-interactive, or --json-stream. Add --json-stream to agent and engineer for NDJSON progress on stdout, and update the default Cursor model to composer-2.5.

  • New Features

    • AskUserQuestion now auto-replies in scripted runs with a fixed message telling the model to assume reasonable answers or ask the caller to rerun with a clearer prompt. Emits an ask_user_skipped event with a count. Applied across Claude/auto and Copilot; Cursor emits the skip event when an ask-user-like tool starts.
    • Added --json-stream to agent and engineer. Streams NDJSON events on stdout (e.g., run_started, tool_start, tool_end, thinking, ask_user_skipped) and ends with {"event":"result",...} using the same schema as --json. Logs remain on stderr. Implies non-interactive mode.
  • Migration

    • Scripted runs will no longer prompt for AskUserQuestion when using --json, --no-interactive, or --json-stream. Remove these flags to enable prompts.
    • Consumers using --json-stream should parse NDJSON and read the final result event for the outcome.
    • Default cursor_model is now composer-2.5. Override cursor_model to composer-2 if you need the previous default.

Written for commit 8840ced. Summary will update on new commits. Review in cubic

Greptile Summary

This PR adds --json-stream (NDJSON progress events) to the agent and engineer commands, makes AskUserQuestion non-interactive in scripted runs by auto-replying with a fixed message, and bumps the default Cursor model to composer-2.5.

  • Non-interactive AskUserQuestion: _ask_user_questions in BaseEngineer now routes to either the interactive questionary UI or a fixed stub reply depending on self.interactive; the same dispatcher is wired into Claude, Auto, and Copilot paths.
  • --json-stream: json_stream.py introduces StreamingUIWrapper and attach_json_stream_to_engineer to intercept UI lifecycle calls and emit NDJSON events; the agent path also emits an undocumented agent_started event after run_started, creating a slightly asymmetric schema versus the engineer path.
  • composer-2.5 default: Updated consistently across config.py, cli.py, cursor_engineer.py, run.mjs, docs, and tests.

Confidence Score: 3/5

Safe to merge after fixing the thinking else-branch crash; the rest of the change is well-scoped.

The StreamingUIWrapper.thinking method contains an else branch that calls self._inner.thinking(text) immediately after hasattr confirmed the attribute does not exist — this will always raise AttributeError if any inner UI lacks a thinking method. It won't crash in current usage if every concrete UI does expose thinking, but the logic is demonstrably wrong and the code path is untested.

src/reverse_api/json_stream.py — the thinking method's else branch and TypeError catch both need attention.

Important Files Changed

Filename Overview
src/reverse_api/json_stream.py New NDJSON streaming module; contains a logic bug in StreamingUIWrapper.thinking where the else branch calls a method that hasattr just confirmed doesn't exist, and uses broad TypeError catching to probe method signatures.
src/reverse_api/base_engineer.py Adds _ask_user_questions dispatcher that routes to interactive UI or a fixed non-interactive stub; logic is clean and both paths (interactive/non-interactive) are correctly handled.
src/reverse_api/cli.py Adds --json-stream flag to agent and engineer, wires the sink through call chains; the agent path emits an undocumented agent_started event in addition to run_started, creating an asymmetric event schema versus the engineer path.
src/reverse_api/engineer.py Adds json_event_sink parameter to run_reverse_engineering and attaches the stream wrapper after engineer construction; ordering is correct relative to start_sync and generate.
src/reverse_api/cursor_engineer.py Emits ask_user_skipped event when a Cursor ask-user-like tool fires in non-interactive mode; note-only (no bridge injection) and clearly documented as such.
src/reverse_api/auto_engineer.py Switches AskUserQuestion handling from _ask_user_interactive to the new _ask_user_questions dispatcher; straightforward and correct.
src/reverse_api/copilot_engineer.py Same _ask_user_questions switch as auto_engineer; change is minimal and correct.
tests/test_cli_json_stream.py New tests cover the NDJSON wiring; tests mock run_agent_capture/run_engineer directly and do not exercise the real attach_json_stream_to_engineer call, so the agent_started vs run_started inconsistency in the real code path is not caught.
tests/test_engineer.py Adds non-interactive stub test and _ask_user_questions routing tests; coverage is appropriate for the new branch.
src/reverse_api/config.py Default cursor_model bumped from composer-2 to composer-2.5; consistent across all affected files.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/reverse_api/cli.py:1852
**`agent_started` event is undocumented and creates an asymmetric event stream.** In the `agent` path, `attach_json_stream_to_engineer` emits `run_started` and then this line immediately emits `agent_started`, so consumers see two startup events. In the `engineer` path (via `run_reverse_engineering`), only `run_started` is emitted. The documentation and PR description list `run_started` as a common event type but make no mention of `agent_started`, which means any consumer parsing the `agent` stream will receive an unexpected extra event before `tool_start`.

### Issue 2 of 3
src/reverse_api/json_stream.py:75-81
**Unreachable `else` branch always raises `AttributeError`.** The `else` block is entered only when `hasattr(self._inner, "thinking")` is `False` — meaning the inner UI does *not* have a `thinking` attribute — yet the very next line calls `self._inner.thinking(text)`, which will always raise `AttributeError`. The intent was clearly to skip silently when the inner UI lacks the method; the call should be removed.

```suggestion
        if hasattr(self._inner, "thinking"):
            try:
                self._inner.thinking(text, max_length=max_length)
            except TypeError:
                self._inner.thinking(text)
```

### Issue 3 of 3
src/reverse_api/json_stream.py:76-79
**Broad `TypeError` catch could silently swallow a real type error from inside `thinking()`.** If the inner `thinking()` body raises `TypeError` for any reason other than the unexpected `max_length` kwarg, the handler will catch it and call `self._inner.thinking(text)` as a fallback, hiding the real error. Consider inspecting the signature once rather than catching by exception.

```suggestion
            import inspect
            sig = inspect.signature(self._inner.thinking)
            if "max_length" in sig.parameters:
                self._inner.thinking(text, max_length=max_length)
            else:
                self._inner.thinking(text)
```

Reviews (1): Last reviewed commit: "Add non-interactive AskUserQuestion auto..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

When interactive=False, AskUserQuestion returns a fixed message instructing
the model to assume reasonable answers instead of blocking on questionary.

Add --json-stream on agent and engineer for NDJSON progress on stdout plus
a terminal result event. Default Cursor model is composer-2.5.

Co-authored-by: kalil0321 <kalil0321@users.noreply.github.com>
@kind-agent
Copy link
Copy Markdown

kind-agent Bot commented May 24, 2026

⚠️ Error — Not enough testing credits. Upgrade your plan or buy credits to continue running tests.

5 similar comments
@kind-agent
Copy link
Copy Markdown

kind-agent Bot commented May 24, 2026

⚠️ Error — Not enough testing credits. Upgrade your plan or buy credits to continue running tests.

@kind-agent
Copy link
Copy Markdown

kind-agent Bot commented May 24, 2026

⚠️ Error — Not enough testing credits. Upgrade your plan or buy credits to continue running tests.

@kind-agent
Copy link
Copy Markdown

kind-agent Bot commented May 24, 2026

⚠️ Error — Not enough testing credits. Upgrade your plan or buy credits to continue running tests.

@kind-agent
Copy link
Copy Markdown

kind-agent Bot commented May 24, 2026

⚠️ Error — Not enough testing credits. Upgrade your plan or buy credits to continue running tests.

@kind-agent
Copy link
Copy Markdown

kind-agent Bot commented May 24, 2026

⚠️ Error — Not enough testing credits. Upgrade your plan or buy credits to continue running tests.

@kalil0321 kalil0321 marked this pull request as ready for review May 25, 2026 14:17
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/reverse_api/cli.py">

<violation number="1" location="src/reverse_api/cli.py:1966">
P2: `engineer --json-stream` does not return machine-readable output for missing `RUN_ID`; it still checks only `as_json` in the early validation path.</violation>
</file>

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

Re-trigger cubic

Comment thread src/reverse_api/cli.py
is_flag=True,
help="Emit NDJSON progress events on stdout during the run, then a final {\"event\":\"result\",...} line. Implies --no-interactive.",
)
def engineer(run_id, prompt, fresh, model, output_dir, no_interactive, as_json, json_stream):
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.

P2: engineer --json-stream does not return machine-readable output for missing RUN_ID; it still checks only as_json in the early validation path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/reverse_api/cli.py, line 1966:

<comment>`engineer --json-stream` does not return machine-readable output for missing `RUN_ID`; it still checks only `as_json` in the early validation path.</comment>

<file context>
@@ -1918,7 +1957,13 @@ def run_auto_capture(
+    is_flag=True,
+    help="Emit NDJSON progress events on stdout during the run, then a final {\"event\":\"result\",...} line. Implies --no-interactive.",
+)
+def engineer(run_id, prompt, fresh, model, output_dir, no_interactive, as_json, json_stream):
     """Run reverse engineering on a previous run."""
     # `run_id` is declared optional at the click level so that wrappers using
</file context>

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review


P1 Badge Emit machine-readable error for missing RUN_ID in --json-stream

When engineer is invoked with --json-stream but no RUN_ID, this branch only checks as_json and falls back to plain-text usage output, so automation expecting NDJSON/JSON gets non-parseable stderr text and no terminal result event. Since this commit adds --json-stream as a scripted output mode, the missing-run-id validation path should treat json_stream the same as --json (including the event wrapper) to keep machine-output behavior consistent.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/reverse_api/cli.py
from .json_stream import attach_json_stream_to_engineer

attach_json_stream_to_engineer(engineer, json_event_sink)
json_event_sink({"event": "agent_started", "mode": mode_label, "url": url})
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.

P2 agent_started event is undocumented and creates an asymmetric event stream. In the agent path, attach_json_stream_to_engineer emits run_started and then this line immediately emits agent_started, so consumers see two startup events. In the engineer path (via run_reverse_engineering), only run_started is emitted. The documentation and PR description list run_started as a common event type but make no mention of agent_started, which means any consumer parsing the agent stream will receive an unexpected extra event before tool_start.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/reverse_api/cli.py
Line: 1852

Comment:
**`agent_started` event is undocumented and creates an asymmetric event stream.** In the `agent` path, `attach_json_stream_to_engineer` emits `run_started` and then this line immediately emits `agent_started`, so consumers see two startup events. In the `engineer` path (via `run_reverse_engineering`), only `run_started` is emitted. The documentation and PR description list `run_started` as a common event type but make no mention of `agent_started`, which means any consumer parsing the `agent` stream will receive an unexpected extra event before `tool_start`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +75 to +81
if hasattr(self._inner, "thinking"):
try:
self._inner.thinking(text, max_length=max_length)
except TypeError:
self._inner.thinking(text)
else:
self._inner.thinking(text)
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.

P1 Unreachable else branch always raises AttributeError. The else block is entered only when hasattr(self._inner, "thinking") is False — meaning the inner UI does not have a thinking attribute — yet the very next line calls self._inner.thinking(text), which will always raise AttributeError. The intent was clearly to skip silently when the inner UI lacks the method; the call should be removed.

Suggested change
if hasattr(self._inner, "thinking"):
try:
self._inner.thinking(text, max_length=max_length)
except TypeError:
self._inner.thinking(text)
else:
self._inner.thinking(text)
if hasattr(self._inner, "thinking"):
try:
self._inner.thinking(text, max_length=max_length)
except TypeError:
self._inner.thinking(text)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/reverse_api/json_stream.py
Line: 75-81

Comment:
**Unreachable `else` branch always raises `AttributeError`.** The `else` block is entered only when `hasattr(self._inner, "thinking")` is `False` — meaning the inner UI does *not* have a `thinking` attribute — yet the very next line calls `self._inner.thinking(text)`, which will always raise `AttributeError`. The intent was clearly to skip silently when the inner UI lacks the method; the call should be removed.

```suggestion
        if hasattr(self._inner, "thinking"):
            try:
                self._inner.thinking(text, max_length=max_length)
            except TypeError:
                self._inner.thinking(text)
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +76 to +79
try:
self._inner.thinking(text, max_length=max_length)
except TypeError:
self._inner.thinking(text)
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.

P2 Broad TypeError catch could silently swallow a real type error from inside thinking(). If the inner thinking() body raises TypeError for any reason other than the unexpected max_length kwarg, the handler will catch it and call self._inner.thinking(text) as a fallback, hiding the real error. Consider inspecting the signature once rather than catching by exception.

Suggested change
try:
self._inner.thinking(text, max_length=max_length)
except TypeError:
self._inner.thinking(text)
import inspect
sig = inspect.signature(self._inner.thinking)
if "max_length" in sig.parameters:
self._inner.thinking(text, max_length=max_length)
else:
self._inner.thinking(text)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/reverse_api/json_stream.py
Line: 76-79

Comment:
**Broad `TypeError` catch could silently swallow a real type error from inside `thinking()`.** If the inner `thinking()` body raises `TypeError` for any reason other than the unexpected `max_length` kwarg, the handler will catch it and call `self._inner.thinking(text)` as a fallback, hiding the real error. Consider inspecting the signature once rather than catching by exception.

```suggestion
            import inspect
            sig = inspect.signature(self._inner.thinking)
            if "max_length" in sig.parameters:
                self._inner.thinking(text, max_length=max_length)
            else:
                self._inner.thinking(text)
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants