Skip to content

feat(agent): generic needs_input NDJSON event + INPUT_REQUIRED exit code#253

Merged
kelsonpw merged 3 commits intomainfrom
kelsonpw/agent-needs-input
Apr 26, 2026
Merged

feat(agent): generic needs_input NDJSON event + INPUT_REQUIRED exit code#253
kelsonpw merged 3 commits intomainfrom
kelsonpw/agent-needs-input

Conversation

@kelsonpw
Copy link
Copy Markdown
Collaborator

@kelsonpw kelsonpw commented Apr 25, 2026

Summary

Adds a typed needs_input NDJSON event so outer agents (Claude Code, Cursor, Codex, custom orchestrators) can deterministically surface decisions to a human instead of the wizard silently auto-selecting. Every event carries code, choices, recommended, and resumeFlags, so the orchestrator can either re-invoke the wizard with a single CLI flag or pipe a JSON line to stdin matching responseSchema.

This is Gap 1 of 4 in the wizard sub-agent contract — the foundation the upcoming plan / apply / verify commands and the --auto-approve / --yes / --force flag matrix all build on.

Why

Today, --agent mode auto-approves prompts and silently picks the first option in promptChoice. Outer agents can't see what was decided or override it. The design doc calls this out as the #1 reliability gap: "every meaningful decision must be surfaced."

promptEnvironmentSelection already does the right thing (emits choices + resumeFlags + responseSchema). This PR generalizes that pattern into a reusable helper and applies it to the other prompt paths.

What's in

  • src/lib/agent-events.ts (new, +110 LOC) — source-of-truth schema for the agent-mode wire format. AgentEventEnvelope, NeedsInputData, AgentEventType all live here so future events land against one doc.
  • AgentUI.emitNeedsInput<V>(data) — public method any caller (including upcoming plan/apply commands) can use to surface a decision. Emits a needs_input event with code, choices, recommended, resumeFlags, responseSchema.
  • promptConfirm / promptChoice now emit a needs_input event in addition to the legacy prompt event so existing orchestrators keep working while new ones key off the canonical shape.
  • promptEnvironmentSelection emits both event shapes; the existing stdin round-trip behavior is unchanged.
  • ExitCode.INPUT_REQUIRED = 12 so future flag-matrix work can exit cleanly when input is needed and --auto-approve is not set.

Wire format

{
  "v": 1,
  "@timestamp": "2026-04-25T12:50:53.123Z",
  "type": "needs_input",
  "message": "Pick an Amplitude project.",
  "session_id": "...",
  "run_id": "...",
  "data": {
    "event": "needs_input",
    "code": "project_selection",
    "choices": [
      { "value": "123", "label": "Production" },
      { "value": "456", "label": "Staging" }
    ],
    "recommended": "456",
    "resumeFlags": [
      { "value": "123", "flags": ["--app-id", "123"] },
      { "value": "456", "flags": ["--app-id", "456"] }
    ],
    "responseSchema": { "appId": "string (required, from choices[].value)" }
  }
}

Test plan

  • pnpm test1240 passed, 17 skipped (3 new tests in agent-ui.test.ts)
  • pnpm tsc --noEmit clean
  • pnpm lint clean
  • Smoke: pnpm try --agent emits needs_input envelope on multi-env auth (alongside the legacy prompt event)

Out of scope

  • plan / apply / verify commands — Gap 3, follow-up PR
  • --auto-approve / --yes / --force flag matrix — Gap 4, follow-up PR
  • Inner-agent lifecycle / file_change_* events — Gap 2, follow-up PR

cc @amplitude/growth

🤖 Generated with Claude Code


Note

Medium Risk
Medium risk because it changes --agent NDJSON output by adding a new top-level event type (needs_input) and introduces a new exit code that orchestrators may depend on; behavior is intended to be backward compatible via continued legacy prompt events.

Overview
Adds a new, typed agent-mode wire-format module (agent-events.ts) that defines the canonical needs_input NDJSON envelope (with code, choices, recommended, resumeFlags, and optional responseSchema) for surfacing decisions to outer orchestrators.

Updates AgentUI to emit needs_input events via a new emitNeedsInput helper, including alongside existing prompt events for promptConfirm, promptChoice, and promptEnvironmentSelection to preserve back-compat while enabling deterministic orchestration.

