Interactive parity with the Codex runtime: ask-user-question · plan mode · goal mode#102261
Interactive parity with the Codex runtime: ask-user-question · plan mode · goal mode#102261100yenadmin wants to merge 58 commits into
Conversation
Mirrors ExecApprovalManager: synchronous register, async promise parks the asking tool, resolve/expire/list, emitter hooks for gateway broadcast+delivery. No default timeout (a human may be asleep); explicit expiresAtMs honored.
Structured question (1-3 items, 2-4 options, Recommended-first, auto 'Other'), hard input validation rejecting unknown fields, registers with the global QuestionManager and parks on its promise until any surface answers. Registered unconditionally in the core tool directory (human-in-the-loop, read-only).
…s (U3)
- QuestionResolveParams/QuestionListParams protocol schemas + validators
- question.list (visibility-filtered) and question.resolve gateway handlers
- question.pending/resolved/expired events; manager emitter bound to the
persistent gateway broadcast at startup so the tool's lifecycle reaches
Control UI and channels from outside any request context
- wire-contract integration test asserting the exact {id,answers:{[id]:{text}}}
param shape crosses the client<->gateway seam; drifted param names FAIL
- rename __resetGlobalQuestionManagerForTest -> resetGlobalQuestionManagerForTest (no-underscore-dangle); drop unused protocol re-exports; unknown->GatewayClient double-cast + remove redundant String() in the wire-contract test.
…estions (U5) Kills the hang-trap: /answer <n|text> resolves the session's pending ask_user_question via the process-global QuestionManager, reusing the shared harness answer parser (numbered select, free-text Other, multi-part 'header: answer' lines). Authorized-sender gated; looks up live registrations (no stale session read). Adds the remote-agent scoping caveat comment at the tool registration site (coordinator-approved).
…sweep (U2) Completes U2: a core-owned SessionEntry.pendingQuestion slot (goal-slot precedent, one-line reserved-slot addition) written when ask_user_question registers and cleared when it resolves/expires. On gateway startup a sweep emits question.expired for orphaned breadcrumbs (whose in-memory record died with the previous process) so no Control UI card or channel prompt silently hangs after a restart. Best-effort store writes; back-compat safe (additive optional field). Tests: record/clear, id-guarded clear, pure collect, restart sweep emits+clears, no-op for sessions that never asked.
Telegram: buildQuestionInlineKeyboard -> inline keyboard; each option is a native-command callback running /answer <n> (existing tgcmd: routing), plus a free-text Other button. Slack: buildQuestionAnswerBlocks builds a portable InteractiveReply (callback-value buttons, since Slack gates command-type buttons to exec-approval commands) rendered via the existing buildSlackInteractiveBlocks. Both route clicks to the shared /answer handler. Single-question prompts only; multi-part uses the numbered /answer text fallback. Tests assert exact payload shapes on both channels. Note: the question.pending -> deliver-to-channel forwarder wiring (subscribe + send the rendered buttons to the turn-source chat) remains follow-up; /answer already answers on these channels as text today.
- app/question-card.ts: parse question.pending/list/resolved payloads + queue store - components/question-card.ts: Lit card modelled on exec-approval — numbered options with a Recommended badge on the first, per-question groups, free-text Other input, submit -> question.resolve - overlays.ts: subscribe question.pending/resolved/expired, refresh via question.list on connect, submitQuestionAnswers over the gateway - app-host.ts: mount <openclaw-question-card> beside the exec-approval prompt - i18n: question.* keys in en.ts, propagated to all locales (ui:i18n:sync) Gates: ui:i18n:check, ui:build, ui vitest (store 5 + overlays 3 + exec 25), full pnpm lint — all green.
…(U6) - harness/question-bridge.ts: registerAgentHarnessQuestion adapter registers harness questions with the global QuestionManager and returns a wait promise (harness-shaped answers) + cancel() to dismiss the pending question. - plugin-sdk/agent-harness-runtime: export the adapter + manager accessor/reset. - codex user-input-bridge: on requestUserInput, ALSO register with the manager so it renders as a Control UI card / channel buttons; whichever resolves first — a structured question.resolve OR the legacy next-free-text reply — wins, and resolvePending cancels (expires) the other. Abort also dismisses the question. - race test: structured-wins, free-text-wins (expires the card), abort-dismisses.
…ttons (U5c) - infra/question-forwarder.ts: on question.pending, resolve the record's turn-source target and push a ReplyPayload (question text + option buttons) via the durable outbound path (sendDurableMessageBatch) — reusing the exec-approval delivery route, not a new one. Per-channel button contract matching the standalone renderers: Telegram command-action (/answer <n> native-command callback), Slack callback-action (Slack gates command buttons to approvals). Deduped by record id (pushed once; not re-pushed on reconnect/replay), cleared on resolve/expiry. - question.ts bindQuestionManagerEmitter composes the forwarder sink alongside the broadcast emitter; server.impl wires it at startup. - Codex questions register without turnSourceChannel, so the forwarder no-ops for them (no double delivery) — the codex bridge delivers its own prompt. Tests: telegram push payload, slack push payload, no-turn-source no-op, dedupe. Gates: core+core-test tsgo 0, full pnpm lint 0, touched suites green.
…ls (U1) Core-owned plan-state slot on the session entry (goal-slot precedent): inactive|planning|pending_approval, with a plan-state module mirroring goals.ts. enter_plan_mode flips to planning; exit_plan_mode persists the plan doc under ~/.openclaw/agents/<id>/plans/, requests approval through PR-A's QuestionManager (2-option approve/keep-planning + auto-Other feedback), and returns approved/revise. No parallel approval path. Migration/roundtrip + lifecycle tests included.
…all choke point (U2) Core policy inside runBeforeToolCallHook (skill-workshop core-policy precedent): when the session plan-state is planning|pending_approval, veto every tool not on the read-only allowlist (read/ls/grep/find/search/fetch/update_plan/ask_user_question/enter|exit_plan_mode /read-only session introspection; exec/bash only for read-only command prefixes). Default-deny for unknown/plugin/MCP tools; mutation/read suffix patterns for MCP; dangerous-flag + shell- operator rejection ported from the Smarter-Claw 156-case parity gate. Subagent spawn is blocked (a child runs outside the gate). Deny messages point at exit_plan_mode. Choke-point verified: every in-session dispatch path (built-in incl. exec, plugin/MCP-backed, catalog, code-mode, client tools) funnels through runBeforeToolCallHook with sessionKey+config; the only ctx-less path (ACPX MCP bridge exposing tools externally) is not in-session dispatch. Includes the NON-NEGOTIABLE adversarial bypass suite (exec/MCP/plugin/subagent-spawn all vetoed through the real hook) + gate matrix.
update_plan is now the live plan-mode checklist for every model; tools.experimental.planTool becomes a kill-switch (false removes it, true force-enables). Explicit allowlist still wins; explicit deny of update_plan opts out. update_plan tool description carries plan-mode checklist semantics (keep exactly one step in_progress, then exit_plan_mode). Gating tests rewritten to the default-on contract.
…n summary (U5) /plan show|status (state + summary), /plan accept|approve and /plan reject|revise <feedback> resolve the pending PR-A approval question via the process-global QuestionManager (turn-source routing preserved; the parked exit_plan_mode tool makes the state transition, avoiding the dup-approval bug class), /plan enter and /plan exit aliases. streaming.ts case plan now renders a structured multi-step summary (count + first steps) through the edit-in-place progress-line correlation key. Plan-state module exported from the sessions barrel.
…shot (U4 backend seam) The plan slot rides the same session event payload as goal (state chip + summary), so the Control-UI plan panel reads plan-mode state and the pending-approval flag without a new gateway method — approve/reject reuse the existing question.pending/question.resolve events (no new approval path) and the live checklist reuses the existing stream:plan broadcast. GatewaySessionRow carries plan; the builder emits an explicit null when inactive so clients drop the chip on merge. NOTE: the Lit panel view itself is intentionally deferred — see manifest (no panel precedent in this base + PR-A's question-card UI lands in the parallel lane; building it here would collide).
The compile-time _AssertAllSessionEntryKeysAreReserved guard requires every SessionEntry key to be in the plugin-collision reserved list; add 'plan' next to 'goal' (same precedent).
…tend) Self-contained plan-panel render component: plan-mode state chip (Planning / Awaiting approval), live plan checklist with per-step status icons + accessible labels, progress summary, presented plan summary, and — only when awaiting approval — Approve / Keep-planning controls. The controls send /plan accept|reject through the existing composer command sender, which resolves the PR-A approval question (no new approval path). Reads plan state from the session snapshot (U4 backend seam). Full i18n: new plan.* keys in en.ts propagated to all 20 locales (English fallback pending localization). Panel CSS matches the goal-chip design tokens; render test covers chip/checklist/ approval-controls/empty states. The live stream:plan checklist data-source threading is the one remaining seam (panel already renders a checklist when fed; mounted with session-derived state today).
Exercises the REAL exit_plan_mode <-> QuestionManager <-> /plan accept|reject seam (no mocks): the tool parks on the PR-A question, the channel command resolves it through the same process-global manager, and the tool returns approved (state cleared) / revise (back to planning with feedback). Completes U6 alongside the state-machine, gate-matrix, adversarial-bypass, panel-render, and store-migration suites.
…ignment, dangle) Brace the setTimeout poll executors, collapse the plan-status read to a single expression, and drop the unused __planModeGateContract introspection export.
…seam 5) handleAgentEvent captures each update_plan tool result (the stream:plan / tool agent-event path) into a per-session plan-checklist store; the chat composer reads it when mounting the panel, so the checklist updates in real time with per-step status transitions as the agent plans. Re-render + coalescing reuse the existing throttled scheduleToolStreamSync (no new throttle); cleared on run reset. Pure extraction + types live in a dependency-free lib/plan-checklist.ts so the node-tested tool-stream module never pulls in i18n/lit. Tests: extraction unit + a live stream:plan sequence asserting the panel re-renders step transitions.
… (U1) Adopt the spike GoalContinuationDriver (4-gate debounced scheduler, no self-re-arm after fire) and bind it to real gateway primitives in a testable createGoalDriverService factory. Ships behind tools.experimental.goalDriver (default off) with schema + labels + help. - Durable no-progress counter: first writers of SessionGoal.continuationTurns (recordSessionGoalContinuation / resetSessionGoalContinuations via patchSessionEntry) so the ceiling survives a gateway restart. - Fold the driver continuation marker into isFormattedGoalContinuationPrompt so both /goal and driver turns are classified uniformly (ceiling reset). - FIRE = enqueueSystemEventEntry + immediate targeted heartbeat together (enqueue alone strands the event, per spike). Spike suites: 11/11 pass. tsgo:core clean.
- extractGoalBudgetFlag parses inline --budget N|=N off the objective for /goal set|start|create (codex parity). - Add /goal stop as the host-facing verb for ending goal pursuit (clears the goal, disarming the driver on its next wake). - Host-only pause/resume already enforced: update_goal tool restricts the model to complete|blocked (MODEL_UPDATABLE_SESSION_GOAL_STATUSES); no fix. commands-goal suite: 14/14 pass.
…ility tests (U1/U3/U5) Wire the driver into the live gateway (behind tools.experimental.goalDriver): - startGoalDriverWiring: rearm persisted active goals on startup; arm the idle debounce on the clearTrackedActiveRun turn-completed seam; classify each turn as a driver continuation (via the driver's own 'fired' events) vs a real inbound turn (ceiling reset). - Register + broadcast 'goal.updated' on driver-initiated status changes (auto-pause); teardown via server-close goal-driver-stop step. Tests (U5): - goals.test: durable continuation-counter record/reset survives reload. - goal-driver-wiring.test: flag gate; fire onto system-event queue + heartbeat wake together; auto-pause ceiling -> durable goal.updated; SAFETY: a driver-continued turn does NOT bypass the exec-approval gate. Suites: goal-driver 11/11, wiring 4/4, commands-goal 14/14, goals 18/18. tsgo core + core-test clean.
…utations (U3)
Backend: single-source goal.updated broadcast via a goals.ts change-emitter
(setGoalUpdatedEmitter/emitGoalUpdated), bound to the gateway in
bindGoalUpdatedBroadcast — every mutation path (/goal commands, model tools,
driver auto-pause) now emits goal.updated {sessionKey,status,objective,
tokensUsed,tokenBudget,source}. Drops the driver wiring's separate broadcast
(dedupe). /goal edit gains --budget N (updateSessionGoalObjective sets budget).
Control UI: openclaw-goal-chip Lit component (persistent 'Pursuing goal' chip
+ objective/status/usage) and edit dialog (objective textarea + budget +
pause/resume/stop/save). overlays.ts subscribes to goal.updated and routes chip
actions through the existing /goal host verbs via chat.send. i18n en keys +
sync-generated fallbacks for all 20 locales (ui:i18n:check clean). Mounted in
app-host next to the question card.
Tests: goal-chip store 12 + goalActionCommand, component render 6 (15 UI total),
commands-goal 14, goal-driver 11, wiring 4. ui:build green; tsgo core + test:ui
clean; oxlint clean on touched files.
1. Plan-approval gate — the Control-UI mode selector sends `/plan exit` when switching away from plan mode, and `/plan exit` approved+executed a pending plan, so merely switching modes could execute an unapproved plan. `/plan exit` now discards a pending plan (resolves "Keep planning" — no execution) before leaving plan mode; approval stays explicit (`/plan accept` or the plan card's Approve). Root-level fix so every caller is safe; + regression test. 2. Plugin SDK surface — stop re-exporting getGlobalQuestionManager and resetGlobalQuestionManagerForTest (a test-only reset helper) from the public plugin-sdk/agent-harness-runtime entrypoint; all real consumers import them directly from the gateway module (types kept). Re-pins the public surface budget (exports 10480->10476, function exports 5229->5225). Verified: surface-report 9/9, commands-plan 7/7 (incl. new /plan exit regression), tsgo:prod clean, plan-mode-tools + ask-user-question 8/8.
|
@clawsweeper re-review please Both code P1s from the last review are fixed in
The remaining blockers you flagged are maintainer-gated, not code I can resolve honestly: redacted live-channel proof for the Slack/Telegram question buttons, and product sign-off on |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Follow-up to the prior commit: removing getGlobalQuestionManager and resetGlobalQuestionManagerForTest from the public agent-harness-runtime entrypoint broke check-test-types — the codex and slack extension TESTS imported them from that public SDK path, and tsgo:prod does not typecheck test files so it slipped through. Move both to plugin-sdk/plugin-test-runtime (the test-only entrypoint, excluded from the published package via package.json !dist), keeping them out of the production surface per the ClawSweeper finding, and redirect the two extension test imports there. Verified: tsgo:test clean; surface budget unchanged [10476, 5225, 212].
GitHub did not schedule the ci.yml (pull_request) workflow for the previous push (15c377c); only the pull_request_target governance workflows ran. Empty commit to force the full CI suite — including check-test-types — to run against the tree that contains the plugin-test-runtime fix.
…rity # Conflicts: # apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json # scripts/plugin-sdk-surface-report.mjs # src/agents/tool-display-config.ts # ui/src/app/app-host.ts # ui/src/app/overlays.ts # ui/src/i18n/.i18n/ar.meta.json # ui/src/i18n/.i18n/de.meta.json # ui/src/i18n/.i18n/es.meta.json # ui/src/i18n/.i18n/fa.meta.json # ui/src/i18n/.i18n/fa.tm.jsonl # ui/src/i18n/.i18n/fr.meta.json # ui/src/i18n/.i18n/fr.tm.jsonl # ui/src/i18n/.i18n/hi.meta.json # ui/src/i18n/.i18n/hi.tm.jsonl # ui/src/i18n/.i18n/id.meta.json # ui/src/i18n/.i18n/id.tm.jsonl # ui/src/i18n/.i18n/it.meta.json # ui/src/i18n/.i18n/it.tm.jsonl # ui/src/i18n/.i18n/ja-JP.meta.json # ui/src/i18n/.i18n/ko.meta.json # ui/src/i18n/.i18n/nl.meta.json # ui/src/i18n/.i18n/nl.tm.jsonl # ui/src/i18n/.i18n/pl.meta.json # ui/src/i18n/.i18n/pl.tm.jsonl # ui/src/i18n/.i18n/pt-BR.meta.json # ui/src/i18n/.i18n/pt-BR.tm.jsonl # ui/src/i18n/.i18n/ru.meta.json # ui/src/i18n/.i18n/ru.tm.jsonl # ui/src/i18n/.i18n/th.meta.json # ui/src/i18n/.i18n/th.tm.jsonl # ui/src/i18n/.i18n/tr.meta.json # ui/src/i18n/.i18n/tr.tm.jsonl # ui/src/i18n/.i18n/uk.meta.json # ui/src/i18n/.i18n/vi.meta.json # ui/src/i18n/.i18n/vi.tm.jsonl # ui/src/i18n/.i18n/zh-CN.meta.json # ui/src/i18n/.i18n/zh-CN.tm.jsonl # ui/src/i18n/.i18n/zh-TW.meta.json # ui/src/i18n/.i18n/zh-TW.tm.jsonl # ui/src/pages/chat/chat-pane.ts # ui/src/pages/chat/chat-view.ts # ui/src/pages/chat/components/chat-composer.ts # ui/src/pages/chat/components/chat-controls.ts # ui/src/pages/chat/components/chat-sidebar.ts
|
@clawsweeper re-review please Head Two architectural reconciliations adopt main's canonical implementations (neither is a parity feature):
Also follows main: |
|
🦞🧹 I asked ClawSweeper to review this item again. |
no-import-type-side-effects: the inline { type X } form leaves a
side-effect import after type-stripping; both check-lint and
check-additional-extension-bundled flagged the same line.
…rity # Conflicts: # scripts/plugin-sdk-surface-report.mjs # ui/src/i18n/.i18n/ar.meta.json # ui/src/i18n/.i18n/de.meta.json # ui/src/i18n/.i18n/es.meta.json # ui/src/i18n/.i18n/fa.meta.json # ui/src/i18n/.i18n/fr.meta.json # ui/src/i18n/.i18n/hi.meta.json # ui/src/i18n/.i18n/id.meta.json # ui/src/i18n/.i18n/it.meta.json # ui/src/i18n/.i18n/ja-JP.meta.json # ui/src/i18n/.i18n/ko.meta.json # ui/src/i18n/.i18n/nl.meta.json # ui/src/i18n/.i18n/pl.meta.json # ui/src/i18n/.i18n/pt-BR.meta.json # ui/src/i18n/.i18n/ru.meta.json # ui/src/i18n/.i18n/th.meta.json # ui/src/i18n/.i18n/tr.meta.json # ui/src/i18n/.i18n/uk.meta.json # ui/src/i18n/.i18n/vi.meta.json # ui/src/i18n/.i18n/zh-CN.meta.json # ui/src/i18n/.i18n/zh-TW.meta.json
…rity # Conflicts: # scripts/plugin-sdk-surface-report.mjs # ui/src/i18n/.i18n/ar.meta.json # ui/src/i18n/.i18n/de.meta.json # ui/src/i18n/.i18n/es.meta.json # ui/src/i18n/.i18n/fa.meta.json # ui/src/i18n/.i18n/fr.meta.json # ui/src/i18n/.i18n/hi.meta.json # ui/src/i18n/.i18n/id.meta.json # ui/src/i18n/.i18n/it.meta.json # ui/src/i18n/.i18n/ja-JP.meta.json # ui/src/i18n/.i18n/ko.meta.json # ui/src/i18n/.i18n/nl.meta.json # ui/src/i18n/.i18n/pl.meta.json # ui/src/i18n/.i18n/pt-BR.meta.json # ui/src/i18n/.i18n/ru.meta.json # ui/src/i18n/.i18n/th.meta.json # ui/src/i18n/.i18n/tr.meta.json # ui/src/i18n/.i18n/uk.meta.json # ui/src/i18n/.i18n/uk.tm.jsonl # ui/src/i18n/.i18n/vi.meta.json # ui/src/i18n/.i18n/zh-CN.meta.json # ui/src/i18n/.i18n/zh-TW.meta.json
|
@clawsweeper re-review please Head is now |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…rity # Conflicts: # ui/src/i18n/.i18n/ar.meta.json # ui/src/i18n/.i18n/de.meta.json # ui/src/i18n/.i18n/es.meta.json # ui/src/i18n/.i18n/es.tm.jsonl # ui/src/i18n/.i18n/fa.meta.json # ui/src/i18n/.i18n/fr.meta.json # ui/src/i18n/.i18n/hi.meta.json # ui/src/i18n/.i18n/id.meta.json # ui/src/i18n/.i18n/it.meta.json # ui/src/i18n/.i18n/ja-JP.meta.json # ui/src/i18n/.i18n/ko.meta.json # ui/src/i18n/.i18n/ko.tm.jsonl # ui/src/i18n/.i18n/nl.meta.json # ui/src/i18n/.i18n/pl.meta.json # ui/src/i18n/.i18n/pt-BR.meta.json # ui/src/i18n/.i18n/ru.meta.json # ui/src/i18n/.i18n/th.meta.json # ui/src/i18n/.i18n/tr.meta.json # ui/src/i18n/.i18n/uk.meta.json # ui/src/i18n/.i18n/vi.meta.json # ui/src/i18n/.i18n/zh-CN.meta.json # ui/src/i18n/.i18n/zh-TW.meta.json
Master hub: #102260 (architecture diagrams, the full test map, and the follow-up roadmap live there).
The full flow — plan mode, the live plan pane, inline approval, and an inline agent question. A 30s narrated walkthrough covers goal mode too.
Why this shape
The primitives were already merged (
update_planexperimental, goal state tools, a codex user-input bridge,stream:planevents, exec-approval UI, Telegram/Slack buttons) — this PR is the completion layer, and every feature rides an existing choke point rather than inventing a parallel system. One branch, take-apart commit groups (reviewable and revertable independently; happy to split into stacked PRs on request) — the three feature groups, then a Control-UI pass bringing the surface to Codex-Desktop parity (inline composer cards, right-sidebar plan, mode selector, goal modal):ask_user_questiontool · global QuestionManager (exec-approval pattern) ·question.list/resolve+ events · Control-UI card · Telegram/Slack buttons + proactive push ·/answer· restart expiry-sweep · codex-bridge convergence (structured vs free-text race)enter/exit_plan_mode· read-only gate at the single before-tool-call choke point ·update_planpromoted default-on · live plan panel ·/plancommands + structured channel summaries · exit-plan approval rides the question infra/goal set/edit/pause/resume/stop --budget· "Pursuing goal" pill + edit modal · single-sourcegoal.updated· behindtools.experimental.goalDriverWhat each feature does
1 · Structured questions
ask_user_question(1-3 questions, 2-4 options, first recommended, auto-"Other") parks the tool on a global-manager promise — the exec-approval pattern — so the answer can come from anywhere: an inline card in the chat composer (Codex-Desktop style, not a page-locking modal), Telegram inline buttons or Slack Block Kit (pushed proactively to the turn-source chat, deduped), or/answer <n|text>on any channel. Ask on Telegram, answer in the browser. No default timeout (humans sleep); a gateway restart expires pending questions loudly (question.expired) instead of hanging. Codex-runtime sessions converge: theirrequest_user_input(already held open as a JSON-RPC request) now renders the same card, racing the structured answer against the legacy next-free-text path.2 · Plan mode
The mode selector by the gear (or
/plan/enter_plan_mode) puts the session in a read-only collaboration mode enforced as a core policy at the one choke point every tool execution funnels through — built-ins incl. exec and subagent spawn, plugin/MCP-backed, catalog, code-mode, client tools. The agent researches and maintains a live checklist (update_plan, promoted from experimental with a kill-switch) that ticks in real time in the right-sidebar plan pane (always viewable, full rendered plan; a compact chip stays inline);exit_plan_modepersists the plan to~/.openclaw/agents/<id>/plans/and asks for approval as an inline composer card (one pipeline for all human-in-the-loop moments); reject-with-feedback loops the agent back into planning. The mode selector keeps plan and goal mutually exclusive.3 · Goal mode
/goal set "<objective>" --budget <n>(or the chip dialog) and walk away. The driver mirrors the Codex goal extension's idle-continuation semantics, adapted honestly to OpenClaw's architecture (no idle event exists — a spike proved the safe shape first): a debounced scheduler armed only by turn-completion, firing only when four gates pass (goal active · no active run · queue empty · no-progress ceiling not hit), via the cron system-event path + a heartbeat wake. After a fire it never self-re-arms — cadence is structurally bounded, no tight loop. Ceiling hit → auto-pause. Budget hit → disarm. The model can only markcomplete|blocked(host-only pause, mirroring Codex).Security
Real behavior proof
Behavior or issue addressed: agents couldn't ask, couldn't plan reviewably, couldn't pursue; codex
request_user_inputdegraded to plain text;update_planevents rendered as a bare text line.Evidence (full gate on the final tip, run and read by the submitting orchestrator): 9 backend suites in one run (question manager/wire-contract integration/tool/forwarder/sweep/
/answer· plan state machine + adversarial bypass matrix + approval wire-contract · goal driver loop-safety + integration probe + wiring/safety + commands + durability) — all green;tsgocore + test 0; UI suites 781/784 (the 3 = the long-documentedterminal-panelfixture flake, failing identically on pristinemain— verified);ui:i18n:checkclean;ui:build0; full type-awarepnpm lint0. The wire-contract integration tests land IN this PR: client↔gateway param drift fails CI here, not in production.▶ Full 30s narrated walkthrough (mp4) — questions, plan mode, the sidebar plan, inline approval, and goal mode running unattended.
Dependency note
No new dependencies; no lockfile change. Plan-mode semantics derive from a 156-case parity-hardened prior implementation; goal-driver semantics mirror
codex-rs/ext/goalfor cross-runtime consistency. Codex plan/goal wire bridges are documented in-code as follow-up seams (hub roadmap) — no speculative plumbing.