[Plan Mode 3/8] Advanced plan interactions#70015
Conversation
There was a problem hiding this comment.
Pull request overview
This PR advances “plan mode” interactions and runtime wiring: plan-mode tool registration, mutation gating, approval/question interactions, skill plan-template seeding, plan persistence helpers, and additional retry/auto-continue behavior in the embedded runner.
Changes:
- Add/extend plan-mode protocol/schema + runner wiring (planMode/planApproval/lastPlanSteps, pending injection plumbing, mutation gate hook).
- Introduce new plan-mode tools and prompt steering (enter_plan_mode, ask_user_question, plan archetype prompt/persistence helpers).
- Add skill plan-template parsing/seeding and broaden test coverage across plan-mode components.
Reviewed changes
Copilot reviewed 57 out of 57 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/gateway/protocol/schema/sessions.ts | Adds sessions.patch fields for planMode/planApproval/lastPlanSteps with TypeBox schema + docs. |
| src/config/zod-schema.ts | Adds config cap for skill plan-template step seeding. |
| src/config/zod-schema.agent-defaults.ts | Adds embeddedPi autoContinue config schema + planMode enabled flag schema. |
| src/config/types.skills.ts | Documents/exports maxPlanTemplateSteps limit. |
| src/config/types.agent-defaults.ts | Adds embeddedPi autoContinue config typing + planMode enabled typing; tweaks compaction notifyUser docs. |
| src/auto-reply/reply/agent-runner-execution.ts | Threads “fresh” session state + pending injection consumption into agent run execution. |
| src/agents/tools/update-plan-tool.parity.test.ts | Adds parity + merge-mode tests for update_plan behavior. |
| src/agents/tools/exit-plan-mode-tool.test.ts | Adds exit_plan_mode subagent-gate + archetype-field tests. |
| src/agents/tools/enter-plan-mode-tool.ts | Adds enter_plan_mode tool implementation. |
| src/agents/tools/ask-user-question-tool.ts | Adds ask_user_question tool implementation + validation. |
| src/agents/tools/ask-user-question-tool.test.ts | Adds tests for ask_user_question tool behavior. |
| src/agents/tool-description-presets.ts | Adds plan-mode tool display summaries and richer plan-mode tool descriptions. |
| src/agents/tool-catalog.ts | Registers plan-mode tools in the catalog (gated by config). |
| src/agents/test-helpers/fast-openclaw-tools-sessions.ts | Updates update_plan mock signature + exported statuses for tests. |
| src/agents/skills/workspace.ts | Carries resolvedPlanTemplates into skill snapshot so seeding works in snapshot-backed runs. |
| src/agents/skills/types.ts | Adds planTemplate typing to skill metadata + snapshot. |
| src/agents/skills/skill-planner.ts | Implements plan-template normalization (dedupe/truncate) + helpers. |
| src/agents/skills/skill-planner.test.ts | Adds tests for skill plan-template payload building + seeding paths. |
| src/agents/skills/frontmatter.ts | Parses plan-template frontmatter (kebab/camel + content alias) into metadata. |
| src/agents/skills/frontmatter.test.ts | Adds tests for plan-template frontmatter parsing and precedence. |
| src/agents/plan-store.test.ts | Adds comprehensive PlanStore behavior/security tests (namespaces, locking, confinement). |
| src/agents/plan-mode/types.ts | Adds plan-mode state types + approvalId generation + decision injection builder. |
| src/agents/plan-mode/plan-archetype-prompt.ts | Adds plan archetype prompt fragment + filename/slug helpers. |
| src/agents/plan-mode/plan-archetype-prompt.test.ts | Tests prompt fragment and filename helper behavior. |
| src/agents/plan-mode/plan-archetype-persist.ts | Persists plan markdown artifacts safely (collision handling, confinement, error classification). |
| src/agents/plan-mode/plan-archetype-persist.test.ts | Tests plan persistence behavior, collisions, confinement, and error wrapping. |
| src/agents/plan-mode/mutation-gate.ts | Implements plan-mode mutation gate (default-deny + exec read-only allowlist). |
| src/agents/plan-mode/mutation-gate.test.ts | Tests mutation gate behavior and exec whitelist rules. |
| src/agents/plan-mode/integration.test.ts | End-to-end wiring smoke test for plan-mode gating + tool enablement. |
| src/agents/plan-mode/index.ts | Barrel exports for plan-mode module. |
| src/agents/plan-mode/approval.ts | Implements plan approval state machine resolver + approved-plan injection helper. |
| src/agents/plan-mode/approval.test.ts | Tests approval state transitions, stale-id guard, and injection builders. |
| src/agents/plan-hydration.ts | Adds post-compaction plan hydration formatter. |
| src/agents/plan-hydration.test.ts | Tests plan hydration formatting/filtering. |
| src/agents/pi-tools.ts | Threads planMode into before-tool-call hook context. |
| src/agents/pi-tools.before-tool-call.ts | Enforces mutation gate in before_tool_call pipeline when planMode is active. |
| src/agents/pi-embedded-runner/skills-runtime.ts | Adds skill plan-template resolution + seeding + snapshot fallback. |
| src/agents/pi-embedded-runner/run/params.ts | Adds planMode param to embedded runner entrypoint params. |
| src/agents/pi-embedded-runner/run/incomplete-turn.ts | Adjusts planning-only retry limits/instructions; adds auto-continue fast-path instruction; introduces planModeActive gating. |
| src/agents/pi-embedded-runner/run/attempt.ts | Seeds plan from skill templates; injects plan-mode prompt fragments into system prompt; forwards planMode. |
| src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts | Stubs skill-template seeder in spawn-workspace test support. |
| src/agents/pi-embedded-runner/run.ts | Threads planMode through run → attempt; adds autoContinue loop behavior; modifies retry instruction escalation; changes hook selection call. |
| src/agents/pi-embedded-runner/run.incomplete-turn.test.ts | Updates/increases retry expectations; adds tests for auto-continue and escalation helper. |
| src/agents/openclaw-tools.ts | Registers plan-mode tools (gated) and threads runId into update_plan + sessions_spawn. |
| src/agents/openclaw-tools.registration.ts | Adds plan-mode tool enablement gate based on config. |
| apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift | Regenerates Swift protocol structs to include new sessions.patch fields; removes ChannelsStartParams. |
| apps/macos/Sources/OpenClawProtocol/GatewayModels.swift | Same Swift regeneration for macOS target. |
Comments suppressed due to low confidence (1)
src/agents/pi-embedded-runner/run.ts:312
resolveHookModelSelectionsupports passingattachmentsinto the before_model_resolve hook, but this call site no longer supplies attachments derived fromparams.images. That changes plugin behavior for vision-capable prompts (hooks can no longer route based on image presence/mimeType). If this wasn't intentional, passbuildBeforeModelResolveAttachments(params.images)again so hooks keep the full context.
const hookSelection = await resolveHookModelSelection({
prompt: params.prompt,
provider,
modelId,
hookRunner,
hookContext: hookCtx,
});
| * Per-variant requirements: | ||
| * - `approve` / `edit`: only `approvalId` (optional but | ||
| * recommended for staleness protection). | ||
| * - `reject`: optional `feedback` (capped to 8 KiB to bound | ||
| * the prompt-cache hash explosion vector — PR-11 H4). | ||
| * - `answer`: REQUIRES `answer` text and `approvalId` (Codex P1 |
There was a problem hiding this comment.
The doc comment says the reject planApproval variant has optional feedback, but the schema below makes feedback required (minLength: 1). Update the per-variant requirements bullet to match the actual contract so clients don't implement against the wrong semantics.
| import { | ||
| composePromptWithPendingInjection, | ||
| consumePendingAgentInjection, | ||
| } from "../../agents/pi-embedded-runner/pending-injection.js"; |
There was a problem hiding this comment.
This import points at ../../agents/pi-embedded-runner/pending-injection.js, but there is no pending-injection module under src/agents/pi-embedded-runner/ in the repo. As-is, this will fail build/tsc at module resolution time. Add the missing module (with these exports) or update the import to the correct existing file.
| } from "../../pi-tool-definition-adapter.js"; | ||
| import { createOpenClawCodingTools, resolveToolLoopDetectionConfig } from "../../pi-tools.js"; | ||
| import { PLAN_ARCHETYPE_PROMPT } from "../../plan-mode/plan-archetype-prompt.js"; | ||
| import { PLAN_MODE_REFERENCE_CARD } from "../../plan-mode/reference-card.js"; |
There was a problem hiding this comment.
PLAN_MODE_REFERENCE_CARD is imported from ../../plan-mode/reference-card.js, but there is no src/agents/plan-mode/reference-card.ts/js in this PR/repo. This will fail module resolution at build time. Either add the missing reference-card module or remove/replace this import (and the usage) with an existing source.
| import { PLAN_MODE_REFERENCE_CARD } from "../../plan-mode/reference-card.js"; | |
| const PLAN_MODE_REFERENCE_CARD = ""; |
| import { createMusicGenerateTool } from "./tools/music-generate-tool.js"; | ||
| import { createNodesTool } from "./tools/nodes-tool.js"; | ||
| import { createPdfTool } from "./tools/pdf-tool.js"; | ||
| import { createPlanModeStatusTool } from "./tools/plan-mode-status-tool.js"; |
There was a problem hiding this comment.
createPlanModeStatusTool is imported from ./tools/plan-mode-status-tool.js, but there is no corresponding module under src/agents/tools/ in the repo. This will fail compilation/module resolution. Add the missing tool implementation file (and ensure it's exported/registered appropriately), or remove the import/usage if it was not meant to land in this slice.
| import { createPlanModeStatusTool } from "./tools/plan-mode-status-tool.js"; |
| "update_plan", | ||
| "exit_plan_mode", |
There was a problem hiding this comment.
PLAN_MODE_ALLOWED_TOOLS omits newly introduced plan-mode tools (e.g. ask_user_question, enter_plan_mode, plan_mode_status). Because this gate is default-deny, those tool calls will be blocked while planMode is active, breaking the intended plan-mode interactions. Add the missing tool names to the allowlist (and add/adjust mutation-gate tests to cover them).
| "update_plan", | |
| "exit_plan_mode", | |
| "update_plan", | |
| "ask_user_question", | |
| "enter_plan_mode", | |
| "exit_plan_mode", | |
| "plan_mode_status", |
Greptile SummaryThis PR adds the core advanced plan-mode interactions: clarifying questions (
Confidence Score: 4/5Two P1 regressions block intended UX paths; safe to merge after fixing both. Both P1 findings are contained to the plan-mode feature (not core agent execution or data integrity), but one makes ask_user_question completely non-functional in plan mode and the other silently degrades all plan-approval error discrimination to generic INVALID_REQUEST. Fixes are small: four lines to error-codes.ts and two lines to mutation-gate.ts. src/gateway/protocol/schema/error-codes.ts (missing plan-mode error code definitions) and src/agents/plan-mode/mutation-gate.ts (PLAN_MODE_ALLOWED_TOOLS missing ask_user_question/enter_plan_mode). Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/gateway/sessions-patch.ts
Line: 798
Comment:
**New plan-mode error codes missing from `error-codes.ts`**
`ErrorCodes.PLAN_APPROVAL_EXPIRED` (line 798) and `ErrorCodes.PLAN_APPROVAL_GATE_STATE_UNAVAILABLE` (line 850) access keys that don't exist on the `ErrorCodes` const object — they evaluate to `undefined` at runtime. Because `invalid()` uses `code ?? ErrorCodes.INVALID_REQUEST`, both calls silently fall back to `"INVALID_REQUEST"`. Client code that branches on these specific codes (e.g. the "auto-dismiss stale approval card" logic described in the inline comment for `PLAN_APPROVAL_EXPIRED`) will never match and the intended UX behaviour is permanently suppressed.
The two string-literal forms on lines 879 and 890 (`"PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS"`, `"PLAN_APPROVAL_WAITING_FOR_SUBAGENT_SETTLE"`) survive at runtime but are TypeScript type errors because they are not assignable to `ErrorCode`.
All four codes need to be added to `src/gateway/protocol/schema/error-codes.ts`:
```typescript
export const ErrorCodes = {
// ... existing codes ...
PLAN_APPROVAL_EXPIRED: "PLAN_APPROVAL_EXPIRED",
PLAN_APPROVAL_GATE_STATE_UNAVAILABLE: "PLAN_APPROVAL_GATE_STATE_UNAVAILABLE",
PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS: "PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS",
PLAN_APPROVAL_WAITING_FOR_SUBAGENT_SETTLE: "PLAN_APPROVAL_WAITING_FOR_SUBAGENT_SETTLE",
} as const;
```
Then replace the two string literals in `sessions-patch.ts` with `ErrorCodes.*` references for consistency.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/agents/plan-mode/mutation-gate.ts
Line: 41-50
Comment:
**`ask_user_question` blocked by default-deny during plan mode**
`ask_user_question` is not in `PLAN_MODE_ALLOWED_TOOLS` and does not match any suffix pattern, so the mutation gate's fail-through default deny blocks it whenever `planMode === "plan"`. The tool's own description explicitly states "Plan-mode safety: questions DO NOT exit plan mode. The session stays in plan mode while waiting" — but an agent in plan mode calling `ask_user_question` will receive `Tool "ask_user_question" is not in the plan-mode allowlist and is blocked by default` before any question reaches the user.
`enter_plan_mode` has the same omission (calling it again while already in plan mode should not be blocked).
```suggestion
const PLAN_MODE_ALLOWED_TOOLS = new Set([
"read",
"web_search",
"web_fetch",
"memory_search",
"memory_get",
"update_plan",
"exit_plan_mode",
"ask_user_question",
"enter_plan_mode",
"session_status",
]);
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "feat(plan-mode): split PR4 advanced plan..." | Re-trigger Greptile |
| // (/plan off, another channel approved, timeout, compaction). | ||
| return invalid( | ||
| "planApproval requires an active plan-mode session (the approval window may have expired or been resolved on another channel)", | ||
| ErrorCodes.PLAN_APPROVAL_EXPIRED, |
There was a problem hiding this comment.
New plan-mode error codes missing from
error-codes.ts
ErrorCodes.PLAN_APPROVAL_EXPIRED (line 798) and ErrorCodes.PLAN_APPROVAL_GATE_STATE_UNAVAILABLE (line 850) access keys that don't exist on the ErrorCodes const object — they evaluate to undefined at runtime. Because invalid() uses code ?? ErrorCodes.INVALID_REQUEST, both calls silently fall back to "INVALID_REQUEST". Client code that branches on these specific codes (e.g. the "auto-dismiss stale approval card" logic described in the inline comment for PLAN_APPROVAL_EXPIRED) will never match and the intended UX behaviour is permanently suppressed.
The two string-literal forms on lines 879 and 890 ("PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS", "PLAN_APPROVAL_WAITING_FOR_SUBAGENT_SETTLE") survive at runtime but are TypeScript type errors because they are not assignable to ErrorCode.
All four codes need to be added to src/gateway/protocol/schema/error-codes.ts:
export const ErrorCodes = {
// ... existing codes ...
PLAN_APPROVAL_EXPIRED: "PLAN_APPROVAL_EXPIRED",
PLAN_APPROVAL_GATE_STATE_UNAVAILABLE: "PLAN_APPROVAL_GATE_STATE_UNAVAILABLE",
PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS: "PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS",
PLAN_APPROVAL_WAITING_FOR_SUBAGENT_SETTLE: "PLAN_APPROVAL_WAITING_FOR_SUBAGENT_SETTLE",
} as const;Then replace the two string literals in sessions-patch.ts with ErrorCodes.* references for consistency.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/sessions-patch.ts
Line: 798
Comment:
**New plan-mode error codes missing from `error-codes.ts`**
`ErrorCodes.PLAN_APPROVAL_EXPIRED` (line 798) and `ErrorCodes.PLAN_APPROVAL_GATE_STATE_UNAVAILABLE` (line 850) access keys that don't exist on the `ErrorCodes` const object — they evaluate to `undefined` at runtime. Because `invalid()` uses `code ?? ErrorCodes.INVALID_REQUEST`, both calls silently fall back to `"INVALID_REQUEST"`. Client code that branches on these specific codes (e.g. the "auto-dismiss stale approval card" logic described in the inline comment for `PLAN_APPROVAL_EXPIRED`) will never match and the intended UX behaviour is permanently suppressed.
The two string-literal forms on lines 879 and 890 (`"PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS"`, `"PLAN_APPROVAL_WAITING_FOR_SUBAGENT_SETTLE"`) survive at runtime but are TypeScript type errors because they are not assignable to `ErrorCode`.
All four codes need to be added to `src/gateway/protocol/schema/error-codes.ts`:
```typescript
export const ErrorCodes = {
// ... existing codes ...
PLAN_APPROVAL_EXPIRED: "PLAN_APPROVAL_EXPIRED",
PLAN_APPROVAL_GATE_STATE_UNAVAILABLE: "PLAN_APPROVAL_GATE_STATE_UNAVAILABLE",
PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS: "PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS",
PLAN_APPROVAL_WAITING_FOR_SUBAGENT_SETTLE: "PLAN_APPROVAL_WAITING_FOR_SUBAGENT_SETTLE",
} as const;
```
Then replace the two string literals in `sessions-patch.ts` with `ErrorCodes.*` references for consistency.
How can I resolve this? If you propose a fix, please make it concise.| const PLAN_MODE_ALLOWED_TOOLS = new Set([ | ||
| "read", | ||
| "web_search", | ||
| "web_fetch", | ||
| "memory_search", | ||
| "memory_get", | ||
| "update_plan", | ||
| "exit_plan_mode", | ||
| "session_status", | ||
| ]); |
There was a problem hiding this comment.
ask_user_question blocked by default-deny during plan mode
ask_user_question is not in PLAN_MODE_ALLOWED_TOOLS and does not match any suffix pattern, so the mutation gate's fail-through default deny blocks it whenever planMode === "plan". The tool's own description explicitly states "Plan-mode safety: questions DO NOT exit plan mode. The session stays in plan mode while waiting" — but an agent in plan mode calling ask_user_question will receive Tool "ask_user_question" is not in the plan-mode allowlist and is blocked by default before any question reaches the user.
enter_plan_mode has the same omission (calling it again while already in plan mode should not be blocked).
| const PLAN_MODE_ALLOWED_TOOLS = new Set([ | |
| "read", | |
| "web_search", | |
| "web_fetch", | |
| "memory_search", | |
| "memory_get", | |
| "update_plan", | |
| "exit_plan_mode", | |
| "session_status", | |
| ]); | |
| const PLAN_MODE_ALLOWED_TOOLS = new Set([ | |
| "read", | |
| "web_search", | |
| "web_fetch", | |
| "memory_search", | |
| "memory_get", | |
| "update_plan", | |
| "exit_plan_mode", | |
| "ask_user_question", | |
| "enter_plan_mode", | |
| "session_status", | |
| ]); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/plan-mode/mutation-gate.ts
Line: 41-50
Comment:
**`ask_user_question` blocked by default-deny during plan mode**
`ask_user_question` is not in `PLAN_MODE_ALLOWED_TOOLS` and does not match any suffix pattern, so the mutation gate's fail-through default deny blocks it whenever `planMode === "plan"`. The tool's own description explicitly states "Plan-mode safety: questions DO NOT exit plan mode. The session stays in plan mode while waiting" — but an agent in plan mode calling `ask_user_question` will receive `Tool "ask_user_question" is not in the plan-mode allowlist and is blocked by default` before any question reaches the user.
`enter_plan_mode` has the same omission (calling it again while already in plan mode should not be blocked).
```suggestion
const PLAN_MODE_ALLOWED_TOOLS = new Set([
"read",
"web_search",
"web_fetch",
"memory_search",
"memory_get",
"update_plan",
"exit_plan_mode",
"ask_user_question",
"enter_plan_mode",
"session_status",
]);
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a30794aaa0
ℹ️ 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".
| const handler = getAgentEventState().persistPlanModeSubagentGateState; | ||
| if (!handler) { | ||
| return; |
There was a problem hiding this comment.
Wire subagent gate persistence before approval gating
The subagent approval gate relies on persisted blockingSubagentRunIds when the parent run context is gone, but this persistence path is effectively disabled: persistPlanModeSubagentGateState exits immediately when no handler is set, and I could not find any call site that registers setPlanModeSubagentGatePersistenceHandler in the repo. In the common flow where clearAgentRunContext(runId) runs before a delayed approval arrives, the gate in sessions-patch.ts can see an empty persisted set and allow approve/edit even though subagents are still running. Please register the persistence handler during gateway/session-store initialization (or fail closed when persistence is unavailable).
Useful? React with 👍 / 👎.
| if (verifiedCriteria && !acceptanceCriteria) { | ||
| throw new ToolInputError( | ||
| `plan[${index}].verifiedCriteria requires plan[${index}].acceptanceCriteria to be set`, | ||
| ); |
There was a problem hiding this comment.
Permit verifiedCriteria-only deltas in merge mode
This validation rejects any step update that includes verifiedCriteria without also resending acceptanceCriteria. That breaks the intended incremental merge workflow (marking criteria as verified over time) because callers must echo the full acceptance list on every update, and a normal delta like merge: true + updated verifiedCriteria against an existing step is rejected before merge. Validate verifiedCriteria against the merged step's acceptance criteria (existing + incoming) instead of requiring acceptanceCriteria in every delta payload.
Useful? React with 👍 / 👎.
|
Closed for sequential rollout — please disregard cumulative diff. This PR was opened with The fix: sequential rollout. Each Currently open:
Next: when 1/9 merges, This PR's branch is preserved on the fork and will be reopened with a clean diff in its turn. |
Summary
[Plan Mode 3/8] — part of the 8-part decomposition of #68939, stacking on #69449 (Part 1).
Stacks on: previous part (see [Plan Mode 2/8] / [Plan Mode 1/9 #69449]).
Adds clarifying questions + plan restate + section-targeted review notes + revision markers. Stacks on Part 2/8.
Lines: ~5860
Diff note
This PR is opened cross-repo (head:
100yenadmin:restack/68939-pr4-advanced-plan-interactions) againstopenclaw:main. Because GitHub doesn't support stacked-PR bases that point at fork refs, the displayed diff cumulatively includes all previous parts in the stack. Reviewers should focus on the changes specific to this part — see commit list for the new content beyond what previous parts established.Test status
After post-v2026.4.21 rebase: all targeted plan-mode tests pass (53/53 in unit-fast for approval + integration suites). The full suite has pre-existing vitest workspace project-name conflicts unrelated to this work.
Related