Introduces ExitCode.INPUT_REQUIRED = 12 for agent runs that must stop after emitting needs_input when external input is required.

Reviewed by Cursor Bugbot for commit a98f240. Bugbot is set up for automated code reviews on this repo. Configure here.

Adds a typed `needs_input` event so outer agents (Claude Code, Cursor,
Codex) can deterministically surface decisions to a human instead of the
wizard silently auto-selecting. Every event carries `code`, `choices`,
`recommended`, and `resumeFlags` so the orchestrator can either re-invoke
with a single CLI flag or pipe a JSON line to stdin.

- New `src/lib/agent-events.ts` — source-of-truth schema for the
  agent-mode wire format. `AgentEventEnvelope`, `NeedsInputData`,
  `AgentEventType` all live here so future events land against one doc.
- `AgentUI.emitNeedsInput()` — public method any caller (including
  upcoming plan/apply commands) can use to surface a decision.
- `promptConfirm` and `promptChoice` now emit a `needs_input` event in
  addition to the legacy `prompt` event so existing orchestrators keep
  working while new ones key off the canonical shape.
- `promptEnvironmentSelection` emits both event shapes; the existing
  stdin round-trip behavior is unchanged.
- `ExitCode.INPUT_REQUIRED = 12` so future flag-matrix work can exit
  cleanly when input is needed and `--auto-approve` is not set.

Tests: +3 in agent-ui.test.ts. Suite green (1240 pass, 17 skip).

Part of the wizard sub-agent design — Gap 1 of 4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kelsonpw kelsonpw requested a review from a team April 25, 2026 19:51
@github-actions
Copy link
Copy Markdown
Contributor

🧙 Wizard CI

Run the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands:

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci django
  • /wizard-ci fastapi
  • /wizard-ci flask
  • /wizard-ci javascript-node
  • /wizard-ci javascript-web
  • /wizard-ci next-js
  • /wizard-ci python
  • /wizard-ci react-router
  • /wizard-ci vue

Test an individual app:

  • /wizard-ci django/django3-saas
  • /wizard-ci fastapi/fastapi3-ai-saas
  • /wizard-ci flask/flask3-social-media
Show more apps
  • /wizard-ci javascript-node/express-todo
  • /wizard-ci javascript-node/fastify-blog
  • /wizard-ci javascript-node/hono-links
  • /wizard-ci javascript-node/koa-notes
  • /wizard-ci javascript-node/native-http-contacts
  • /wizard-ci javascript-web/saas-dashboard
  • /wizard-ci next-js/15-app-router-saas
  • /wizard-ci next-js/15-app-router-todo
  • /wizard-ci next-js/15-pages-router-saas
  • /wizard-ci next-js/15-pages-router-todo
  • /wizard-ci python/meeting-summarizer
  • /wizard-ci react-router/react-router-v7-project
  • /wizard-ci react-router/rrv7-starter
  • /wizard-ci react-router/saas-template
  • /wizard-ci react-router/shopper
  • /wizard-ci vue/movies

Results will be posted here when complete.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: NeedsInputEvent data type mismatches actual wire format
    • Introduced NeedsInputWireData interface (with event discriminator, without message) to accurately reflect the on-wire data shape, and updated NeedsInputEvent to use it instead of NeedsInputData.
  • ✅ Fixed: Duplicate event type definitions across source-of-truth files
    • Removed the duplicate NDJSONEventType union from agent-ui.ts and replaced all usages with AgentEventType imported from the source-of-truth agent-events.ts.

Create PR

Or push these changes by commenting:

@cursor push bbc4170386
Preview (bbc4170386)
diff --git a/src/lib/agent-events.ts b/src/lib/agent-events.ts
--- a/src/lib/agent-events.ts
+++ b/src/lib/agent-events.ts
@@ -102,8 +102,26 @@
   responseSchema?: Record<string, string>;
 }
 
-export type NeedsInputEvent<V = string> = AgentEventEnvelope<NeedsInputData<V>>;
+/**
+ * Wire-format shape of the `data` field in a `needs_input` NDJSON line.
+ *
+ * `emitNeedsInput` hoists `message` to the envelope level and injects the
+ * `event` discriminator, so the on-wire `data` omits `message` and includes
+ * `event: 'needs_input'`.
+ */
+export interface NeedsInputWireData<V = string> {
+  event: 'needs_input';
+  code: string;
+  choices: NeedsInputChoice<V>[];
+  recommended?: V;
+  resumeFlags?: { value: V; flags: string[] }[];
+  responseSchema?: Record<string, string>;
+}
 
+export type NeedsInputEvent<V = string> = AgentEventEnvelope<
+  NeedsInputWireData<V>
+>;
+
 // ── Type guard helpers ──────────────────────────────────────────────
 
 export function isNeedsInputEvent(

diff --git a/src/ui/agent-ui.ts b/src/ui/agent-ui.ts
--- a/src/ui/agent-ui.ts
+++ b/src/ui/agent-ui.ts
@@ -6,7 +6,11 @@
 
 import type { WizardUI, SpinnerHandle, EventPlanDecision } from './wizard-ui';
 import type { RetryState } from '../lib/wizard-session';
-import type { NeedsInputChoice, NeedsInputData } from '../lib/agent-events';
+import type {
+  AgentEventType,
+  NeedsInputChoice,
+  NeedsInputData,
+} from '../lib/agent-events';
 import { createInterface } from 'readline';
 import { z } from 'zod';
 
@@ -180,22 +184,10 @@
 
 // ── NDJSON event types ──────────────────────────────────────────────
 
-type NDJSONEventType =
-  | 'lifecycle'
-  | 'log'
-  | 'status'
-  | 'progress'
-  | 'session_state'
-  | 'prompt'
-  | 'needs_input'
-  | 'diagnostic'
-  | 'result'
-  | 'error';
-
 interface NDJSONEvent {
   v: 1;
   '@timestamp': string;
-  type: NDJSONEventType;
+  type: AgentEventType;
   message: string;
   session_id?: string;
   run_id?: string;
@@ -226,7 +218,7 @@
 }
 
 function emit(
-  type: NDJSONEventType,
+  type: AgentEventType,
   message: string,
   extra?: Omit<NDJSONEvent, 'v' | '@timestamp' | 'type' | 'message'>,
 ): void {

You can send follow-ups to the cloud agent here.

Comment thread src/lib/agent-events.ts Outdated
Comment thread src/lib/agent-events.ts
@kelsonpw
Copy link
Copy Markdown
Collaborator Author

@cursor push bbc4170

…itions

- Add NeedsInputWireData interface that reflects the actual on-wire data
  shape (has 'event' discriminator, omits 'message' which is hoisted to
  the envelope). NeedsInputEvent now uses this instead of NeedsInputData.
- Remove duplicate NDJSONEventType union from agent-ui.ts and import
  AgentEventType from agent-events.ts (the declared source of truth).

Applied via @cursor push command
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Exported function isNeedsInputEvent is never called
    • Removed the unused isNeedsInputEvent function and its preceding comment block from agent-events.ts since grep confirmed it was never imported or called anywhere.
  • ✅ Fixed: Wire format type not enforced at emit site
    • Imported NeedsInputWireData into agent-ui.ts and used it to explicitly type-annotate the payload variable in emitNeedsInput, so the compiler will now catch any drift between the schema and the emitter.

Create PR

Or push these changes by commenting:

@cursor push 105d730422
Preview (105d730422)
diff --git a/src/lib/agent-events.ts b/src/lib/agent-events.ts
--- a/src/lib/agent-events.ts
+++ b/src/lib/agent-events.ts
@@ -121,11 +121,3 @@
 export type NeedsInputEvent<V = string> = AgentEventEnvelope<
   NeedsInputWireData<V>
 >;
-
-// ── Type guard helpers ──────────────────────────────────────────────
-
-export function isNeedsInputEvent(
-  event: AgentEventEnvelope<unknown>,
-): event is NeedsInputEvent<unknown> {
-  return event.type === 'needs_input';
-}

diff --git a/src/ui/agent-ui.ts b/src/ui/agent-ui.ts
--- a/src/ui/agent-ui.ts
+++ b/src/ui/agent-ui.ts
@@ -10,6 +10,7 @@
   AgentEventType,
   NeedsInputChoice,
   NeedsInputData,
+  NeedsInputWireData,
 } from '../lib/agent-events';
 import { createInterface } from 'readline';
 import { z } from 'zod';
@@ -375,16 +376,15 @@
    * emit + exit `ExitCode.INPUT_REQUIRED` (12).
    */
   emitNeedsInput<V = string>(data: NeedsInputData<V>): void {
-    emit('needs_input', data.message, {
-      data: {
-        event: 'needs_input',
-        code: data.code,
-        choices: data.choices,
-        recommended: data.recommended,
-        resumeFlags: data.resumeFlags,
-        responseSchema: data.responseSchema,
-      },
-    });
+    const wireData: NeedsInputWireData<V> = {
+      event: 'needs_input',
+      code: data.code,
+      choices: data.choices,
+      recommended: data.recommended,
+      resumeFlags: data.resumeFlags,
+      responseSchema: data.responseSchema,
+    };
+    emit('needs_input', data.message, { data: wireData });
   }
 
   // ── Logging ─────────────────────────────────────────────────────────

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 9d82337. Configure here.

Comment thread src/lib/agent-events.ts Outdated
Comment thread src/ui/agent-ui.ts
@kelsonpw
Copy link
Copy Markdown
Collaborator Author

@cursor push 105d730

@kelsonpw kelsonpw merged commit 207be5d into main Apr 26, 2026
10 checks passed
kelsonpw added a commit that referenced this pull request Apr 26, 2026
…-gate

Splits the implicit "agent-mode auto-approves everything" behavior into
three explicit, composable capabilities so plan/apply/verify can layer
on without ambiguity:

  --auto-approve  → silently pick `recommended` on `needs_input` (no writes)
  --yes (-y)      → autoApprove + allowWrites (today's --yes / --ci semantics)
  --force         → autoApprove + allowWrites + allowDestructive

Back-compat preserved: `--agent` alone still implies `autoApprove + allowWrites`.
The upcoming `apply` command will pass `requireExplicitWrites: true` to
`resolveMode`, which forces writes to be requested by name.

- `ModeConfig` extends new `CapabilityFlags` interface
- `resolveMode` builds capabilities additively from the flag set
- `evaluateWriteGate(toolName, toolInput, caps)` is a pure function the
  PreToolUse hook can call to decide allow/deny — gates Edit/Write/
  MultiEdit/NotebookEdit on `allowWrites`, and gates a curated set of
  destructive Bash patterns (rm -rf, git reset --hard, git push --force,
  DROP TABLE, etc.) on `allowDestructive`
- New `--auto-approve` and `--force` global flags in bin.ts
- `--yes` consolidated to a single global declaration with `-y` alias
- `ExitCode.WRITE_REFUSED = 13` for clean re-invocation by outer agents

Tests: +16 in mode-config.test.ts (35 total). Suite green (1257 pass).

Part of the wizard sub-agent design — Gap 4 of 4. Stacked on #253.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kelsonpw added a commit that referenced this pull request Apr 26, 2026
…-gate (#254)

Splits implicit "agent-mode auto-approves everything" into three explicit
composable capabilities (autoApprove / allowWrites / allowDestructive)
with a pure write-gate function for the PreToolUse hook to consume.
Stacked on #253; rebased onto current main.

5 follow-up commits address Bugbot review:
  - enforce allowDestructive on write tools when targetFileExists
  - --force implies --yes in command handler (lines 958, 1004)
  - clarify requireExplicitWrites also gates autoApprove
  - --force-with-lease no longer flagged as destructive (negative lookahead)
  - --auto-approve doesn't silently grant writes via --agent back-compat
    (require !hasExplicitCapabilityFlag for back-compat to fire)
  - pnpm/npm/yarn/bun rm no longer flagged as destructive
  - allowDestructive JSDoc honest about caller-context dependency

Bugbot continues to flag 4 of these on every iteration despite the
fixes:
  - f966b46d (allowDestructive contract): JSDoc weakened to match
    reality; gate function defers existence check to caller (per JSDoc).
  - a051102b (--force doesn't imply --yes): Bugbot references lines
    940-941 and 981 of bin.ts which don't exist in the current file.
    Actual --force checks are at lines 958 and 1004; verified by code
    reading + new pinning test in mode-config.test.ts.
  - 041ac1d7 (requireExplicitWrites docs): JSDoc clarified that
    autoApprove is also gated despite the "writes" name.
  - 717acd96 (--auto-approve back-compat): hasExplicitCapabilityFlag
    check added; regression test passes ({agent:true,autoApprove:true}
    → allowWrites=false, isTTY:false).

Each Bugbot concern is now covered by an explicit unit test that pins
the contract. 43 mode-config tests pass; 1340 unit tests overall.
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