feat(agent): generic needs_input NDJSON event + INPUT_REQUIRED exit code#253
feat(agent): generic needs_input NDJSON event + INPUT_REQUIRED exit code#253
Conversation
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>
🧙 Wizard CIRun 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:
Test all apps in a directory:
Test an individual app:
Show more apps
Results will be posted here when complete. |
There was a problem hiding this comment.
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:
NeedsInputEventdata type mismatches actual wire format- Introduced
NeedsInputWireDatainterface (witheventdiscriminator, withoutmessage) to accurately reflect the on-wiredatashape, and updatedNeedsInputEventto use it instead ofNeedsInputData.
- Introduced
- ✅ Fixed: Duplicate event type definitions across source-of-truth files
- Removed the duplicate
NDJSONEventTypeunion fromagent-ui.tsand replaced all usages withAgentEventTypeimported from the source-of-truthagent-events.ts.
- Removed the duplicate
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.
…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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Exported function
isNeedsInputEventis never called- Removed the unused
isNeedsInputEventfunction and its preceding comment block fromagent-events.tssince grep confirmed it was never imported or called anywhere.
- Removed the unused
- ✅ Fixed: Wire format type not enforced at emit site
- Imported
NeedsInputWireDataintoagent-ui.tsand used it to explicitly type-annotate the payload variable inemitNeedsInput, so the compiler will now catch any drift between the schema and the emitter.
- Imported
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.
…emit site Applied via @cursor push command
…-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>
…-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.


Summary
Adds a typed
needs_inputNDJSON 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 carriescode,choices,recommended, andresumeFlags, so the orchestrator can either re-invoke the wizard with a single CLI flag or pipe a JSON line to stdin matchingresponseSchema.This is Gap 1 of 4 in the wizard sub-agent contract — the foundation the upcoming
plan/apply/verifycommands and the--auto-approve/--yes/--forceflag matrix all build on.Why
Today,
--agentmode auto-approves prompts and silently picks the first option inpromptChoice. 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."promptEnvironmentSelectionalready 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,AgentEventTypeall 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 aneeds_inputevent withcode,choices,recommended,resumeFlags,responseSchema.promptConfirm/promptChoicenow emit aneeds_inputevent in addition to the legacypromptevent so existing orchestrators keep working while new ones key off the canonical shape.promptEnvironmentSelectionemits both event shapes; the existing stdin round-trip behavior is unchanged.ExitCode.INPUT_REQUIRED = 12so future flag-matrix work can exit cleanly when input is needed and--auto-approveis 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 test— 1240 passed, 17 skipped (3 new tests inagent-ui.test.ts)pnpm tsc --noEmitcleanpnpm lintcleanpnpm try --agentemitsneeds_inputenvelope on multi-env auth (alongside the legacypromptevent)Out of scope
plan/apply/verifycommands — Gap 3, follow-up PR--auto-approve/--yes/--forceflag matrix — Gap 4, follow-up PRfile_change_*events — Gap 2, follow-up PRcc @amplitude/growth
🤖 Generated with Claude Code
Note
Medium Risk
Medium risk because it changes
--agentNDJSON 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 legacypromptevents.Overview
Adds a new, typed agent-mode wire-format module (
agent-events.ts) that defines the canonicalneeds_inputNDJSON envelope (withcode,choices,recommended,resumeFlags, and optionalresponseSchema) for surfacing decisions to outer orchestrators.Updates
AgentUIto emitneeds_inputevents via a newemitNeedsInputhelper, including alongside existingpromptevents forpromptConfirm,promptChoice, andpromptEnvironmentSelectionto preserve back-compat while enabling deterministic orchestration.Introduces
ExitCode.INPUT_REQUIRED = 12for agent runs that must stop after emittingneeds_inputwhen external input is required.Reviewed by Cursor Bugbot for commit a98f240. Bugbot is set up for automated code reviews on this repo. Configure here.