feat(plan-mode): plan archetype + ask_user_question + auto mode (PR-10)#68440
feat(plan-mode): plan archetype + ask_user_question + auto mode (PR-10)#68440100yenadmin wants to merge 105 commits into
Conversation
…injection scanning Consolidation of v3 PRs #66371, #66372, #66373, #66374, #66375 into a single review unit. Ports the Hermes Agent GPT-5 prompt stack to OpenClaw with Hermes-verbatim semantics (only mechanical tool-ID substitutions for OpenClaw's catalog: terminal→exec, execute_code→ code_execution, read_file→read, search_files→exec). ## Prompt additions (extensions/openai/prompt-overlay.ts) OPENAI_GPT5_TOOL_ENFORCEMENT — Hermes mandatory_tool_use block: "NEVER answer these from memory — ALWAYS use a tool" + 7 categories. OPENAI_GPT5_EXECUTION_BIAS enhanced with 3 Hermes subsections: - Act, Don't Ask (prompt_builder.py:221-229) - Tool Persistence (prompt_builder.py:198-204) - Verification (prompt_builder.py:238-245) ## Architecture (src/agents/system-prompt-contribution.ts) Add "tool_enforcement" to ProviderSystemPromptSectionId union so providers can inject mandatory tool-use guidance independently of execution_bias. GPT-5 overlay uses sectionOverrides.tool_enforcement. ## Security (src/agents/context-file-injection-scan.ts) Port Hermes's _CONTEXT_THREAT_PATTERNS (prompt_builder.py:36-52): 10 threat patterns + 10 invisible chars. Flagged files are BLOCKED (content replaced with placeholder), matching Hermes behavior. ## Tests - index.test.ts: EXPECTED_GPT5_STABLE_PREFIX helper + 7 assertions updated for tool_enforcement sectionOverride - context-file-injection-scan.test.ts: 17 tests covering all patterns, false-positive avoidance, and BLOCK behavior Part of GPT 5.4 Enhancement v3 sprint. Tracking: #66345. Supersedes: #66371, #66372, #66373, #66374, #66375.
…e, activeForm, hydration
Ports three Hermes todo-tool features that OpenClaw's update_plan
was missing, plus adds plan hydration for post-compaction recovery.
## cancelled status (Hermes parity)
Add "cancelled" as a fourth plan step status. Hermes uses this to
mark steps that were started but abandoned due to failure, keeping
them visible in history instead of silently dropping them.
Ref: Hermes tools/todo_tool.py Status enum.
## merge mode (Hermes parity)
Add optional merge: boolean parameter (default false). When true,
incoming steps update existing ones by matching step text and new
steps are appended. When false, the entire plan is replaced.
Ref: Hermes tools/todo_tool.py write(todos, merge=False).
## activeForm field (Claude Code TodoWrite parity)
Add optional activeForm: string field on plan steps. Shows the
present-continuous form while in_progress (e.g. "Running tests"
instead of "Run tests"). Small schema addition with disproportionate
UX impact for plan rendering.
## Post-compaction plan hydration (Hermes parity)
New src/agents/plan-hydration.ts helper that formats active
(pending/in_progress) plan items for injection after context
compression. The injected text uses factual phrasing ("Your active
plan was preserved...") to avoid triggering the planning-only retry
guard's promise-language detection.
Ref: Hermes run_agent.py:6754-6756 + tools/todo_tool.py:format_for_injection().
Note: the compaction hook point in compact.ts is not wired in this
PR — that requires a follow-up to integrate with the compaction
checkpoint system (#62146). This PR provides the formatter; the
hook wiring is the next step.
Part of GPT 5.4 Enhancement v3 sprint. Tracking: #66345.
…ne, types Phase 3.C of the GPT 5.4 parity sprint. Implements plan mode as an opt-in feature where mutation tools are blocked during Phase A (plan) until the user approves the plan, then Phase B (execute) runs normally with full tool access. ## Plan mode mutation gate (mutation-gate.ts) - Blocks 11 mutation tools by exact name: apply_patch, edit, exec, gateway, message, nodes, process, sessions_send, sessions_spawn, subagents, write - Blocks suffix patterns: *.write, *.edit, *.delete - Allows: read, web_search, web_fetch, memory_search, memory_get, update_plan, exit_plan_mode, session_status - Special exec whitelist: read-only commands (ls, cat, git status, grep, find, etc.) are allowed; mutating commands are blocked - 30 unit tests covering all paths ## Approval state machine (approval.ts) - States: none → pending → approved | edited | rejected | timed_out - approve: transitions to normal mode, records confirmedAt - edit: re-enters plan mode with edited plan - reject/timeout: transitions to normal mode, aborts turn - buildApprovedPlanInjection(): generates system message for Phase B - 7 unit tests ## Types (types.ts) - PlanMode, PlanApprovalState, PlanModeSessionState - DEFAULT_PLAN_MODE_STATE Built independently of PR #61845. Plan mode is NEVER auto-enabled — it requires explicit user opt-in via config, slash command, or UI toggle (entry points follow in a separate commit). Tracking: #66345, #67520.
Phase 4.1 of the GPT 5.4 parity sprint. Extends the skill metadata
schema with an optional planTemplate field that auto-populates
update_plan when the skill is activated.
## Schema extension (types.ts)
- New SkillPlanTemplateStep type: { step: string, activeForm?: string }
- New planTemplate?: SkillPlanTemplateStep[] field on OpenClawSkillMetadata
- Parsed from YAML frontmatter `plan-template` in SKILL.md
## Skill planner (skill-planner.ts)
- buildPlanTemplatePayload(): converts template to update_plan args
(all steps start as "pending")
- hasSkillPlanTemplate(): quick check for non-empty templates
- 7 unit tests
## Example frontmatter
```yaml
plan-template:
- step: "Run test suite"
activeForm: "Running test suite"
- step: "Build artifacts"
activeForm: "Building artifacts"
```
Note: the runtime wiring in skills-runtime.ts to auto-call
update_plan on activation follows once #67514 (task-system parity)
merges with the extended update_plan schema.
Tracking: #66345, #67522.
Phase 4.2 of the GPT 5.4 parity sprint. Adds a PlanStore class for cross-session task coordination, modeled after Claude Code's Tasks API concept. ## PlanStore (plan-store.ts) - read(namespace): loads plan from ~/.openclaw/plans/<ns>/plan.json - write(namespace, plan): persists plan to disk with auto-mkdir - lock(namespace): file-level exclusive lock with O_EXCL + 10s stale-lock auto-cleanup to prevent deadlocks from crashes - mergeSteps(): merge incoming steps into existing plan by matching step text, appending new steps, preserving existing order. Tracks updatedBy (session key) and updatedAt per step. ## Schema (StoredPlan, StoredPlanStep) - StoredPlanStep: step, status (4 values), activeForm?, updatedBy?, updatedAt? - StoredPlan: namespace, steps[], createdAt, updatedAt ## Tests (8 tests) - Read/write round-trip + nested directory creation - Lock acquisition, release, and concurrent contention - Merge: update by text match, append new, preserve order Default behavior (no namespace configured): plan is session-scoped, PlanStore is not used. Cross-session coordination only activates when planStore.namespace is set in config. Tracking: #66345, #67523.
…rge mode The execute() third param is an AbortSignal/context, not a plan context, so context?.previousPlan was always undefined and merge mode was a no-op. Fall back to replace until PlanStore (#67542) provides previous-plan lookup.
…efault deny, confirmedAt - Block shell compound operators (&&, ||, |, ;, >, >>, backticks, $()) in exec/bash commands to prevent chaining past the prefix allowlist - Add "bash" to the mutation tool blocklist (alias for exec) - Default to blocked for unknown tools (fail-closed, not fail-open) - Clear confirmedAt on "edit" transition to prevent stale approval leaking - Remove unused PlanApprovalState import
… errors, lock ownership - Validate namespace to prevent path traversal (reject "..", absolute paths) - Atomic write: write to temp file then rename to prevent mid-write corruption - Fix file handle leak: ensure handle.close() in finally on open errors - Distinguish ENOENT from parse/permission errors in read() instead of swallowing all errors - Verify lock ownership token before unlinking on release to prevent one process from deleting another's lock
…itelist Two Codex P1 issues at 95%+ confidence: 1. Newlines as shell separators: "ls\nrm -rf tmp" bypassed the compound operator check because \n wasn't in the regex. Added \n and \r to the operator detection pattern. 2. Dangerous flags on allowed prefixes: "find . -delete" passed because "find" is whitelisted. Added a dangerous-flag check for -delete, -exec, -execdir, --delete, -rf, -i that runs after the operator check but before the prefix allowlist. Added 4 new test cases covering both vectors + bash alias.
`env` as a prefix allows arbitrary command execution (e.g. `env npm install`). Remove it — users who need environment variables can use `printenv` which is already allowlisted.
- Convert args from mappings to arrays - Replace sendAgentMessage with state.addInboundMessage - Replace waitForToolCall with waitForOutboundMessage + lambda predicates - Replace assertions: blocks with assert: actions - Add waitForGatewayHealthy + waitForQaChannelReady setup - Use ref: state / ref: env for runtime objects - Fix gpt54-plan-mode-default-off scenario ID to match filename - Simplify injection-scan to baseline check (scanner unit-tested in #67512)
- Add explicit tests for bash tool blocked in plan mode, shell compound operators (redirect, pipe, semicolon), newlines in commands, and dangerous flags (find -delete, find -exec) - Add early-return guard in resolvePlanApproval for stale timeouts that fire after the user has already acted on the approval - Add test verifying stale timeout is ignored after approval is resolved
- Add parsePlanTemplate() helper in frontmatter.ts to parse the plan-template YAML field into SkillPlanTemplateStep[] - Include planTemplate in resolveOpenClawMetadata output - Add resolveSkillPlanTemplate() in skills-runtime.ts that scans activated skill entries and returns an update_plan payload via buildPlanTemplatePayload when a plan template is found
Replace O(n) linear scan with a Set when checking whether incoming steps already exist, improving merge performance for large plans.
- Add plan-hydration.test.ts: tests formatPlanForHydration returns null for empty/all-completed/all-cancelled steps, filters out completed and cancelled steps, includes pending and in_progress with correct markers, and validates preserved plan header format - Add update-plan-tool.parity.test.ts: tests cancelled status is accepted in schema, activeForm field is preserved in output, and merge=true with no previousPlan falls back to replace
…ear confirmedAt Three fixes at 95%+ confidence from Copilot review: 1. Remove -i from DANGEROUS_FLAGS — blocks grep -i (case-insensitive search), the most common grep usage in plan mode. The remaining flags (-delete, -exec, -execdir, --delete, -rf) are genuinely dangerous. 2. Add read-only suffix patterns (.read, .search, .list, .get, .view) so MCP read tools like custom_mcp.read pass the fail-closed default. Mutation suffixes (.write, .edit, .delete) are still blocked. 3. Clear confirmedAt on reject and timeout transitions in the approval state machine. Previously only edit cleared it; reject/timeout spread current which could leak a stale confirmedAt.
Three fixes from Copilot review: 1. mergeSteps: only set updatedBy when sessionKey is truthy, avoiding undefined overwriting existing attribution 2. write(): validate plan.namespace matches the namespace argument to prevent accidental mismatches 3. Added tests: path traversal rejection + namespace mismatch in write
Filename is interpolated into the BLOCKED string. A crafted file path containing brackets or newlines could confuse the model about what content is blocked. Strip ], [, and newlines from the filename.
…g in mergeSteps 1. read(): verify plan.namespace matches requested namespace to catch file corruption or misrouted reads 2. mergeSteps(): deduplicate incoming steps by tracking appended step texts in a Set, preventing duplicate step entries when incoming contains the same step text multiple times
…ject/timeout - mutation-gate: use word-boundary matching for DANGEROUS_FLAGS to prevent substring false positives (e.g. -exec matching -executable, -delete matching -delete-old). Added tests for find -executable and grep -rfl. - approval: reject and timeout now stay in plan mode instead of transitioning to normal mode, preventing fail-open after rejection.
Now that activeForm is explicitly declared, disallow arbitrary extra keys on plan step objects to catch malformed input early.
Change from truthy check to explicit undefined check so that plans with no namespace field (backward-compat) pass through, while non-undefined mismatches still throw a corruption error.
Decision 4 from adversarial audit: defines how plan rejection, editing, and revision work across all surfaces (web, CLI, messaging). ## State machine changes - reject: stays in plan mode (fail-closed), increments rejectionCount, captures user feedback. Agent receives [PLAN_DECISION] injection on next turn with feedback text and revision instructions. - edit: transitions to normal mode (user edits count as approval), records confirmedAt. Edited plan is the approved plan. - timeout: stays in plan mode. Agent told proposal expired, may re-propose. - After 3+ rejections: injection adds "consider asking the user to clarify their goal" guidance. ## New exports - buildPlanDecisionInjection(): generates the structured [PLAN_DECISION] block for rejection/expiry context injection - PlanModeSessionState now tracks feedback and rejectionCount ## UI contract (documented in types.ts) Web/Desktop: [Approve] [Edit] [Reject + Feedback] buttons + persistent "Plan Mode Active" banner + [Exit Plan Mode] escape hatch Messaging: [Approve] [Reject] inline buttons, user types feedback as next message after rejection. No Edit on messaging (limitation). ## Tests 14 test cases covering: approve, edit, reject with feedback, rejection accumulation, timeout, stale timeout, enteredAt preservation, feedback clearing on approval, plan decision injection format.
…bosity Additions to the GPT-5.4 prompt overlay (interaction_style, output_contract, execution_bias sections): - Identity Enforcement: prime GPT-5.4 to adopt SOUL.md as primary identity, ban corporate default patterns - Voice Calibration: counter flat/analytical drift, anti-sycophancy reinforcement - Response Length Discipline: 200-word target with 95% confidence self-evaluation gate leveraging thinking mode, file-offload for genuine long-form - Investigation Discipline: prevent partial-findings-then-ask pattern, force parallel tool calls for independent lookups - Plan Confidence Gate: 95%+ → execute without approval, 80-94% → state uncertainty and begin, <80% → iterate privately through research
PR review-loop pass 1 complete (commit
|
| # | Comment | Outcome |
|---|---|---|
| #3104741578 P1 | ask_user_question blocked by mutation gate | Fixed — added to PLAN_MODE_ALLOWED_TOOLS |
| #3104741583 | Duplicate display-summary constant | Fixed (single source of truth) |
| #3104741591 | [QUESTION_ANSWER]: format mismatch | Fixed (canonical format with colon) |
| #3104741595 | onAnswerOption typing | Fixed (runtime fallback + warning) |
| #3104741600 | window.prompt suggestion | Already fixed in PR-13 (3a6ec73433) — inline textarea |
| #3104741606 | Cancelled-step markdown | Fixed (clean |
| #3104743331 P1 | Same as #3104741578 | Fixed (allowlist entry) |
| #3104743333 P2 | update_plan merge sidebar refresh | Escalated — see below |
| #3105169112 P1 | sessions_spawn blocklisted + ask_user_question | Fixed (removed from blocklist + added to allowlist) |
| #3105169120 | Same as #3104741583 | Fixed (re-export pattern) |
Escalated comment
#3104743333 (Codex P2 — app-tool-stream.ts:519): sidebar refresh reads data.args (tool input at start time) for live-plan rendering. With update_plan merge: true mode, the input is a delta, not the merged result. Real bug, but the proper fix requires re-architecting either the agent-event channel (emit a separate "merged plan" event) or the UI subscription path (read from SessionEntry.planMode.lastPlanSteps after each tool-end event). Both options are larger than a single review-pass fix. Flagged for next sprint — would benefit from architecture discussion before implementing.
Live install: OpenClaw 2026.4.15 (c928790) — gateway reachable.
Build/lint/test: lint 0 errors; tsgo only the pre-existing baseline error from #67542; build + ui:build clean.
cc @copilot @greptile-apps please re-review the latest commit c9287908eb.
Greptile SummaryPR-10 delivers plan-mode archetype enforcement, the Confidence Score: 4/5Safe to merge after addressing the acceptanceCriteria prompt/schema mismatch, which currently silently misleads agents about closure-gate behavior. One P1 finding: the archetype prompt instructs agents to include acceptanceCriteria in exit_plan_mode steps for the closure-gate, but the field is silently dropped. Agents believing the gate is armed (and skipping the update_plan call) could hit premature step completion on high-risk tasks. All prior adversarial-review items appear resolved. The P2 stale comment is cosmetic. src/agents/plan-mode/plan-archetype-prompt.ts — acceptanceCriteria reference needs to be corrected or the exit_plan_mode step schema needs to be extended to support it. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/agents/plan-mode/plan-archetype-prompt.ts
Line: 54-58
Comment:
**`acceptanceCriteria` in `exit_plan_mode` steps is silently dropped**
The archetype prompt instructs agents to include `acceptanceCriteria: [...]` on high-risk `exit_plan_mode` plan steps to arm the runtime closure-gate. However, `exit_plan_mode`'s step schema does not define this field (`additionalProperties: false` on the step object), and `readPlanSteps()` reads only `step`, `status`, and `activeForm` — `acceptanceCriteria` is never extracted or forwarded. Any agent that follows this guidance will silently fail to arm the gate; the field is thrown away by `readPlanSteps` in `exit-plan-mode-tool.ts`.
The closure-gate currently only works through `update_plan` (which does declare `acceptanceCriteria` on its step schema). Agents shouldn't need to be told to call `update_plan` separately after `exit_plan_mode` just to get gate enforcement on the steps they already declared.
Fix options:
- Remove the `acceptanceCriteria` mention from the archetype prompt and note that the gate must be armed via `update_plan`, OR
- Add `acceptanceCriteria` to `ExitPlanModeToolSchema`'s step object and to `readPlanSteps()` so it flows through to the approval payload and can be persisted (mirroring what `update_plan` + the persister already do).
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/config/sessions/types.ts
Line: 238-239
Comment:
**Stale PR reference in comment**
The comment says `autoApprove` is "Cleared explicitly via ... the `/plan auto` slash command (PR-11)" but `/plan auto on|off` is implemented in this PR (PR-10) via `ui/src/ui/chat/slash-command-executor.ts`.
```suggestion
* Cleared explicitly via
* sessions.patch { planApproval: { action: "auto", autoEnabled: false } }
* or via the `/plan auto` slash command.
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "PR-10 hardening: deep-dive review fixes" | Re-trigger Greptile |
📋 Architecture & Status — single source of truth (commit
|
|
To use Codex here, create an environment for this repo. |
|
@copilot please re-review this PR with the latest changes in |
|
Closing in favor of consolidated PR #68939. Rationale: this branch was 734 commits behind upstream/main and the The full iteration history + decision log lives in |
Summary
PR-10 of the plan-mode rollout series. Sits on top of #67514 / #67538 / #67542 / #67721 / PR-8 / PR-9 (all currently in flight on the rollup branch). Net-new in this PR are 2 commits (`78c6eb488c` + `1bf9d7b4e7`) — focused review surface is `git diff 34c082b..feat/plan-archetype-and-questions`.
Change Type
Scope
Linked Issue/PR
Root Cause
N/A — feature work.
Testing
Evidence
Human Verification
Review Conversations
Compatibility / Migration
Risks and Mitigations