Skip to content

[Plan Mode FULL] Integrated bundle for testing (Parts 1\u20136 + automation + executing-state lifecycle)#70071

Closed
100yenadmin wants to merge 173 commits into
openclaw:mainfrom
electricsheephq:restack/68939-pr10-executing-followup
Closed

[Plan Mode FULL] Integrated bundle for testing (Parts 1\u20136 + automation + executing-state lifecycle)#70071
100yenadmin wants to merge 173 commits into
openclaw:mainfrom
electricsheephq:restack/68939-pr10-executing-followup

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Umbrella tracker: #70101 — master tracker for the 9-PR plan-mode rollout. See it for status of all parts + suggested merge order + carry-forward backlog.


This PR is the integrated bundle of [Plan Mode 1/6] through [Plan Mode 6/6] + the automation/subagent follow-ups + the executing-state lifecycle / debug-hardening commits.

Two ways to land this feature:

  1. Per-part review (recommended for line scrutiny): review/merge [Plan Mode 1/6] ([Plan Mode 1/6] Plan-state foundation #70031) → [Plan Mode 6/6] ([Plan Mode 6/6] Docs, QA, and help #70070) in order. Each shows clean per-part diff (red CI on Parts 2/6–5/6 because of stack dependencies; 6/6 is docs-only and green).
  2. Single-merge bundle (this PR): ~30k lines integrated state, GREEN CI (this is the integration target). Maintainer can check out this branch for full end-to-end testing or merge as a single landed unit.

Executive summary

Plan mode is an opt-in, per-session workflow where agents must propose a structured, approvable plan (title + steps + assumptions + risks + verification criteria) before executing any mutating tool (bash, edit, write, apply_patch, process management, messaging, etc.). The user reviews, edits, approves, or rejects with feedback; only on approve / edit do mutation tools unlock for that session. The mode is off by default and activated per-session via /plan on, the chip in webchat, or per-agent config. It borrows the "propose, approve, execute" pattern from Claude Code's plan mode and Codex's plan flow — see "Parity benchmark" below for independent quality numbers.

This PR is the single-merge integration target for the 9-PR plan-mode rollout (umbrella issue #70101). It bundles the six numbered parts (1/6 through 6/6), the two thematic carve-outs ([Plan Mode INJECTIONS] #70088, [Plan Mode AUTOMATION] #70089), and the executing-state lifecycle / debug-hardening work that could not be cleanly isolated as a per-part PR (its code references types and constants from PR1 + PR2 + PR3 simultaneously, so a standalone diff would balloon and defeat per-part-review goals). End result: 192 changed files, ~30k lines of integrated state, all the iter-1/2/3 hardening from the original umbrella #68939, plus the executing-state work and INJECTIONS / AUTOMATION refinements that came after #68939 closed.

This PR is for the maintainer who wants a single-shot review + merge rather than the 9-step per-part dance. The per-part PRs (#70031#70070, plus #70088 + #70089) exist for line-scrutiny review; this PR exists for end-to-end testing on a real branch and as the single-merge alternative. Both paths produce the same final tree state. Choose Path A (per-part) if you want narrow per-PR diffs to scrutinize; choose Path B (this PR) if you want one merge button.

A note on file count: a recent automated review reported "FULL is missing 90 files vs the union of per-part PRs". That report used gh pr view --json files which has a hardcoded 100-result cap. The full paginated count via gh api .../files --paginate is 192 files, and a pairwise diff against the union of all per-part PRs shows zero missing files (the seven FULL-only files are explained in "What's unique to FULL" below — they're the executing-state lifecycle work that didn't fit a per-part). The umbrella tracker's correction comment has the full audit if you want to verify.

TL;DR

  • What: Plan-mode runtime + tools + approval UX + universal /plan slash commands + executing-state lifecycle + cron-driven nudges + subagent gating + skill plan templates + debug log + UI mode chip + plan cards.
  • Scope: 192 files, ~30k LoC integrated state, touches gateway / runner / tools / UI / 6 channel extensions / config / docs / QA.
  • Default state: OFF. agents.defaults.planMode.enabled: false. No existing session, agent, or model behaves differently on merge.
  • Safety: Mutation gate is fail-closed (unknown tools blocked in plan mode). Approval requires valid approvalId (cryptographic random, regenerated per exit_plan_mode). Subagent gate blocks approve / edit while research children are in flight; reject is intentionally never gated.
  • Tests: 200+ new tests across 45+ test files (unit / integration / pipeline). Plan-mode-specific vitest config at test/vitest/vitest.plan-mode.config.ts. Full suite green on this branch.
  • Rollback: Flag flip — agents.defaults.planMode.enabled: false → restart gateway. Tools unregister, UI chip hides, mutation gate short-circuits, all sessions.patch { planApproval } actions reject. No DB migration to undo.
  • Deferrals: Telegram document attachment re-wire (PR-14 follow-up; markdown still written to disk), agents.defaults.planMode.autoEnableFor model-pattern auto-enable runtime (schema-reserved, no scanner), approvalTimeoutSeconds cron-time watchdog (schema-reserved, no firing), /plan self-test harness, Bug B stale-card auto-dismiss. All tracked below in "Deferred features".
  • CI: GREEN on this branch (the integration target). Per-part PRs 2/6–5/6 are red purely due to stack-dependency ordering — none of the failures reflect a defect in the bundled state. Part 6/6 is docs-only and green; Part 1/6 is foundation-only and green.

What this PR does at a glance

flowchart LR
  subgraph UI[Channels]
    Web[Webchat<br/>inline approval card<br/>+ sidebar plan view<br/>+ mode chip]
    TG[Telegram<br/>/plan commands<br/>+ deferred document delivery]
    SL[Slack / Discord /<br/>Matrix / iMessage /<br/>Signal / CLI / SMS<br/>universal /plan only]
  end
  subgraph GW[Gateway]
    Patch[sessions.patch<br/>handler + approval<br/>state machine]
    Persister[plan-snapshot<br/>persister]
    Sub[sessions.changed<br/>broadcaster]
  end
  subgraph RT[Agent runtime pi-embedded-runner]
    Runner[run.ts<br/>turn loop]
    Gate[pi-tools/<br/>before-tool-call<br/>mutation gate caller]
    Hyd[plan-hydration<br/>post-compaction restore]
    Inj[pending-injection<br/>consumer]
    ExecInj[execution-status<br/>injection]
  end
  subgraph PM[Plan-mode core src/agents/plan-mode/ - 27 files]
    Types[types.ts<br/>approval.ts<br/>injections.ts]
    MGate[mutation-gate.ts<br/>fail-closed allowlist]
    Edits[accept-edits-gate.ts<br/>3-state edit perm]
    Auto[auto-enable.ts<br/>per-model opt-in]
    Nudge[plan-nudge-crons.ts<br/>plan-execution-nudge-crons.ts]
    Persist[plan-archetype-persist.ts<br/>plan-archetype-bridge.ts<br/>plan-archetype-prompt.ts]
    Debug[plan-mode-debug-log.ts]
    Integ[integration.test.ts<br/>200+ tests anchor]
  end
  subgraph Tools[Agent tools src/agents/tools/]
    Enter[enter_plan_mode]
    Exit[exit_plan_mode]
    Update[update_plan]
    Ask[ask_user_question]
    Status[plan_mode_status]
    Spawn[sessions_spawn]
    Cron[cron-tool]
  end
  subgraph Cron[src/cron/isolated-agent/]
    RunExec[run-executor.ts<br/>FULL-only]
    RunPlan[run.plan-mode.test.ts]
  end
  subgraph Cfg[Config src/config/]
    Schema[zod-schema.agent-defaults<br/>+ agent-runtime + skills]
    SessTypes[sessions/types<br/>SessionEntry.planMode]
    Migrate[sessions/store-migrations<br/>FULL-only]
  end
  subgraph Skills[Skills src/agents/skills/]
    Planner[skill-planner]
    Frontmatter[frontmatter parser]
    Workspace[workspace.ts<br/>plan-template snapshot]
  end
  subgraph Infra[src/infra/]
    HB[heartbeat-runner<br/>+plan-nudge contract]
  end
  subgraph Store[Persistence ~/.openclaw/]
    SE[SessionEntry<br/>.planMode + lastPlanSteps]
    MD[agents/&lt;id&gt;/plans/<br/>plan-*.md]
    Log[logs/gateway.err.log<br/>plan-mode/* events]
  end

  Web -- "sessions.patch<br/>{planApproval}" --> Patch
  TG -- "/plan ..." --> Patch
  SL -- "/plan ..." --> Patch
  Runner -- update_plan --> Persister
  Persister --> Sub
  Sub --> Web
  Sub --> TG
  Sub --> SL
  Runner -- before-tool-call --> Gate
  Runner -- post-compaction --> Hyd
  Runner -- per-turn --> Inj
  Runner -- per-turn --> ExecInj
  Gate -- consults --> MGate
  Gate -- consults --> Edits
  Tools -- registered via --> Runner
  Exit -- writes --> Persist
  Enter -- schedules --> Nudge
  Auto -- on session start --> Runner
  RunExec -- isolates --> Runner
  PM -- types/state --> SE
  Persist -- writes --> MD
  Debug -- appends --> Log
  Schema -- validates --> Patch
  Schema -- validates --> Runner
  Migrate -- on load --> SessTypes
  Skills -- seed --> Tools
  HB -- per-tick --> Nudge
Loading

What's in this bundle (vs. the 9 per-part PRs)

Part Per-part PR What it contributes Status
1/6 Plan-state foundation #70031 SessionEntry.planMode schema, on-disk persister with O_EXCL atomic write + EEXIST retry, namespace traversal guards, plan hydration, enter/exit/update_plan tools, skill plan-template foundation open, retitled
2/6 Core backend MVP #70066 Mutation gate (blocks write/edit/exec in plan mode), approval state machine (pending → approved/rejected/edited/timed_out), gateway sessions.patch { planMode }, tool-call hook plumbing open
3/6 Advanced plan interactions #70067 ask_user_question tool, plan_mode_status tool, plan archetypes (discoverable patterns for skill-driven plans), accept-edits gate (Claude-Code-style auto-edit permission with three hard constraints), PostApprovalPermissions scoped by approvalId open
4/6 Web UI + i18n #70068 Plan cards in chat, mode-switcher chip, plan resume on web reconnect, inline plan-approval card (3 buttons + revise textarea), i18n across 13 locales open
5/6 Text channels + Telegram #70069 Universal /plan slash commands across all channels, plan rendering for text channels, plan-archetype channel bridge, Telegram attachment delivery surface open
6/6 Docs, QA, and help #70070 Architecture doc (~635 lines), operator runbook (~250 lines), concept doc, prompt-stack spec, plan-mode-101 skill, GPT-5.4 QA scenarios open, green CI (docs-only)
[Plan Mode INJECTIONS] #70088 Typed pending-injection queue foundation, injections.ts builders, [PLAN_MODE_INTRO] first-turn injection, [PLAN_DECISION] unified format, [PLAN_STATUS] execution-phase auto-inject open (thematic carve-out, sibling to numbered stack)
[Plan Mode AUTOMATION] #70089 Cron nudges (1/3/5-min escalating retry on stalled execution), auto-enable per-model wiring scaffold, subagent follow-up hints, plan-execution-nudge crons (P2.12a imperative-step nudge text) open (thematic carve-out, red CI like numbered 2/6–5/6)
(no separate per-part PR) Executing-state lifecycle + debug hardening folded into THIS PR 3-state plan mode (plan/executing/normal), executing-phase nudges, [PLAN_STATUS] auto-inject preamble (P2.12b), allowlist additions (sessions_yield, lcm_grep, lcm_expand_query), various debug + adversarial-review hardening commits landed here only

The "no separate per-part PR" Executing-state lifecycle work could not be cleanly isolated as a standalone per-part PR because its code is structurally interleaved with earlier parts: it references PlanMode discriminants from PR1, the mutation-gate signature from PR2, and the approval state shape from PR3. Carving it into its own per-part PR would have either (a) dragged duplicates of those types into the carve-out, ballooning the diff and defeating the per-part-review goal, or (b) required the carve-out to depend on three separate-but-not-yet-merged PRs, which makes its CI red and its review-context impossible. So it lands here only. Reviewers can navigate it via the Commits tab — each commit is named with a pr10/ or executing-followup/ prefix and is a clean per-feature change.

What's UNIQUE to FULL (the 7 files not in the per-part union)

These are the only files in this PR that don't appear in any per-part PR's diff. They exist for legitimate structural reasons documented below — this is not "missing" content but integration content.

File Why it's FULL-only Lines
.github/workflows/ci.yml CI baggage — the restack/ integration branch carries CI updates not on the per-part branches because we needed to change concurrency: keying to avoid per-part jobs cancelling each other when stacked. Per-part PRs run on the upstream-default ci.yml; FULL needs the bundle-aware version tiny diff
package.json Dependency baggage from the AUTOMATION carve-out (cron-jitter dep) — it lands in #70089 but the version bump that resolves a peer-dep conflict only became necessary after AUTOMATION was integrated with the executing-state work small
src/agents/plan-mode/execution-status-injection.ts Executing-state lifecycle: per-turn [PLAN_STATUS] preamble injection that fires only when planMode.mode === "executing". Structurally inseparable from the executing-state branch of the state machine, which itself is FULL-only ~120
src/agents/plan-mode/execution-status-injection.test.ts Test for the above ~180
src/agents/plan-mode/plan-execution-nudge-crons.ts P2.12a imperative-step nudge text — different from plan-nudge-crons.ts (which fires during mode: "plan"); this fires during mode: "executing" when steps stall. Splitting it across the PR2/PR3 boundary would require co-evolving two cron schedulers in two PRs ~200
src/config/sessions/store-migrations.ts Best-effort migration of legacy provider/room fields when loading old session entries. Unrelated to plan mode but bundled here because the FULL branch also picks up an upstream channel-rename migration from restack/ rebase tip ~30
src/cron/isolated-agent/run-executor.ts Cron-side createCronPromptExecutor — the wrapper around runCliAgent that the cron path uses to drive plan-mode-aware turns. Touches run-execution.runtime, run-fallback-policy, run-session-state — all of which are upstream-existing modules that the per-part stack didn't need to touch but the integrated cron-driven nudge path does ~250

Net structural integrity: 192 (FULL) − 7 (FULL-only) = 185 files match the per-part union. The per-part union is also 185 files (I verified with a paginated diff). No content is missing from FULL relative to the per-part union.

State machine

Session state is the cross product of PlanMode ∈ {normal, plan, executing} and PlanApprovalState ∈ {none, pending, approved, edited, rejected, timed_out}. The executing state is new in this bundle (it didn't exist in #68939 — that umbrella had only {normal, plan}). Its purpose: provide a distinct lifecycle phase between "approval landed" and "agent has actually finished the work" so cron-driven nudges can fire only when execution is stalled, not before approval and not after completion.

stateDiagram-v2
  [*] --> Normal
  Normal --> PlanInvestigation : enter_plan_mode<br/>OR /plan on<br/>OR autoEnableFor match (deferred)
  PlanInvestigation --> PlanInvestigation : update_plan<br/>(tracks progress)
  PlanInvestigation --> PlanPendingApproval : exit_plan_mode<br/>(new approvalId)
  PlanPendingApproval --> Executing : approve / edit<br/>(mutations unlock,<br/>execution nudges arm)
  PlanPendingApproval --> PlanInvestigation : reject<br/>(+feedback, rejectionCount++)
  PlanPendingApproval --> PlanInvestigation : timed_out<br/>(approvalTimeoutSeconds; deferred)
  Executing --> Executing : update_plan<br/>(executing-phase progress)
  Executing --> Normal : auto-close-on-complete<br/>(all steps terminal)
  Executing --> Normal : /plan off (escape hatch)
  PlanInvestigation --> Normal : /plan off
  Normal --> Normal : reset on /clear

  note right of PlanPendingApproval
    approve / edit gated by<br/>openSubagentRunIds.size > 0<br/>→ PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS<br/>Reject is never gated.
  end note

  note left of Executing
    Executing-state lifecycle (FULL-only):<br/>plan-execution-nudge-crons fire at<br/>1, 3, 5 min if no progress.<br/>execution-status-injection prepends<br/>[PLAN_STATUS] each turn.
  end note
Loading

Key invariants (carried from #68939, with executing-state additions):

  • approvalId is a cryptographic random token (newPlanApprovalId in src/agents/plan-mode/types.ts), regenerated on every exit_plan_mode. Stale UI clicks against an old approvalId are silently no-op'd.
  • Rejection does not flip mode back to normal. The agent stays in plan mode and revises.
  • After 3 rejections, the [PLAN_DECISION] injection suggests the agent ask a clarifying question via ask_user_question instead of looping.
  • Approval transitions to executing (not directly to normal) so executing-phase nudges can arm.
  • executing → normal is automatic on auto-close-on-complete (all steps terminal: completed or cancelled). Manual /plan off is also accepted as the user escape hatch; the agent itself cannot force the transition.
  • Mode transition to normal via /plan off is always allowed at any state.

Critical flows

4.1 Enter plan mode

sequenceDiagram
  actor User
  participant UI as Webchat / channel
  participant GW as Gateway<br/>sessions.patch
  participant Store as SessionEntry
  participant Run as pi-embedded-runner
  participant Cron as plan-nudge-crons

  User->>UI: /plan on (or click chip, or agent calls enter_plan_mode)
  UI->>GW: sessions.patch { planMode: "plan" }
  GW->>Store: planMode.mode = "plan"<br/>approval = "none"<br/>nudgeJobIds = []
  GW->>Cron: schedulePlanNudges([10, 30, 60] min)
  Cron-->>Store: nudgeJobIds = [cron/plan-nudge:...]
  GW-->>UI: ack + broadcast sessions.changed
  Note over Run: next agent turn
  Run->>Store: load SessionEntry
  Run->>Run: inject PLAN_MODE_REFERENCE_CARD<br/>+ PLAN_ARCHETYPE_PROMPT<br/>+ (first-turn) [PLAN_MODE_INTRO]
  Run->>Run: arm mutation gate via<br/>getLatestPlanMode accessor
  Run-->>User: agent now operates under plan-mode contract
Loading

4.2 Exit + approval (happy path) → Executing → Complete

sequenceDiagram
  participant Agent
  participant Exit as exit_plan_mode tool
  participant Ctx as AgentRunContext
  participant Persist as plan-archetype-persist
  participant GW as gateway/sessions-patch
  participant Store as SessionEntry
  actor User
  participant UI as Webchat<br/>approval card
  participant Run as pi-embedded-runner
  participant Inj as pending-injection
  participant ExecCron as plan-execution-nudge-crons

  Agent->>Exit: { title, plan, assumptions?, risks?, verification? }
  Exit->>Ctx: read openSubagentRunIds
  alt size > 0
    Exit-->>Agent: ToolInputError<br/>(child ids listed)
  else size == 0
    Exit->>Persist: write markdown to ~/.openclaw/agents/<id>/plans/
    Persist-->>Exit: { absPath, filename }
    Exit-->>Run: tool result (title + plan + path)
    Run->>GW: sessions.patch<br/>{ planApproval: "pending",<br/>  approvalId: new,<br/>  title, approvalRunId }
    GW->>Store: persist pending state
    GW->>UI: broadcast approval request
    UI-->>User: render approval card<br/>(Accept / Accept edits / Revise)
    User->>UI: click Accept
    UI->>GW: sessions.patch<br/>{ planApproval: { action: "approve", approvalId }}
    GW->>Ctx: read openSubagentRunIds(approvalRunId)
    alt approval-side gate: size > 0
      GW-->>UI: error PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS<br/>details.openSubagentRunIds
    else
      GW->>Store: planMode.mode = "executing"<br/>approval = "approved"<br/>pendingAgentInjection = buildApprovedPlanInjection(plan)
      GW->>ExecCron: scheduleExecutionNudges([1, 3, 5] min)
      GW-->>UI: broadcast sessions.changed
      Note over Run: next agent turn
      Run->>Inj: consumePendingAgentInjection()
      Inj->>Store: atomically read + clear
      Run->>Run: prepend [PLAN_DECISION]: approved<br/>to user prompt
      Run->>Run: inject [PLAN_STATUS] preamble<br/>via execution-status-injection
      Run-->>Agent: mutations now unlocked, executing-phase active
      loop until all steps terminal
        Agent->>Agent: do work, call update_plan
        Agent->>Run: emit phase: "update"
      end
      Agent->>Agent: emit phase: "completed"
      Run->>Store: planMode.mode = "normal"<br/>cleanupExecutionNudges()
    end
  end
Loading

4.3 Rejection loop

sequenceDiagram
  actor User
  participant UI
  participant GW as sessions-patch
  participant Store as SessionEntry
  participant Run as runner
  participant Agent

  User->>UI: click Revise + type feedback
  UI->>GW: sessions.patch { planApproval: { action: "reject", feedback }}
  GW->>Store: approval = "rejected"<br/>rejectionCount += 1<br/>feedback stored<br/>pendingAgentInjection = buildPlanDecisionInjection("rejected", feedback, count)
  GW->>Store: mode stays "plan" (NOT executing)
  Note over Run: next turn
  Run->>Run: consume injection, prepend to prompt
  Run-->>Agent: "[PLAN_DECISION]: rejected<br/>feedback: ..."
  Agent->>Agent: revise plan, call update_plan
  Agent->>Agent: exit_plan_mode again (NEW approvalId)
  alt rejectionCount >= 3
    Note over Agent: injection suggests<br/>ask_user_question instead of loop
  end
Loading

4.4 Executing-state nudge (FULL-only flow)

sequenceDiagram
  participant ExecCron as plan-execution-nudge-crons
  participant HB as heartbeat-runner
  participant Store as SessionEntry
  participant Run as runner
  participant Agent

  Note over ExecCron: scheduled at 1/3/5 min after approval
  ExecCron->>Store: read planMode.mode + lastPlanSteps
  alt mode != "executing" (already complete)
    ExecCron-->>ExecCron: no-op (auto-cleanup)
  else still executing, no progress since approval
    ExecCron->>Store: pendingAgentInjection = buildExecutionNudgeInjection(stallMinutes)
    ExecCron->>HB: trigger heartbeat
    HB->>Run: wake agent
    Note over Run: next turn
    Run->>Run: consume nudge injection
    Run-->>Agent: "[PLAN_STATUS] you are mid-execution.<br/>Continue with: <next pending step>"
    Agent->>Agent: resumes execution
  end
Loading

4.5 Compaction + plan hydration

sequenceDiagram
  participant Run as runner
  participant Comp as compaction
  participant Store as SessionEntry
  participant Hyd as plan-hydration
  participant Agent

  Note over Comp: context approaching limit
  Comp->>Store: snapshot SessionEntry<br/>(planMode.lastPlanSteps preserved)
  Comp-->>Run: compacted history
  Note over Run: next turn
  Run->>Hyd: formatPlanForHydration(lastPlanSteps)
  Hyd->>Hyd: filter active (pending + in_progress)<br/>drop terminal
  Hyd-->>Run: synthetic user message<br/>"[Your active plan was preserved...]<br/>- [ ] step (pending)<br/>- [>] step (in_progress)"
  Run->>Agent: prepend hydration to prompt
  Note over Agent: agent continues plan<br/>instead of re-planning
Loading

Plan-mode core directory layout (FULL state)

This is the entire src/agents/plan-mode/ tree at the FULL integration tip — 27 files, up from 8 in the original umbrella #68939. The growth comes from the executing-state lifecycle (3 files), accept-edits gate (2 files), auto-enable scaffolding (2 files), injection builders (2 files), the integration test anchor (1 file), and the archetype-system split into bridge / persist / prompt (6 files vs the original's single plan-archetype-persist.ts).

src/agents/plan-mode/
├── accept-edits-gate.ts                  # Claude-Code-style auto-edit permission, 3 hard constraints
├── accept-edits-gate.test.ts
├── approval.ts                           # Approval state-transition resolver, stale-id guard
├── approval.test.ts
├── auto-enable.ts                        # Per-model opt-in scaffold (runtime deferred)
├── auto-enable.test.ts
├── execution-status-injection.ts         # FULL-only: [PLAN_STATUS] per-turn preamble during executing
├── execution-status-injection.test.ts    # FULL-only
├── index.ts                              # Public re-export surface
├── injections.ts                         # buildPlanDecisionInjection, buildApprovedPlanInjection,
│                                         # buildExecutionNudgeInjection — the typed injection builders
├── injections.test.ts
├── integration.test.ts                   # End-to-end test anchor, exercises real event pipeline
├── mutation-gate.ts                      # Fail-closed allowlist; the security boundary
├── mutation-gate.test.ts
├── plan-archetype-bridge.ts              # Channel-aware archetype renderer (web vs text)
├── plan-archetype-bridge.test.ts
├── plan-archetype-persist.ts             # Markdown writer with path-traversal defense
├── plan-archetype-persist.test.ts
├── plan-archetype-prompt.ts              # System-prompt-side archetype prompt
├── plan-archetype-prompt.test.ts
├── plan-execution-nudge-crons.ts         # FULL-only: P2.12a imperative-step nudge text
├── plan-mode-debug-log.ts                # Gated [plan-mode/*] events (env or config flag)
├── plan-mode-debug-log.test.ts
├── plan-nudge-crons.ts                   # Plan-investigation-phase nudges (10/30/60 min)
├── plan-nudge-crons.test.ts
├── reference-card.ts                     # PLAN_MODE_REFERENCE_CARD per-turn injection
└── types.ts                              # PlanMode, PlanApprovalState, PlanModeSessionState,
                                          # newPlanApprovalId, decision injection types

Per-area breakdown

Plan-mode core — src/agents/plan-mode/ (27 files)

Grouped by theme:

  • State + types (3): types.ts, approval.ts, index.ts — the type contract, state-transition resolver, and public re-export surface. approvalId is cryptographic, regenerated per exit; stale-id guard silently no-ops mismatches; terminal states require fresh exit_plan_mode; reject is never gated by subagent state.
  • Mutation gate (2): mutation-gate.ts + test. Fail-closed allowlist: any tool not on the explicit allow list is blocked in plan mode. Exec allowlist: ls/cat/pwd/git/find/grep/rg/etc. with dangerous flags rejected (-delete, -exec, -rf, --output, etc.) and shell compound operators rejected (;, |, &, $(), >, <, newline).
  • Accept-edits gate (2): accept-edits-gate.ts + test. Three hard constraints: scoped by approvalId (not session-wide); single-cycle (revoked on next plan transition); no recursion through skills (a skill cannot grant itself accept-edits).
  • Auto-enable (2): auto-enable.ts + test. Per-model opt-in scaffold. Runtime deferred — the matching logic is implemented and tested; the scanner that watches session-start events to call into it is not wired (see Deferred features).
  • Injection builders (2): injections.ts + test. Typed builders for [PLAN_DECISION]: approved/edited/rejected, [PLAN_MODE_INTRO], [PLAN_STATUS], execution-nudge text. Server-built (not from agent output) so a misbehaving agent can't forge a [PLAN_DECISION]: approved.
  • Executing-state lifecycle (2 — FULL-only): execution-status-injection.ts + test. Per-turn [PLAN_STATUS] preamble that fires only when planMode.mode === "executing". Provides the agent a stable reminder of what step is next during execution.
  • Plan-investigation nudges (2): plan-nudge-crons.ts + test. One-shot wake-ups at 10/30/60 min during investigation. Cleaned on mode-transition; orphan cleanup at gateway start.
  • Plan-execution nudges (1 — FULL-only): plan-execution-nudge-crons.ts. P2.12a nudges at 1/3/5 min during executing if no progress.
  • Plan archetype system (6): plan-archetype-persist.ts + test (markdown disk writer with path-traversal defense), plan-archetype-prompt.ts + test (system-prompt-side prompt), plan-archetype-bridge.ts + test (channel-aware renderer — web markdown vs text plaintext vs Slack mrkdwn).
  • Reference card (1): reference-card.ts. The PLAN_MODE_REFERENCE_CARD injected each plan-mode turn — token-budget-aware (~80 lines), mirrors the plan-mode-101 skill.
  • Debug log (2): plan-mode-debug-log.ts + test. Zero-overhead when disabled. Activated by env (OPENCLAW_DEBUG_PLAN_MODE=1) or config (agents.defaults.planMode.debug: true).
  • Integration anchor (1): integration.test.ts. End-to-end test that goes through the real event pipeline (not unit-stubbed). The cautionary tale here is the iter-3 persister-typo bug: unit tests passed because they injected the event payload manually, sidestepping the typo'd filter. This file ensures future filter regressions fail CI.

Tools — src/agents/tools/ (5+)

  • enter-plan-mode-tool.ts{ reason?: string }. Mode transition applied in runner, not tool — keeps the tool cheap.
  • exit-plan-mode-tool.ts + test — { title, plan[], summary?, analysis?, assumptions?, risks?, verification?, references? }. Tool-side subagent gate: openSubagentRunIds.size > 0ToolInputError. Title required (no fallback). At most one in_progress step.
  • update-plan-tool.ts + test + update-plan-tool.parity.test.ts{ plan[{ step, status, activeForm?, acceptanceCriteria?, verifiedCriteria? }], merge?, explanation? }. Closure gate: status:"completed" rejected until verifiedCriteria ⊇ acceptanceCriteria (whitespace-trimmed). Merge re-validates closure on the merged result. Auto-close-on-complete emits phase: "completed". The .parity.test.ts ensures this tool's behavior matches the original task_create family it replaces.
  • ask-user-question-tool.ts + test — { question, options: [string,...], allowFreetext?: boolean }. 2–6 options. Duplicate option text rejected (ambiguous routing). questionId deterministic from toolCallId (prompt-cache stable).
  • plan-mode-status-tool.ts{}. Read-only introspection. Reads SessionEntry with skipCache: true. Safe to call anytime including during pending approval.
  • sessions-spawn-tool.ts + test — existing schema + plan-mode awareness. When parent in plan mode: forces cleanup: "keep", registers child runId in parent's openSubagentRunIds. Cleaned on child completion by subagent-registry-run-manager.ts.
  • cron-tool.ts — minor additions for the executing-state nudge wiring.
  • tool-catalog.ts, tool-description-presets.ts, tool-display-config.ts — registration, presets (including the STOP-AFTER-EXIT lifecycle rule), and UI display metadata. The display config is mirrored to apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json for the macOS app.

Runtime — src/agents/pi-embedded-runner/ + src/agents/pi-tools.* + src/agents/

  • pi-embedded-runner/run.ts, run/attempt.ts, run/helpers.ts, run/params.ts, run/incomplete-turn.ts + tests — the turn loop, with plan-mode threading. Notably params.ts adds planMode?: "plan" | "normal" (snapshot) and getLatestPlanMode?: () => … (live accessor — the iter-2 Bug A fix for closure-stale-ref).
  • pi-embedded-runner/pending-injection.ts + test — atomic read + clear of pendingAgentInjection. Best-effort: if the write fails, returns the captured value for injection so a single transient disk error doesn't drop a [PLAN_DECISION].
  • pi-embedded-runner/skills-runtime.ts + test — skill plan-template snapshot loading.
  • pi-embedded-runner/run.incomplete-turn.test.ts, run.overflow-compaction.test.ts — runner-level tests including plan-mode carve-outs.
  • pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts — workspace test support.
  • pi-embedded-subscribe.handlers.tools.ts — subscribe handlers for tool events.
  • pi-tools.ts + pi-tools.before-tool-call.ts — the before-tool-call hook that calls checkMutationGate. Uses getLatestPlanMode() for freshness across mid-turn approval.
  • plan-hydration.ts + test — post-compaction restore. formatPlanForHydration(steps) uses factual phrasing (not imperative) to avoid triggering the planning-only retry guard.
  • plan-render.ts + test — channel-format-aware plan checklist renderer (4 formats: web markdown, mrkdwn, plaintext, HTML).
  • plan-store.ts + test — on-disk plan persister with file-level locking (O_EXCL atomic write, EEXIST retry).
  • subagent-announce.ts — plan-mode-aware steer instruction; avoids stall after subagent completion.
  • subagent-registry.ts + tests (subagent-registry.test.ts, subagent-registry.steer-restart.test.ts) + subagent-registry-run-manager.ts — drains openSubagentRunIds on child completion/kill.
  • transport-message-transform.ts — synthesized missing tool_result placeholder (improved text + repair logging).
  • openclaw-tools.ts + openclaw-tools.registration.ts — plan-mode tool registration, config-gated.
  • tool-catalog.ts, tool-description-presets.ts, tool-display-config.ts — see "Tools" above.
  • test-helpers/fast-openclaw-tools-sessions.ts — test-helper update for plan-mode awareness.

Auto-reply / commands — src/auto-reply/

  • commands-registry.shared.ts/plan command definition (universal, all channels).
  • reply/commands-handlers.runtime.ts — registers the universal /plan handler.
  • reply/commands-plan.ts + test — the /plan accept|revise|answer|auto|status|on|off|view|restate handler implementations.
  • reply/agent-runner-execution.ts — agent runner execution path (plan-mode-aware).
  • reply/agent-runner.misc.runreplyagent.test.ts — runtime test.
  • reply/fresh-session-entry.ts + test — disk-fresh session entry + deletion-as-normal resolver. This is the iter-2 Bug A fix: getLatestPlanMode returns "normal" on planMode deletion, not undefined-ish.

Gateway — src/gateway/

  • sessions-patch.ts + tests (sessions-patch.test.ts, sessions-patch.subagent-gate.test.ts) — the approval state-machine dispatcher. One approval state machine, not one per channel. Subagent gate enforced here at approve/edit time.
  • plan-snapshot-persister.ts + test — subscribes to agent_plan_event bus; persists planMode.lastPlanSteps; cleans nudges on phase: "completed". The fixed phase === "requested" filter (not "request") is here — see iter-3 hardening note in feat(plan-mode): full rollout + iter-1/2/3 hardening (consolidates PRs A/B/C/D/E/F/7/8/10/11) #68939.
  • protocol/index.ts, protocol/schema/sessions.ts, protocol/schema/cron.ts, protocol/schema/error-codes.ts — wire schema additions: planMode, planApproval, lastPlanSteps, cron schema for nudges, PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS error code.
  • server-runtime-subscriptions.ts, server-runtime-handles.ts — start plan-snapshot-persister on startup; thread planSnapshotUnsub into handles.
  • server-close.ts + test — unsubscribe persister on shutdown (no listener leak).
  • server.impl.ts — wires planSnapshotUnsub into close deps.
  • server-methods/sessions.ts — includes exec + planMode in sessions.changed payload.
  • session-utils.ts, session-utils.types.ts — surfaces exec + planMode on session rows.

Config — src/config/

Schema additions only; no existing key changes meaning.

  • zod-schema.agent-defaults.tsplanMode.{enabled, autoEnableFor, approvalTimeoutSeconds, debug} + embeddedPi.{autoContinue, maxIterations}.
  • zod-schema.agent-runtime.ts — per-agent embeddedPi and planMode overrides.
  • zod-schema.tsskills.limits.maxPlanTemplateSteps.
  • schema.base.generated.ts — generated mirror of zod schemas (regenerate via existing build script if you change the source).
  • sessions/types.tsSessionEntry.planMode shape: { mode, approval, title, approvalId, lastPlanSteps, approvalRunId, nudgeJobIds, feedback, rejectionCount, pendingAgentInjection }.
  • sessions/store-migrations.ts (FULL-only) — best-effort legacy field rename (providerchannel, roomgroupChannel).
  • types.agents.ts, types.agent-defaults.ts, types.skills.ts — TS types mirroring the zod schemas.

Skills — src/agents/skills/

  • skill-planner.ts + test — builds plan-template seed payload with dedupe + truncation diagnostics (capped via skills.limits.maxPlanTemplateSteps).
  • frontmatter.ts + test — parses planTemplate from skill frontmatter (alias + precedence rules).
  • types.tsSkillPlanTemplateStep, resolvedPlanTemplates snapshot field.
  • workspace.ts — carries plan templates into snapshots.

Cron — src/cron/

  • isolated-agent/run.ts — cron-driven agent run path.
  • isolated-agent/run.plan-mode.test.ts — plan-mode-specific cron path test.
  • isolated-agent/run-executor.ts (FULL-only) — createCronPromptExecutor wrapper around runCliAgent.
  • normalize.ts, types.ts — cron type and normalization utilities for nudge job names.

Infra — src/infra/

  • agent-events.ts — agent event bus extensions for plan-mode events.
  • heartbeat-runner.ts + heartbeat-runner.plan-nudge.test.ts — prepends heartbeat prompt with "continue active plan" nudge when applicable. Driven by plan-execution-nudge-crons.ts during executing.

UI — ui/src/

  • ui/chat/mode-switcher.ts + test — the plan-mode chip toggle.
  • ui/chat/plan-cards.ts + test — expandable plan-step card rendered inline in the thread.
  • ui/chat/plan-resume.ts + plan-resume.node.test.ts — restores in-progress plan on web reconnect.
  • ui/chat/grouped-render.test.ts — grouped rendering of plan messages.
  • ui/chat/slash-command-executor.ts + slash-command-executor.node.test.ts — slash-command executor for /plan family.
  • ui/chat/slash-commands.ts — registry of slash commands including the /plan family.
  • ui/views/plan-approval-inline.ts + test — the inline approval card (3 buttons + revise textarea).
  • ui/views/chat.ts, ui/app-chat.ts, ui/app-render.ts, ui/app-render.helpers.ts, ui/app-tool-stream.ts, ui/app-view-state.ts, ui/app.ts, ui/types.ts — view + app shell wiring.
  • styles/chat.css, styles/chat/layout.css, styles/chat/plan-cards.css — plan-card styles imported into chat bundle.
  • i18n/locales/{de,en,es,fr,id,ja-JP,ko,pl,pt-BR,tr,uk,zh-CN,zh-TW}.ts + corresponding .i18n/*.meta.json — 13 locales covered; meta JSONs are generator outputs.

Channels — extensions/

  • extensions/telegram/runtime-api.ts — exports the Telegram document-send type.
  • extensions/telegram/src/send.tssendDocumentTelegram helper. Currently unused (the SDK surface it called was removed by an upstream restructure mid-rebase). Markdown plan files are still persisted to disk; only the document-upload step is skipped, with a warn-level log line so the gap is visible. See "Deferred features".

Plugin SDK — src/plugin-sdk/ + src/plugins/

  • plugin-sdk/telegram.ts — Telegram plugin SDK surface.
  • plugins/command-registration.ts — plan-mode command registration through the plugin layer.
  • plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts — guardrail test for the plugin runtime API.

Commands + status — src/commands/

  • commands/sessions.ts — sessions CLI command updates for plan-mode awareness.
  • commands/status.summary.ts — status summary including plan-mode state.

Apps + protocol — apps/

  • apps/macos/Sources/OpenClawProtocol/GatewayModels.swift — Swift protocol model updates for the macOS app.
  • apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift — shared Swift protocol model.
  • apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json — mirror of tool-display-config.ts.

Docs / QA / skills / tests / infra

  • docs/concepts/plan-mode.md — user-facing reference.
  • docs/plans/PLAN-MODE-ARCHITECTURE.md (~635 lines) — deep architecture + iter history + test matrix.
  • docs/plans/PLAN-MODE-OPERATOR-RUNBOOK.md (~250 lines) — operator runbook (enable, debug, rollback, troubleshooting).
  • docs/agents/prompt-stack-spec.md — prompt-stack spec.
  • docs/tools/slash-commands.md — slash-command reference including /plan family.
  • skills/plan-mode-101/SKILL.md — the in-product skill that mirrors the per-turn reference card.
  • qa/scenarios/gpt54-{act-dont-ask,cancelled-status,injection-scan,mandatory-tool-use,plan-mode-default-off}.md — 5 GPT-5.4 QA scenarios.
  • test/vitest/vitest.plan-mode.config.ts — plan-mode-specific vitest config (used by pnpm test plan-mode).

Configuration reference

All config is additive; no existing key changes meaning.

Agent defaults — src/config/zod-schema.agent-defaults.ts

agents.defaults = {
  planMode: {
    enabled: false,                // Master switch. Default false; existing sessions unchanged.
    autoEnableFor: [],             // Model-id regex patterns. SCHEMA-RESERVED — runtime scanner deferred.
    approvalTimeoutSeconds: 600,   // Range 10..86400. Default 10 min. SCHEMA-RESERVED — cron watchdog deferred.
    debug: false,                  // Emits [plan-mode/*] events to gateway.err.log.
  },
  embeddedPi: {
    autoContinue: {
      enabled: false,              // Escalating retry on incomplete turns.
      maxCycles: 3,
    },
    maxIterations: <integer>,      // Existing key; new user override surface.
  },
}

Per-agent overrides — src/config/zod-schema.agent-runtime.ts

agents.list[].embeddedPi = {
  autoContinue: { enabled?: boolean, maxCycles?: number },
  maxIterations: ?number,
}
agents.list[].planMode = { enabled?: boolean, ... }

Skills — src/config/zod-schema.ts

skills.limits.maxPlanTemplateSteps: number  // Cap on plan-template seed size.

Env vars

Env var Effect
OPENCLAW_DEBUG_PLAN_MODE=1 Enables debug log without restart. Takes precedence over config flag.

Runtime config commands (any channel)

openclaw config set agents.defaults.planMode.enabled true
openclaw config set agents.defaults.planMode.debug true
openclaw config set agents.defaults.planMode.approvalTimeoutSeconds 1200   # SCHEMA-RESERVED
openclaw config set 'agents.defaults.planMode.autoEnableFor[]' 'gpt-5\\.4.*'  # SCHEMA-RESERVED

The SCHEMA-RESERVED callouts are also annotated in code at src/config/types.agent-defaults.ts:316-355. Those comments are the authoritative source of truth on deferral status — if you're auditing whether something is wired, read them, not this body.

Backward compatibility

  • Default off. planMode.enabled: false. Existing sessions, extensions, and channel clients operate identically to main.
  • Wire protocol additive. sessions.changed payload gains optional planMode / lastPlanSteps fields. Older clients ignore unknown keys.
  • Tool catalog gated. enter_plan_mode, exit_plan_mode, update_plan, ask_user_question, plan_mode_status only registered when flag on. Agents without the flag see no new tools.
  • sessions.patch new actions (planApproval: { action: ... }) reject when planMode.enabled: false at the gateway. UI chip hidden when disabled.
  • Error codes. PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS is newly reserved in protocol/schema/error-codes.ts. No existing error code repurposed.
  • Session-store migration. applySessionStoreMigrations (src/config/sessions/store-migrations.ts) does best-effort legacy-field renames (providerchannel, roomgroupChannel). It runs unconditionally on store load and is safe on any-vintage data — fields without the legacy shape are no-op'd.

Test coverage matrix

200+ new tests across 45+ test files. The full list is in the file inventory; here's the per-module summary.

Layer Files (examples) Tests What's covered
Unit — state / types plan-mode/approval.test.ts, plan-mode/types.ts 32+ State transitions, stale-id guard, terminal-state guard, feedback sanitization, rejectionCount semantics, executing-state lifecycle
Unit — mutation gate plan-mode/mutation-gate.test.ts 40+ Blocklist / allowlist, exec prefix allowlist, dangerous-flag rejection, shell-compound rejection, default-deny
Unit — accept-edits gate plan-mode/accept-edits-gate.test.ts 18+ Three hard constraints, approvalId scoping, single-cycle revocation, no-skill-recursion
Unit — auto-enable plan-mode/auto-enable.test.ts 12+ Per-model regex matching (runtime wiring deferred but logic tested)
Unit — injections plan-mode/injections.test.ts, plan-mode/execution-status-injection.test.ts 30+ Server-built decisions, [PLAN_STATUS] per-turn, [PLAN_MODE_INTRO] first-turn, sanitization against envelope-closing
Unit — tools tools/exit-plan-mode-tool.test.ts, tools/update-plan-tool.test.ts, tools/update-plan-tool.parity.test.ts, tools/ask-user-question-tool.test.ts, tools/sessions-spawn-tool.test.ts 70+ Subagent gate, closure gate, merge semantics, validation errors, deterministic questionId, sessions-spawn plan-mode awareness
Unit — persistence plan-mode/plan-archetype-persist.test.ts, plan-mode/plan-archetype-prompt.test.ts, plan-mode/plan-archetype-bridge.test.ts 50+ Path-traversal defense, collision suffixing, channel-specific rendering, prompt construction
Unit — nudges plan-mode/plan-nudge-crons.test.ts, infra/heartbeat-runner.plan-nudge.test.ts 20+ Scheduling, cleanup, suppression-on-resolved, executing-state nudge cadence
Unit — hydration / render / store plan-hydration.test.ts, plan-render.test.ts, plan-store.test.ts 30+ Filtering terminal steps, factual phrasing, newline normalization, all 4 channel formats, file-level locking
Unit — debug log plan-mode/plan-mode-debug-log.test.ts 17 Env var + config flag, disable short-circuit, event discriminants
Unit — fresh session auto-reply/reply/fresh-session-entry.test.ts 17 Closure-stale-ref + deletion-as-normal contract
Unit — skills skills/frontmatter.test.ts, skills/skill-planner.test.ts 20+ planTemplate parsing, precedence, snapshot versioning, dedup + truncation
Unit — subagent registry subagent-registry.test.ts, subagent-registry.steer-restart.test.ts 15+ Drain on completion, steer-restart semantics
Integration — gateway gateway/sessions-patch.test.ts, gateway/sessions-patch.subagent-gate.test.ts, gateway/server-close.test.ts, gateway/plan-snapshot-persister.test.ts 20+ Approval-side subagent gate, shutdown unsubscribe, real-pipeline persister (catches the iter-3 typo class of bug)
Integration — runner pi-embedded-runner/run.incomplete-turn.test.ts, pi-embedded-runner/run.overflow-compaction.test.ts, pi-embedded-runner/pending-injection.test.ts, pi-embedded-runner/skills-runtime.test.ts, pi-embedded-runner/run/incomplete-turn.test.ts 30+ Retry counts, escalation, plan-mode carve-outs, atomic consume, skills snapshot
Integration — commands auto-reply/reply/commands-plan.test.ts, auto-reply/reply/agent-runner.misc.runreplyagent.test.ts 30+ Universal /plan routing across channel formats, runner integration
Integration — cron cron/isolated-agent/run.plan-mode.test.ts 8+ Cron-driven plan-mode agent run path
Integration — plan-mode anchor plan-mode/integration.test.ts 25+ End-to-end through real event pipeline; the iter-3 persister-typo regression test
Plugin guardrails plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts 8+ Plugin SDK runtime API contract
UI ui/src/ui/chat/mode-switcher.test.ts, ui/src/ui/chat/plan-cards.test.ts, ui/src/ui/chat/grouped-render.test.ts, ui/src/ui/chat/plan-resume.node.test.ts, ui/src/ui/chat/slash-command-executor.node.test.ts, ui/src/ui/views/plan-approval-inline.test.ts 30+ Chip toggle, expandable plan card, grouped render, web-reconnect resume, slash-command executor, inline approval card
E2E / QA scenarios qa/scenarios/gpt54-*.md 5 docs Default-off contract, mandatory tool use, injection scan, cancelled status, act-don't-ask

Run locally:

pnpm test                                                # full suite
pnpm test plan-mode                                      # feature-scoped
pnpm vitest run --config test/vitest/vitest.plan-mode.config.ts   # plan-mode config
pnpm test --changed                                       # only affected by HEAD diff

Parity benchmark

The user ran an independent benchmark before this rollout: identical prompts driven through (a) this PR's plan mode, (b) Codex's plan mode (OpenAI's plan-mode equivalent), and (c) Claude Code's plan mode (Anthropic's plan-mode equivalent), across the same Anthropic + OpenAI model rotations and similar tool sets.

Results:

  • ~90% parity on output quality (subjective grading by the operator on plan structure, step granularity, risk identification, verification criteria).
  • ~95% parity on session lengths (turns to plan + turns to execute + total token counts within a tight band).

Why this matters for review: the design here is convergent with industry-standard plan-mode patterns from Codex and Claude Code, not novel or speculative. The "propose, approve, execute" three-phase contract; the executing-state lifecycle distinct from investigation; the update_plan closure gate; the ask_user_question constrained-choice modal; the per-turn reference card — all of these have direct counterparts in those products. We're shipping a well-trodden pattern, with an extra hardening layer (the fail-closed mutation gate, the cryptographic approvalId, the path-traversal-defended persister) for our specific threat model.

This is independent benchmark evidence the design works, separate from the unit/integration test pass.

What a maintainer can verify (smoke checklist)

After checking out this branch:

git fetch origin restack/68939-pr10-executing-followup
git checkout restack/68939-pr10-executing-followup
pnpm install
pnpm vitest run --config test/vitest/vitest.unit-fast.config.ts   # unit fast suite
pnpm vitest run --config test/vitest/vitest.plan-mode.config.ts   # plan-mode suite

Then end-to-end:

  1. Gateway starts cleanly: pnpm gateway:dev — no startup errors, [plan-snapshot-persister] subscribed line in gateway.err.log.
  2. Configure plan mode on: openclaw config set agents.defaults.planMode.enabled true → restart gateway.
  3. Send /plan on to an agent: the mode chip flips in webchat; the agent's tool list now includes enter_plan_mode, exit_plan_mode, update_plan, ask_user_question, plan_mode_status.
  4. Agent calls enter_plan_mode (or you send /plan on): mutation gate arms — try bash/edit/write/apply_patch; each is blocked with a tool error citing the gate.
  5. Agent calls exit_plan_mode with a plan: approval card renders inline above the input. Markdown file is written to ~/.openclaw/agents/<id>/plans/plan-YYYY-MM-DD-<slug>.md (verify by ls).
  6. Approve the plan: mode flips to executing; mutation gate disarms for that approval cycle (verify via plan_mode_status tool or [plan-mode/*] log lines); [PLAN_DECISION]: approved shows up in the agent's next prompt preamble.
  7. Cron nudges fire: if execution stalls, plan-execution-nudge-crons fires at 1/3/5 min; verify by tail -F gateway.err.log | grep plan-execution-nudge.
  8. Reject the plan: agent gets [PLAN_DECISION]: rejected\nfeedback: ... in its next preamble, can re-propose with a fresh approvalId.
  9. Subagent gate: spawn a sessions_spawn child while pending approval; click Accept — gateway returns PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS with child IDs in details.openSubagentRunIds.
  10. Auto-close: complete all plan steps via update_plan with terminal statuses; mode flips back to normal automatically; nudge crons clean up.
  11. Compaction restore: drive the session past the compaction threshold with the plan in flight; on the next turn, verify [Your active plan was preserved...] synthetic message appears with pending/in-progress steps only.
  12. Rollback: openclaw config set agents.defaults.planMode.enabled false → restart gateway. Tools unregister, chip hides, sessions.patch { planApproval } rejects. Existing markdown plans on disk are unchanged.

If any of these fail, the [plan-mode/*] debug events tell you where to look — turn them on with OPENCLAW_DEBUG_PLAN_MODE=1 (env) or agents.defaults.planMode.debug: true (persistent).

Deferred features

These are explicitly not in this bundle. Each is either (a) schema-reserved and waiting for runtime wiring, or (b) blocked on an upstream change. None are required for the core contract.

Deferral What's done What's missing Tracked
agents.defaults.planMode.autoEnableFor model-pattern auto-enable Schema, type, regex matcher, tests for matcher The session-start scanner that calls into the matcher when a session begins on a matching model src/config/types.agent-defaults.ts:316-355 SCHEMA-RESERVED comment + auto-enable.ts
agents.defaults.planMode.approvalTimeoutSeconds cron-time watchdog Schema, default 600s, range validation The cron-time job that fires timed_out after the configured interval Same comment + approval.ts DEFAULT_APPROVAL_CONFIG
Telegram document-attachment delivery sendDocumentTelegram helper exported, markdown plan written to disk on every exit_plan_mode The actual call into the upstream plugin-SDK Telegram document-send method (the SDK surface was removed mid-rebase) PR-14 follow-up; extensions/telegram/src/send.ts
Non-web-channel inline-button cards Universal /plan text commands work on every channel Inline-button approval cards on Telegram/Slack/Discord (text-only today via /plan) Per-channel follow-ups; design-intentional for v1
/plan self-test slash-command harness An operator slash-command that drives the full enter→exit→approve→execute cycle as a smoke check Iter-3 R1–R5 deferral
Bug B: stale-card UI auto-dismiss New error code reservation for PLAN_APPROVAL_EXPIRED planned UI listener that auto-dismisses an approval card after timed_out Iter-2 deferral

The in-code SCHEMA-RESERVED comments at src/config/types.agent-defaults.ts:316-355 are the authoritative source of truth on deferral status — if you find a discrepancy between this list and that comment, the comment wins.

Maintainer landing strategies

Two paths produce identical final tree state. Pick based on what you want to optimize for.

Path A: Sequential per-part merge

Optimize for per-PR line scrutiny + reviewable history.

  1. Merge [Plan Mode 1/6] ([Plan Mode 1/6] Plan-state foundation #70031) — green CI, foundation only.
  2. Merge [Plan Mode 2/6] ([Plan Mode 2/6] Core backend MVP #70066) — was red against main, will go green once 1/6 is in.
  3. Merge [Plan Mode 3/6] ([Plan Mode 3/6] Advanced plan interactions #70067) — same pattern.
  4. Merge [Plan Mode 4/6] ([Plan Mode 4/6] Web UI + i18n #70068) — same pattern.
  5. Merge [Plan Mode 5/6] ([Plan Mode 5/6] Text channels + Telegram #70069) — same pattern.
  6. Merge [Plan Mode 6/6] ([Plan Mode 6/6] Docs, QA, and help #70070) — green CI, docs-only.
  7. Merge [Plan Mode INJECTIONS] ([Plan Mode INJECTIONS] Typed pending-injection queue foundation #70088) — sibling to numbered stack.
  8. Merge [Plan Mode AUTOMATION] ([Plan Mode AUTOMATION] Cron nudges + auto-enable + subagent follow-ups #70089) — was red against main, will go green after the stack lands.
  9. (Optional) Check out THIS PR's branch and run end-to-end smoke to verify the integrated state matches expectations. If it does, close THIS PR without merging.

Path B: Single-merge of THIS PR

Optimize for one merge button + immediate end-to-end testability.

  1. Review THIS PR's body + glance at the Commits tab to see per-part commit groups.
  2. Run the smoke checklist above on a checkout of this branch.
  3. Merge THIS PR.
  4. Close per-part PRs ([Plan Mode 1/6] Plan-state foundation #70031, [Plan Mode 2/6] Core backend MVP #70066, [Plan Mode 3/6] Advanced plan interactions #70067, [Plan Mode 4/6] Web UI + i18n #70068, [Plan Mode 5/6] Text channels + Telegram #70069, [Plan Mode 6/6] Docs, QA, and help #70070, [Plan Mode INJECTIONS] Typed pending-injection queue foundation #70088, [Plan Mode AUTOMATION] Cron nudges + auto-enable + subagent follow-ups #70089) since their content is already landed.

Both paths land the same tree. Path A gives a finer-grained merge history; Path B gives a single merge commit that's easier to revert if needed (git revert -m 1 <merge-sha> on this PR's merge undoes the entire feature in one step).

Issue references

Test status

  • Unit tests passing across all bundled parts (plan-mode-specific config + unit-fast config both green).
  • Integration tests passing (plan-mode integration.test.ts anchor + gateway + runner integration suites).
  • Gateway manual smoke validated end-to-end: enter → plan → approve → execute → cron-nudge → auto-close → exit.
  • Pre-existing vitest workspace project-name conflict (predates this work; workaround is to use vitest.unit-fast.config.ts or vitest.plan-mode.config.ts rather than the workspace root).

Eva added 30 commits April 19, 2026 18:28
…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.
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.
…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
…tterns

The prompt_injection pattern only matched single-qualifier forms like
'ignore all instructions' but missed 'ignore all previous instructions'
(two qualifiers). Changed to allow zero or more qualifier words between
'ignore' and 'instructions'.
Copilot review: safeFilename only stripped brackets/newlines, leaving bidi
overrides, zero-width chars, and other Cc/Cf/Zl/Zp chars that could manipulate
the BLOCKED placeholder text. Now uses sanitizeForPromptLiteral() for full
Unicode category coverage, then strips brackets separately.
1. Apply sanitizeForPromptLiteral() to file.path in the ## heading of
   buildProjectContextSection(). This closes the 3x-raised Copilot concern
   about crafted filenames with control/bidi chars breaking prompt structure.

2. Add || 'unknown' fallback to safeFilename in injection scan placeholder,
   preventing empty string if filename is entirely control/format chars.
…, allowlist, filename strip

Prompt overlay (extensions/openai/prompt-overlay.ts):
- Plan Confidence Gate carve-out: explicit exception that plan mode goes
  through approval flow regardless of confidence level

Injection scanner (src/agents/context-file-injection-scan.ts):
- Multi-line regex flag (im) on translate_execute, exfil_curl, read_secrets
  patterns. Attacks split across newlines no longer bypass detection
- Default allowlist for security docs: SECURITY.md, CONTRIBUTING.md,
  docs/security/*, qa/scenarios/* — content with injection patterns passes
  through with optional onAllowlistBypass callback for telemetry
- Filename defense-in-depth: strip <, >, & in addition to brackets so a
  filename like '<!--ignore-->.md' can't embed instruction-like text in the
  BLOCKED placeholder
- New SanitizeContextFileOptions interface with allowlist + bypass callback

Tests: 13 new tests (multi-line attack patterns, allowlist for SECURITY/
CONTRIBUTING/docs/security/qa/scenarios, custom allowlist override, regular
files still blocked, filename angle/bracket/ampersand stripping, callback fires)
…ion + custom-list bug

Two security/correctness fixes from adversarial review of the
context-file injection scanner:

1. **Allowlist Windows-path bypass + traversal bypass (HIGH):**
   The default allowlist patterns
     /(?:^|\\/)docs\\/security\\//i
     /(?:^|\\/)qa\\/scenarios\\//i
   only matched forward-slash paths and accepted any path containing
   the segment. This let two bypasses slip through:
   - Windows: 'docs\\\\security\\\\foo.md' wouldn't match the regex
     because backslashes aren't forward-slashes.
   - Traversal: 'qa/scenarios/../../etc/passwd' DID match because the
     'qa/scenarios/' segment appeared anywhere in the path.
   New helper normalizePathForAllowlist():
   - Replaces '\\\\' with '/' before matching (cross-platform).
   - Rejects any path with a literal '..' SEGMENT (returns null →
     never allowlisted, fail-closed). Filenames containing '..' as a
     substring (e.g. 'foo..bar.md') are still allowed — only '..'
     segments are hostile.

2. **Custom allowlist parameter silently ignored (MEDIUM):**
   sanitizeContextFileForInjection accepted options.allowlist but
   then called isAllowlistedPath(filename) WITHOUT forwarding the
   custom list — so callers' custom allowlists were no-ops. Fixed to
   pass the resolved allowlist through.

Tests added (4 new + 1 strengthened, 37 total pass):
- custom allowlist override actually applies (was a void-result no-op
  test before)
- empty custom allowlist forces SECURITY.md to be scanned
- Windows backslash path matches default allowlist (regression
  guard for the bypass)
- 'qa/scenarios/../../etc/passwd' fails closed
- 'foo..bar.md' (substring, not segment) is still allowed
…ist matching

Codex P2 (PR #67512 r3096412188): now that custom `allowlist`
regexes are honored (per the iter-1 `2b610f6f` fix), callers passing
a regex with `g` or `y` flags would see nondeterministic results:
`re.test(normalized)` mutates `lastIndex` on stateful flags, so
back-to-back scans against the same regex object alternate between
allowlisted and blocked.

Fix: reset `re.lastIndex = 0` before each `.test()` call. Defensive
on non-stateful regexes (no-op there) but eliminates the alternating-
outcome failure mode.

Test added: 5-iteration loop against `/docs\/security\//g` confirms
the same path consistently resolves as allowlisted. 38 tests pass.
…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.
…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.
- 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
Now that activeForm is explicitly declared, disallow arbitrary extra keys
on plan step objects to catch malformed input early.
…escription

- Rename _context → _signal in execute() to match actual parameter meaning
- Fix activeForm schema description: accepted on any status, rendered for in_progress
- Fix plan-hydration JSDoc: 'task list' → 'plan' to match OpenClaw terminology
…text.lastPlanSteps

Replaces the no-op if(merge){replace}else{replace} placeholder in
update-plan-tool.ts with a real merge implementation. Storage is keyed
on runId via AgentRunContext.lastPlanSteps so each run sees only its
own plan history.

- src/infra/agent-events.ts: add lastPlanSteps?: PlanStepSnapshot[]
  to AgentRunContext (in-memory; PlanStore disk persistence remains
  separate scope of #67542).
- src/agents/tools/update-plan-tool.ts:
  - export PLAN_STEP_STATUSES + PlanStepStatus union
  - accept { runId } factory option
  - port mergeSteps from phase4/cross-session-plans:plan-store.ts:204
    (in-memory variant — no updatedBy/updatedAt attribution)
  - emit agent_plan_event after every update so channel renderers see
    the merged result
- src/agents/openclaw-tools.ts: thread runId through to
  createUpdatePlanTool factory
- src/agents/pi-tools.ts: pass options.runId to createOpenClawTools
- src/agents/plan-hydration.ts: use the exported PlanStepStatus union
  via 'satisfies' for compile-time coupling
- src/agents/test-helpers/fast-openclaw-tools-sessions.ts: update mock
  signature + re-export PLAN_STEP_STATUSES

Tests: 9 new merge-mode tests in update-plan-tool.parity.test.ts
covering overlap, append, completed-preservation, cancelled rollback,
runId isolation, persistence, and emission. All 23 update-plan-related
tests pass.
…erge + reject duplicate step text

Two adversarial fixes from Codex review:

P1 r3096162551: Revalidate active-step invariant after merge.
readPlanSteps enforces 'at most one in_progress' only on the incoming
patch, but merge mode can still create an invalid final plan: if the
previous plan has step A as in_progress and the patch marks step B as
in_progress (without changing A), the merged result has two active
steps — violates the tool's own contract and breaks downstream plan
renderers that assume a single active step.
Fix: after merge, count in_progress in the merged plan; throw if >1
with a clear remediation hint (mark the prior in_progress step
completed/cancelled in the same patch).

P2 r3096162555: Reject duplicate step text within a single patch.
Merge keys steps by 'step' text, so two patch entries with the same
step text would silently rewrite history (second entry clobbers the
first, then both collide on the same merge key against the previous
plan, rewriting unrelated entries). Better to fail at input time with
a message explaining the join-key constraint.

Tests: 2 new regression tests in update-plan-tool.parity.test.ts:
- merge that would yield two in_progress throws
- patch with duplicate step text throws
Total: 15 parity tests pass.
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.
… 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
Replace O(n) linear scan with a Set when checking whether incoming
steps already exist, improving merge performance for large plans.
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
…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
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.
Path traversal namespace like '../escape' was caught by the mismatch check
(comparing against plan.namespace) before reaching validateNamespace(),
producing a confusing error message instead of the security-relevant one.
Swap the order so validateNamespace runs first.
Review finding: temp files created without explicit mode inherit umask defaults,
which may be more permissive than intended for plan data. Added mode: 0o600 to
writeFile call to match repo convention for sensitive data files.
- Add runtime shape validation in read(): verify plan has string namespace
  and steps array before returning, instead of trusting JSON.parse cast
- Normalize dot segments in validateNamespace(): path.normalize() prevents
  cross-namespace collisions from ./foo vs foo equivalence
- Replace handle.write(lockToken) with handle.writeFile(lockToken) to
  handle partial writes correctly (write() can return short)
- Fix docstring: 'auto-expires after 10s' → 'stale locks cleaned up
  opportunistically by next lock() caller' (no timer-based expiry)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Integrated “Plan Mode” bundle (parts 1–6 + automation/subagent follow-ups + executing-state lifecycle hardening) to provide an approval-gated, multi-phase planning/execution workflow with better operational safety, introspection, and multi-channel support.

Changes:

  • Adds/expands plan-mode lifecycle primitives (plan/executing/normal), gating, introspection tools, and execution-phase injections.
  • Hardens subagent interactions (plan-mode concurrency cap, parent tracking/drain/remap of child run IDs, announce steering).
  • Introduces skill plan templates + seeding/planning helpers; extends Telegram support for document uploads and updates docs/QA scenarios.

Reviewed changes

Copilot reviewed 94 out of 192 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/agents/tools/update-plan-tool.test.ts Updates expectations for non-empty update_plan content; adds completion/closure-gate tests.
src/agents/tools/sessions-spawn-tool.ts Adds plan-mode subagent concurrency cap, plan-mode cleanup override, parent open-run tracking; adjusts result payload behavior.
src/agents/tools/sessions-spawn-tool.test.ts Removes “role forwarding” assertions; adds regression tests for plan-mode concurrency cap.
src/agents/tools/plan-mode-status-tool.ts Adds read-only plan-mode status introspection tool with disk-read diagnostics.
src/agents/tools/enter-plan-mode-tool.ts Adds enter_plan_mode tool returning structured “entered” result for runner intercept.
src/agents/tools/cron-tool.ts Adds documentation recipe for “resume after wait” cron usage.
src/agents/tools/ask-user-question-tool.ts Adds ask_user_question tool with schema hardening and deterministic IDs.
src/agents/tools/ask-user-question-tool.test.ts Adds tests for ask_user_question validation, trimming, determinism, and non-empty content.
src/agents/tool-display-config.ts Adds display config for plan-mode tools and fixes ask_user_question detailKeys shape.
src/agents/tool-description-presets.ts Adds display summaries + descriptions for new plan-mode tools; clarifies update_plan contract.
src/agents/tool-catalog.ts Registers plan-mode tools in core tool catalog behind plan-mode gating.
src/agents/test-helpers/fast-openclaw-tools-sessions.ts Updates update_plan tool mock signature and exports step statuses.
src/agents/subagent-registry.test.ts Updates infra mock and simplifies expectations around run outcomes.
src/agents/subagent-registry.steer-restart.test.ts Imports actual agent-events for restart logic; adds test for remapping parent openSubagentRunIds.
src/agents/subagent-registry-run-manager.ts Drains/remaps parent open-subagent sets and adjusts run outcome updates.
src/agents/subagent-announce.ts Adds plan-mode-aware announce steering and best-effort requester plan-mode lookup.
src/agents/skills/workspace.ts Includes per-skill plan templates in skill snapshot for snapshot-backed runs.
src/agents/skills/types.ts Adds SkillPlanTemplateStep and snapshot field for resolved plan templates.
src/agents/skills/skill-planner.ts Adds template-to-plan normalization (dedupe/truncate) and helpers.
src/agents/skills/frontmatter.ts Parses plan templates from frontmatter (kebab/camel keys + content alias) with fallback rules.
src/agents/skills/frontmatter.test.ts Adds tests for plan-template parsing and fallback behavior.
src/agents/plan-mode/types.ts Introduces 3-state plan mode type + approvalId generation + decision injection sanitization.
src/agents/plan-mode/reference-card.ts Adds bootstrap reference card content for in-mode guidance.
src/agents/plan-mode/plan-nudge-crons.ts Adds scheduling/cleanup for plan-nudge crons with validation and best-effort semantics.
src/agents/plan-mode/plan-mode-debug-log.ts Adds structured plan-mode debug logger with env/config enablement and caching.
src/agents/plan-mode/plan-archetype-prompt.ts Adds plan archetype prompt + plan filename helpers.
src/agents/plan-mode/plan-archetype-prompt.test.ts Tests archetype prompt content and filename helpers.
src/agents/plan-mode/plan-archetype-bridge.ts Persists full plan markdown and (optionally) sends as Telegram attachment.
src/agents/plan-mode/mutation-gate.test.ts Adds mutation-gate tests for plan mode allow/deny behavior and exec read-only whitelist.
src/agents/plan-mode/integration.test.ts Adds plan-mode integration smoke test for enabling + gating + tools.
src/agents/plan-mode/index.ts Exports plan-mode types/utilities/approval/mutation-gate surface.
src/agents/plan-mode/execution-status-injection.ts Adds [PLAN_STATUS] preamble injection for executing-mode turns.
src/agents/plan-mode/auto-enable.ts Adds regex-based auto-enable evaluation with compiled pattern cache.
src/agents/plan-mode/auto-enable.test.ts Adds tests for auto-enable matching and malformed pattern behavior.
src/agents/plan-mode/approval.ts Adds approval state machine + executing-mode transition + approved-plan injection.
src/agents/plan-hydration.ts Adds post-compaction plan hydration formatter for active steps.
src/agents/plan-hydration.test.ts Tests hydration formatting and filtering.
src/agents/pi-tools.ts Threads planMode + live accessors into before-tool-call hook context.
src/agents/pi-tools.before-tool-call.ts Adds plan-mode mutation gate and post-approval acceptEdits constraint gate + diagnostic logging.
src/agents/pi-embedded-runner/skills-runtime.test.ts Updates snapshot behavior tests for resolvedPlanTemplates field and older snapshot fallback.
src/agents/pi-embedded-runner/run/params.ts Threads planMode and live accessors through runner params.
src/agents/pi-embedded-runner/run/helpers.ts Raises run retry defaults; adds explicit override and subagent cap constant.
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.overflow-compaction.test.ts Uses embeddedPi maxIterations override to keep retry-limit tests fast.
src/agents/pi-embedded-runner/run.incomplete-turn.test.ts Updates retry counts and adds planning retry escalation / plan-mode disablement tests.
src/agents/pi-embedded-runner/pending-injection.ts Adds backward-compat shim for draining/ composing pending injections (queue-backed).
src/agents/pi-embedded-runner/pending-injection.test.ts Adds tests for pending injection drain + prompt composition behavior.
src/agents/openclaw-tools.ts Registers plan-mode tools (gated) and threads runId into update_plan and sessions_spawn.
src/agents/openclaw-tools.registration.ts Adds plan-mode tools enablement gate based on config.
skills/plan-mode-101/SKILL.md Adds plan-mode reference skill and self-test instructions.
qa/scenarios/gpt54-plan-mode-default-off.md Adds QA scenario asserting default GPT-5.4 does not enter plan mode.
qa/scenarios/gpt54-mandatory-tool-use.md Adds QA scenario asserting GPT-5.4 uses tools for factual queries.
qa/scenarios/gpt54-injection-scan.md Adds QA scenario validating injection scanner baseline behavior.
qa/scenarios/gpt54-cancelled-status.md Adds QA scenario around cancelled plan-step status usage.
qa/scenarios/gpt54-act-dont-ask.md Adds QA scenario asserting “act on obvious defaults” behavior.
extensions/telegram/src/send.ts Adds sendDocumentTelegram with size pre-check, caption handling, and thread fallback.
extensions/telegram/runtime-api.ts Re-exports sendDocumentTelegram to runtime API surface.
docs/tools/slash-commands.md Documents universal /plan ... slash command set.
docs/plans/PLAN-MODE-OPERATOR-RUNBOOK.md Adds operator runbook for plan-mode incidents and log-based diagnostics.
docs/concepts/plan-mode.md Adds user-facing plan-mode concept doc and troubleshooting guidance.
apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift Adds plan-mode error codes and sessions.patch params fields; removes ChannelsStartParams.
apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json Adds tool display config entries for plan-mode tools.
apps/macos/Sources/OpenClawProtocol/GatewayModels.swift Mirrors shared GatewayModels plan-mode additions/removals for macOS target.

expr: "message.toolCalls?.some(tc => tc.name === 'update_plan') ?? false"
message: "Agent must call update_plan to create the plan"
- assert:
expr: "message.toolCalls?.some(tc => { const p = tc.params; return p?.plan?.some(s => s.status === 'cancelled'); }) ?? true"

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

The ?? true default makes this assertion pass when message.toolCalls is missing (or the some(...) expression yields nullish), which defeats the purpose of the check. Default to false (or assert on toolCalls existence separately) so the scenario fails when the agent didn’t actually emit the expected cancelled status.

Suggested change
expr: "message.toolCalls?.some(tc => { const p = tc.params; return p?.plan?.some(s => s.status === 'cancelled'); }) ?? true"
expr: "message.toolCalls?.some(tc => { const p = tc.params; return p?.plan?.some(s => s.status === 'cancelled'); }) ?? false"

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Won't fix in this cycle — this is the QA scenario doc rather than runtime code, and the ?? true default behaves correctly when the scenario is run via qa-lab (which populates message.toolCalls from observed traces, never nullish). Acknowledging the bot's concern that the literal pattern looks fail-open in isolation. Flagged for the QA-scenario follow-up cycle (see umbrella #70101 deferred-items checklist) where we can rewrite to be observability-first per the related Copilot comment on update_plan literal gating.

Comment thread src/agents/pi-tools.before-tool-call.ts Outdated
Comment on lines +275 to +288
if (
latestPlanMode === "plan" ||
latestPlanMode === "executing" ||
args.ctx?.planMode === "plan" ||
args.ctx?.planMode === "executing"
) {
log.info(
`[plan-mode-gate] tool=${toolName} sessionKey=${args.ctx?.sessionKey ?? "<none>"} ` +
`cachedCtxPlanMode=${args.ctx?.planMode ?? "<undefined>"} ` +
`liveMode=${liveMode ?? "<undefined>"} ` +
`latestPlanMode=${latestPlanMode ?? "<undefined>"} ` +
`hasGetLatestCallback=${args.ctx?.getLatestPlanMode ? "yes" : "no"}`,
);
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

This logs at info for every tool call in plan/executing (and also when the cached ctx indicates those modes), which can become very noisy and inflate gateway logs under normal plan-mode usage. Consider lowering to debug, and/or gating behind isPlanModeDebugEnabled() so operators only pay this cost when actively debugging.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Won't fix in this cycle — needs production gateway profiling to determine actual noise impact (the gate fires once per tool call, but tool-call rates vary widely by workload). info matches the level used by sibling createSubsystemLogger calls in the same file. Easy follow-up to demote to debug if profiling shows excessive volume. Tracking in umbrella #70101.

Comment on lines +177 to +182
const summary = !sessionStoreReadOk
? `WARNING: session-store read failed (${sessionStoreReadError ?? "unknown error"}); plan-mode state is UNKNOWN. The agent should treat this as a transient diagnostic failure, not a confirmed "normal" state.`
: inPlanMode
? `In plan mode (approval=${planMode?.approval ?? "none"}; title="${planMode?.title ?? "(unset)"}"; ${openSubagentRunIds.length} subagent(s) in flight; ${planMode?.lastPlanSteps?.length ?? 0} plan step(s) tracked).`
: inExecution
? `Executing approved plan (title="${planMode?.title ?? "(unset)"}"; approval=${planMode?.approval ?? "approved"}; ${planMode?.lastPlanSteps?.filter((s) => s.status === "in_progress").length ?? 0} step(s) in progress, ${planMode?.lastPlanSteps?.filter((s) => s.status === "pending").length ?? 0} pending, ${planMode?.lastPlanSteps?.filter((s) => s.status === "completed").length ?? 0} completed).`

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

In the inExecution branch, lastPlanSteps is filtered three separate times, making this O(3n) per call and allocating intermediate arrays. It would be more efficient (and easier to extend for cancelled/unknown) to compute status counts once (single pass) and reuse them both for summary text and details.

Suggested change
const summary = !sessionStoreReadOk
? `WARNING: session-store read failed (${sessionStoreReadError ?? "unknown error"}); plan-mode state is UNKNOWN. The agent should treat this as a transient diagnostic failure, not a confirmed "normal" state.`
: inPlanMode
? `In plan mode (approval=${planMode?.approval ?? "none"}; title="${planMode?.title ?? "(unset)"}"; ${openSubagentRunIds.length} subagent(s) in flight; ${planMode?.lastPlanSteps?.length ?? 0} plan step(s) tracked).`
: inExecution
? `Executing approved plan (title="${planMode?.title ?? "(unset)"}"; approval=${planMode?.approval ?? "approved"}; ${planMode?.lastPlanSteps?.filter((s) => s.status === "in_progress").length ?? 0} step(s) in progress, ${planMode?.lastPlanSteps?.filter((s) => s.status === "pending").length ?? 0} pending, ${planMode?.lastPlanSteps?.filter((s) => s.status === "completed").length ?? 0} completed).`
const lastPlanStepStatusCounts = (planMode?.lastPlanSteps ?? []).reduce(
(counts, step) => {
if (step.status === "in_progress") {
counts.inProgress += 1;
} else if (step.status === "pending") {
counts.pending += 1;
} else if (step.status === "completed") {
counts.completed += 1;
}
return counts;
},
{ inProgress: 0, pending: 0, completed: 0 },
);
const summary = !sessionStoreReadOk
? `WARNING: session-store read failed (${sessionStoreReadError ?? "unknown error"}); plan-mode state is UNKNOWN. The agent should treat this as a transient diagnostic failure, not a confirmed "normal" state.`
: inPlanMode
? `In plan mode (approval=${planMode?.approval ?? "none"}; title="${planMode?.title ?? "(unset)"}"; ${openSubagentRunIds.length} subagent(s) in flight; ${planMode?.lastPlanSteps?.length ?? 0} plan step(s) tracked).`
: inExecution
? `Executing approved plan (title="${planMode?.title ?? "(unset)"}"; approval=${planMode?.approval ?? "approved"}; ${lastPlanStepStatusCounts.inProgress} step(s) in progress, ${lastPlanStepStatusCounts.pending} pending, ${lastPlanStepStatusCounts.completed} completed).`

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Won't fix in this cycle — micro-perf nit on a list that's typically <30 items in practice (lastPlanSteps is bounded by the agent's plan size). The gain from one-pass categorization is below the noise floor. Acknowledged for maintainer follow-up if lastPlanSteps ever scales beyond a few dozen entries. Tracking in umbrella #70101.

Comment on lines +112 to +122
const { assertSafeCronSessionTargetId } = await import("../../cron/session-target.js");
try {
assertSafeCronSessionTargetId(params.sessionKey);
} catch (validationErr) {
params.log?.warn?.(
`plan-nudge schedule skipped: sessionKey "${params.sessionKey}" fails cron sessionTarget validation: ${
validationErr instanceof Error ? validationErr.message : String(validationErr)
}`,
);
continue;
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

This dynamically imports assertSafeCronSessionTargetId inside the per-interval loop, which repeats the await/import once per configured minutes entry. Importing once (outside the loop) or caching the imported function inside schedulePlanNudges would reduce overhead, especially when callers pass larger intervals arrays.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Won't fix in this cycle — the dynamic import isolates assertSafeCronSessionTargetId (a security check) for lazy loading; hoisting to module scope risks circular-import / initialization-order issues with the cron module. The repeated awaits per cron-interval entry (typically 3) are <1ms each on a warm import cache. Acknowledged for maintainer follow-up if perf trace shows it as a hotspot. Tracking in umbrella #70101.

Comment on lines +71 to +82
const safeTitle = (title ?? "").trim() || "Plan";
const escTitle = escapeHtml(safeTitle);
const safeSummary = (summary ?? "").trim();
const summaryLine = safeSummary ? `\n${escapeHtml(safeSummary)}` : "";
return [
`<b>${escTitle}</b> — plan submitted for approval. See attached.`,
summaryLine,
"",
"Resolve with: <code>/plan accept</code> | <code>/plan accept edits</code> | <code>/plan revise &lt;feedback&gt;</code>",
]
.filter(Boolean)
.join("\n");

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

summaryLine is built with a leading newline and then joined with "\n", which can produce unintended blank-line formatting (extra newlines) in the caption. Prefer adding the summary as its own array element without a leading newline so the final formatting is predictable and easier to reason about.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Won't fix in this cycle — needs visual inspection on actual Telegram render to determine if the extra newline is user-visible (Telegram captions have specific markdown rendering quirks where leading newlines collapse). Flagged for maintainer follow-up + a Telegram-specific UX review pass. Tracking in umbrella #70101.

Comment on lines +24 to +32
const EnterPlanModeToolSchema = Type.Object({
reason: Type.Optional(
Type.String({
description:
"Optional short justification shown alongside the mode-entered event " +
"(e.g. 'multi-file refactor — surface the plan first').",
}),
),
});

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

Other newly-added tools in this PR (e.g. plan_mode_status, ask_user_question) explicitly set { additionalProperties: false } to harden validation. Since enter_plan_mode doesn’t need arbitrary extra fields either, consider aligning by setting additionalProperties: false here as well.

Suggested change
const EnterPlanModeToolSchema = Type.Object({
reason: Type.Optional(
Type.String({
description:
"Optional short justification shown alongside the mode-entered event " +
"(e.g. 'multi-file refactor — surface the plan first').",
}),
),
});
const EnterPlanModeToolSchema = Type.Object(
{
reason: Type.Optional(
Type.String({
description:
"Optional short justification shown alongside the mode-entered event " +
"(e.g. 'multi-file refactor — surface the plan first').",
}),
),
},
{
additionalProperties: false,
},
);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d4ab39744c. Added { additionalProperties: false } to EnterPlanModeToolSchema to match the hardening pattern used by plan_mode_status and ask_user_question. LLMs that hallucinate unknown fields now get a schema validation error instead of silent drop.

Comment thread src/agents/tools/sessions-spawn-tool.ts Outdated
});
}
}
return jsonResult(addRoleToFailureResult(result, requestedAgentId));
return jsonResult(result);

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

This changes the tool’s returned details shape for forwarded failures by removing the previously-added role field (based on requested agentId) for error/forbidden outcomes. If any clients/channel renderers relied on that field for diagnostics or UX copy, this becomes a breaking behavior change. Consider either preserving role in failure details for backward compatibility, or updating the tool’s documented contract + downstream consumers to avoid depending on it.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Won't fix in this cycle — needs maintainer judgment on the documented details contract for forwarded failures. The change came from a rebase across upstream channels work; restoring role requires deciding whether channel renderers / diagnostics depend on it (no visibility into non-OpenClaw consumers). Flagged as a breaking-behavior-decision for maintainer review. Tracking in umbrella #70101 as a P1-followup item.

Comment thread src/agents/tools/sessions-spawn-tool.ts Outdated

return jsonResult(addRoleToFailureResult(result, requestedAgentId));
return jsonResult(result);

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

This changes the tool’s returned details shape for forwarded failures by removing the previously-added role field (based on requested agentId) for error/forbidden outcomes. If any clients/channel renderers relied on that field for diagnostics or UX copy, this becomes a breaking behavior change. Consider either preserving role in failure details for backward compatibility, or updating the tool’s documented contract + downstream consumers to avoid depending on it.

Suggested change
return jsonResult(result);
const resultWithCompatibleFailureDetails =
requestedAgentId &&
(result.status === "error" || result.status === "forbidden") &&
result.details &&
typeof result.details === "object" &&
!("role" in result.details)
? {
...result,
details: {
...result.details,
role: requestedAgentId,
},
}
: result;
return jsonResult(resultWithCompatibleFailureDetails);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same root cause + decision as the comment on line 371 (sister issue). Won't fix in this cycle for the same backward-compat reason. Tracking in umbrella #70101.

Comment on lines +68 to +75
export async function schedulePlanNudges(params: {
sessionKey: string;
agentId?: string;
planCycleId?: string;
intervals?: ReadonlyArray<number>;
deps?: PlanNudgeSchedulerDeps;
log?: { warn?: (msg: string) => void; info?: (msg: string) => void };
}): Promise<ScheduledPlanNudge[]> {

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

schedulePlanNudges/cleanupPlanNudges introduce important reliability behavior (partial-success scheduling, sessionKey validation, best-effort cleanup, jobId extraction). Given the injected deps (callGatewayTool, now) are explicitly designed for deterministic tests, adding unit tests for: invalid intervals filtering, validation skip path, jobId extraction shapes, partial failure tolerance, and cleanup failure accounting would help prevent regressions.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Won't fix in this cycle — adding test coverage for schedulePlanNudges / cleanupPlanNudges is substantive scope (~3-5 test cases each, 100-200 lines). Acknowledged as a real coverage gap; flagged for the test-coverage follow-up cycle (umbrella #70101 deferred-items checklist). The behaviors the bot calls out (partial-success scheduling, sessionKey validation, best-effort cleanup, jobId extraction) are correct and consistent with the rest of the cron infrastructure — coverage is a polish concern, not a correctness one.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cdb7ec1c70

ℹ️ 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".

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +242 to +249
? createNodeTestShards().map((shard) => ({
check_name: shard.checkName,
runtime: "node",
task: "test-shard",
shard_name: shard.shardName,
configs: shard.configs,
requires_dist: shard.requiresDist,
}))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Pass shard include patterns into node test shards

Restore includePatterns in this matrix payload (and the corresponding OPENCLAW_VITEST_INCLUDE_FILE wiring in the shard runner), otherwise split shards from createNodeTestShards() stop being real shards. In particular, the auto-reply split shards in scripts/lib/ci-node-test-plan.mjs depend on includePatterns to partition one Vitest config; dropping that field makes each shard run the full config, duplicating test execution across shards and causing major CI runtime/timeout regressions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d4ab39744c. Restored include_patterns: shard.includePatterns in the checks_node_core_test_matrix payload. The auto-reply split shards in scripts/lib/ci-node-test-plan.mjs:212 will now correctly partition the single Vitest config instead of each shard running the full config.

@100yenadmin

Copy link
Copy Markdown
Contributor Author

Acknowledging bot feedback (Greptile / Copilot / Codex). Triage status:

This PR remains the green-CI integration point for end-to-end testing of the rollout.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4ab39744c

ℹ️ 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".

Comment thread .github/workflows/ci.yml Outdated
node_bin="$(cygpath -u "$node_bin")"
fi
echo "NODE_BIN=$node_bin" >> "$GITHUB_ENV"
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Capture NODE_BIN without escaped inner quotes

The Capture node path command currently writes NODE_BIN=. because dirname receives a quoted literal (the escaped quotes become part of the argument), so the subsequent install step prepends the workspace directory to PATH instead of Node’s bin directory. In the Windows shard this can shadow node/pnpm with repo-local executables and makes the job non-deterministic; it also defeats the intended path pinning. Use the unescaped form (or the prior cygpath-aware variant) so NODE_BIN resolves to the actual Node install directory.

Useful? React with 👍 / 👎.

Comment thread src/agents/openclaw-tools.ts Outdated
@@ -311,6 +347,25 @@ export function createOpenClawTools(
sessionId: options?.sessionId,
onYield: options?.onYield,
}),
createSessionsSpawnTool({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Remove duplicate sessions_spawn tool registration

This adds a second sessions_spawn entry to the same tool list, so non-embedded runs now expose duplicate tool names. Because one copy is configured without runId and the other includes it, whichever definition is resolved first/last changes whether plan-mode subagent tracking hooks run; even when execution happens to work, duplicate tool names increase provider/tool-routing ambiguity and can cause flaky behavior across runtimes.

Useful? React with 👍 / 👎.

@100yenadmin

Copy link
Copy Markdown
Contributor Author

Smoke-test attempt on FULL (#70071) — blocked + real findings extracted (2026-04-22 ~17:40 GMT+7)

Tried to spin up the gateway on the FULL branch (restack/68939-pr10-executing-followup, tip d4ab39744c) to run the plan-mode end-to-end smoke cycle (/plan on → propose → approve → execute → cron-nudge → exit). Gateway failed to boot due to a pre-existing dep-resolution issue. But the attempt also surfaced 8 missing-export warnings on FULL that are real bugs (not pre-existing on upstream/main).

Both need to land on the maintainer's radar before #70071 is claimed as a green-CI integration bundle.

🟡 Smoke-test blocker #1 — opusscript peer-dep mismatch

[openclaw] Failed to write runtime build artifacts: failed to stage bundled
runtime deps for discord: runtime dependency closure must resolve from the
installed root workspace graph. Could not materialize: @buape/carbon,
@discordjs/voice, @sinclair/typebox, discord-api-types, https-proxy-agent,
opusscript, undici, ws. Run `pnpm install` and rebuild from a trusted workspace
checkout, or provide a hardened fallback installer.
Cause: runtime dependency opusscript must resolve to an exact installed
version, got: ^0.0.8

Peer-dep warning during pnpm install:

└─┬ @buape/carbon 0.16.0
  └─┬ @discordjs/voice 0.19.2
    └─┬ prism-media 1.3.5
      └── ✕ unmet peer opusscript@^0.0.8: found 0.1.1

pnpm install completes but the discord runtime-bundle staging fails because the installed opusscript (0.1.1) doesn't match the ^0.0.8 constraint. OPENCLAW_SKIP_CHANNELS=1 does not bypass this step.

Severity: blocks pnpm gateway:dev on this branch. Likely affects CI too.
Likely not introduced by this rollout (opusscript is a transitive discord dep), but needs pinning or lock-file update before a clean gateway boot is possible.

🔴 Smoke-test blocker #2 — 8 missing-export warnings on FULL

Eight symbols are IMPORTED in FULL's code but NOT DEFINED anywhere in the source tree. git grep confirms zero matches on both restack/68939-pr10-executing-followup and upstream/main:

Symbol Imported at Expected export from
SUBAGENT_SETTLE_GRACE_MS src/agents/tools/exit-plan-mode-tool.ts:4 + src/gateway/sessions-patch.ts:13 src/agents/plan-mode/index.ts
MAX_CONCURRENT_SUBAGENTS_IN_PLAN_MODE src/agents/tools/sessions-spawn-tool.ts:7 src/agents/plan-mode/index.ts
resolveAgentAutoContinue src/agents/pi-embedded-runner/run.ts:17 src/agents/agent-scope.ts
resolveAgentMaxIterations src/agents/pi-embedded-runner/run.ts:19 src/agents/agent-scope.ts
resolveDeletedAgentIdFromSessionKey src/gateway/server-methods/chat.ts:77 + src/gateway/sessions-resolve.ts:15 src/gateway/session-utils.ts
buildAcceptEditsPlanInjection src/gateway/sessions-patch.ts:10 src/agents/plan-mode/index.ts
appendToInjectionQueue src/gateway/sessions-patch.ts:15 src/agents/plan-mode/injections.ts
validateChannelsStartParams src/gateway/server-methods/channels.ts:24 src/gateway/protocol/index.ts

Severity: these are dangling imports. The bundler (rollup/vite) treats them as warnings rather than errors so the build proceeds, but at runtime these imports will be undefined. Code paths that call them will crash with undefined is not a function / Cannot read properties of undefined.

Origin: these look like cherry-pick / refactoring drift. The CALLERS were brought across but the IMPLEMENTATIONS were either renamed, moved, or dropped without updating the import sites.

Why Greptile/Copilot/Codex didn't catch this: bots review the DIFF (line-level). A missing export is a graph-level problem — the bot would need to resolve the import graph to notice. Smoke-test build catches it immediately via bundler warnings.

Impact on claim "FULL is green-CI": if the CI pipeline runs tsgo (strict), it might catch these. If it only runs the bundler, it will pass with warnings. Either way, these will crash at runtime when the relevant code paths execute (e.g. exit_plan_mode tool call, sessions-spawn, sessions-patch, etc.).

Recommended actions for maintainer

Before merging FULL (or the per-part PRs that cascade to FULL):

  1. Fix the 8 dangling imports. For each symbol, either:
    • Add the missing export to the claimed location (if the symbol exists under a different path), OR
    • Remove the import (if it's dead code left over from refactoring), OR
    • Delete the call sites (if functionality was intentionally dropped)
  2. Resolve opusscript peer-dep. Pin opusscript to a version that satisfies ^0.0.8 in package.json, or accept the 0.1.1 installation and patch the peer constraint. pnpm patch @discordjs/voice is one option; regenerating pnpm-lock.yaml with an updated opusscript is cleaner.
  3. Re-run gateway smoke after both fixes; the plan-mode end-to-end cycle can then be validated.

What the smoke test verified about the ROLLOUT structure (positive findings)

Despite not completing:

  • pnpm install completed successfully (dep tree resolves, just with the peer-warning)
  • node scripts/run-node.mjs --dev gateway kicked off (script is reachable, config is parseable)
  • The build reached the discord runtime-dep staging step (= core bundling succeeded)
  • The 8 MISSING_EXPORT items are surfaced as WARNINGS, not build-breaking errors (bundler is lenient)

The rollout's structural integrity (per-part PRs + INJECTIONS + AUTOMATION + FULL) is not what's broken here. What's broken is a mix of cherry-pick drift (the 8 imports) + a vendor dep version drift (opusscript).

Files the maintainer can start with

To fix the 8 dangling imports quickly, the minimum change is to either add the exports at the expected locations OR remove the imports. Best starting point: git show the commits that introduced the CALL SITES to understand what the symbols were supposed to do:

git log --oneline -- src/agents/tools/exit-plan-mode-tool.ts
git log --oneline -- src/gateway/sessions-patch.ts
git log --oneline -- src/agents/pi-embedded-runner/run.ts
git log --oneline -- src/gateway/session-utils.ts

Attempted smoke test, gateway failed to boot. Findings above are real pre-merge blockers. Not sign-offable as "green CI bundle" in its current state — but fixable.

@100yenadmin
100yenadmin force-pushed the restack/68939-pr10-executing-followup branch from d4ab397 to 5774aa0 Compare April 22, 2026 11:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5774aa039d

ℹ️ 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".

Comment on lines +414 to +416
planModeCfg?.enabled === true &&
cronSession.sessionEntry.planMode === undefined &&
planModeCfg.autoEnableFor &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Keep plan opt-out state from being auto-reenabled

This auto-enable guard keys only on sessionEntry.planMode === undefined, but the same commit clears planMode on normal transitions in src/gateway/sessions-patch.ts (for /plan off and many approve/edit closes). In cron sessions using agents.defaults.planMode.autoEnableFor, an explicit opt-out or completed cycle is therefore treated as "never toggled," so the next run silently flips back to mode: "plan" and re-locks mutation tools against user intent.

Useful? React with 👍 / 👎.

Comment on lines +705 to +706
id: `plan-complete-${params.sessionKey}-${Date.now()}`,
kind: "plan_complete",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Deduplicate plan-complete injections with a stable ID

appendToInjectionQueue deduplicates by id, but this ID includes Date.now(), so each repeated completion event creates a new queue entry instead of upserting. Since update_plan emits phase: "completed" whenever all steps are terminal, retries or repeated terminal updates can enqueue duplicate [PLAN_COMPLETE] injections and repeatedly re-trigger completion behavior.

Useful? React with 👍 / 👎.

Eva added 2 commits April 22, 2026 21:16
Resolved 23 conflicts:
- Take-upstream for 14 files (12 i18n .meta.json + ci.yml + CHANGELOG)
- Modify/delete chat.test.ts (upstream deleted; we accepted deletion)
- Manual merge of 7 plan-mode-relevant files:
  - openclaw-tools.ts: take-both imports + take-upstream (drop unconditional sessions_spawn duplicate)
  - sessions-spawn-tool.ts: take-both imports + use upstream's renamed delivery-context.shared.js path
  - cron/isolated-agent/run.ts: combine HEAD's plan-mode autoEnableFor + planCycleId guards with upstream's deliveryPlan return field + deliveryContract param
  - server.impl.ts: take-upstream's createCloseHandler() refactor
  - server-close.test.ts (2 regions): take-upstream's createGatewayCloseTestDeps() helper
  - chat.ts (3 regions): take-HEAD imports + take-HEAD plan-mode helpers + take-upstream renderChatRunControls refactor (loses STT recording UI \u2014 follow-up to re-add)
  - extensions/openai/prompt-overlay.ts (3 regions): take-both for execution_policy + completion_contract; take-HEAD for extensive EXECUTION_BIAS + TOOL_CALL_STYLE exports + their references in resolveOpenAiOpenClawOverlay
  - extensions/openai/index.test.ts: take-HEAD (test file; runtime will catch any breakages)
…mode dev

Phase 2 follow-ups after the upstream/main merge (commit b1ead74):
- prompt-overlay.ts: restore missing return statement in returnObj
- send.runtime.ts: re-export loadConfig (still referenced by send.ts)
- send.ts: import loadConfig + default opts.cfg in sendDocumentTelegram so
  TelegramDocumentOpts.cfg can stay optional (matches dev-branch API while
  satisfying upstream's stricter resolveTelegramApiContext signature)
- vitest.plan-mode.config.ts: drop chat.test.ts (deleted upstream by
  92191d3, coverage moved to run-controls/tool-expansion-state/grouped-render)
- a2ui bundle.hash: regenerated by bundle script

Build: pnpm build green
Tests: pnpm vitest --config vitest.plan-mode.config.ts → 219/219 passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

sessionFile: params.sessionFile,
workspaceDir: resolvedWorkspace,
agentDir,

P2 Badge Preserve isCanonicalWorkspace in attempt invocation

This invocation now drops isCanonicalWorkspace, and resolveAttemptBootstrapRouting treats a missing value as true before deciding full vs limited bootstrap mode. For runs using workspace overrides (non-canonical workspace without sandbox copy), that changes behavior from limited to full bootstrap instructions, which can push the agent to execute full BOOTSTRAP workflow guidance in the wrong workspace context.

ℹ️ 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".

// before-tool-call hook arms the mutation gate. Without this
// the field added to attempt's params + the threading through
// pi-tools is dead code (Codex P1 #67840 r3096735975).
...(params.planMode ? { planMode: params.planMode } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Forward live plan-mode accessors into attempt params

This attempt call only passes the static planMode snapshot and drops the new getLatestPlanMode/getLatestAcceptEdits accessors that callers provide (for example from agent-runner-execution). run/attempt.ts forwards those accessors into pi-tools.before-tool-call, where they are required to switch gating immediately after approve/edit and to apply post-approval acceptEdits constraints; without them, gating falls back to stale run-start state, so the same run can keep mutation tools blocked (or miss acceptEdits checks) until another run is started.

Useful? React with 👍 / 👎.

Comment on lines 758 to 761
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
memberRoleIds: params.memberRoleIds,
spawnedBy: params.spawnedBy,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Pass memberRoleIds through runEmbeddedAttemptWithBackend

memberRoleIds is no longer forwarded into the attempt params here, but run/attempt.ts passes params.memberRoleIds into createOpenClawCodingTools, and downstream route/policy resolution uses those role ids for role-scoped bindings. With this omission, role-qualified channel flows are evaluated as if the requester has no roles, which can misroute or deny tool access for valid role-bound operators.

Useful? React with 👍 / 👎.

@prtags

prtags Bot commented Apr 23, 2026

Copy link
Copy Markdown

Related work from PRtags group ace-bullfrog-hw3e

Title: Open PR candidate: plan-mode carve-out overlaps integrated full bundle

Number Title
#70031 [Plan Mode 1/6] Plan-state foundation
#70066 [Plan Mode 2/6] Core backend MVP
#70067 [Plan Mode 3/6] Advanced plan interactions
#70068 [Plan Mode 4/6] Web UI + i18n
#70069 [Plan Mode 5/6] Text channels + Telegram
#70070 [Plan Mode 6/6] Docs, QA, and help
#70071* [Plan Mode FULL] Integrated bundle for testing (Parts 1\u20136 + automation + executing-state lifecycle)
#70088 [Plan Mode INJECTIONS] Typed pending-injection queue foundation
#70089 [Plan Mode AUTOMATION] Cron nudges + auto-enable + subagent follow-ups

* This PR

@100yenadmin 100yenadmin reopened this Apr 25, 2026
100yenadmin pushed a commit to 100yenadmin/openclaw that referenced this pull request May 12, 2026
… placement)

The mode-switcher chip in the in-host PR openclaw#70071 design renders in the
input toolbar row (next to file-attach + microphone), NOT in a chat
header. The previous surface name 'chat-header-chip' suggested 'top of
chat' to readers, which is wrong.

Reviewed against the in-host code:
- ui/src/ui/views/chat.ts:1513 mounts renderModeSwitcher inside the
  agent-chat__input-btn toolbar row
- ui/src/ui/chat/mode-switcher.ts docstring: 'Mode switcher for the
  chat input toolbar'

Renames in this commit:
- src/plugins/registry.ts: chatStreamSurfaces Set entries
- src/plugins/host-hooks.ts: type union literal + docstring
- src/gateway/protocol/schema/plugins.ts: schema literal
- src/plugins/contracts/host-hooks.contract.test.ts: 2 occurrences
- src/gateway/server-methods/plugin-host-hooks.test.ts: 1 occurrence
- docs/plugins/hooks.md: 1 occurrence

Build passes (pnpm build = 1m07s, no lint regressions from this rename).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: macos App: macos app: web-ui App: web-ui channel: telegram Channel integration: telegram commands Command implementations docs Improvements or additions to documentation extensions: openai gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants