markdown.engineering
Lesson 47

ULTRAPLAN: 30-Minute Remote Planning

How Claude Code offloads deep multi-agent planning to a remote CCR session — from keyword trigger to plan approval and teleport-back-to-terminal.

01 Overview

ULTRAPLAN is Claude Code's remote planning mode. When you type /ultraplan or embed the word ultraplan anywhere in a prompt, the CLI spawns a full Claude Code session in the cloud (CCR — Claude Code on the web), running on the most powerful available model (Opus), with a 30-minute window to iterate a plan with you via the browser. Your local terminal stays free the entire time.

Source files covered
commands/ultraplan.tsxutils/ultraplan/ccrSession.tsutils/ultraplan/keyword.tsutils/teleport.tsxtasks/RemoteAgentTask/RemoteAgentTask.tsx

At the highest level ULTRAPLAN has four phases, each managed by different modules:

Phase 1

Trigger Detection

Keyword scanner (keyword.ts) finds "ultraplan" in freeform input or the slash command routes it directly.

Phase 2

CCR Session Launch

teleportToRemote() in teleport.tsx creates the remote session, uploads a git bundle, and returns a session ID.

Phase 3

Long-Poll

pollForApprovedExitPlanMode() in ccrSession.ts polls the event stream every 3 s for up to 30 min, tracking phase transitions.

Phase 4

Plan Delivery

On approval the plan lands locally via UltraplanChoiceDialog (teleport path) or stays in CCR (remote-execute path).

02 End-to-End Flow

The diagram traces the exact code path from user keystroke to plan delivery:

flowchart TD A["User types prompt\ncontaining 'ultraplan'\nor /ultraplan <prompt>"] --> KW["keyword.ts\nfindUltraplanTriggerPositions()\nfilters quoted/path contexts"] KW --> LP["launchUltraplan()\nultraplan.tsx"] LP --> GUARD{"ultraplanSessionUrl\nor ultraplanLaunching\nalready set?"} GUARD -->|"Yes"| DUPE["Return 'already polling'\nmessage"] GUARD -->|"No"| SET["setAppState: ultraplanLaunching=true\n(prevents duplicate launch\nduring async window)"] SET --> DETACH["launchDetached() — detached void\nReturns launch message immediately\nso terminal stays responsive"] DETACH --> ELIG["checkRemoteAgentEligibility()\nchecks: OAuth login, git repo,\ngit remote, GitHub app, policy"] ELIG -->|"ineligible"| ERR["enqueuePendingNotification\nwith reason string"] ELIG -->|"eligible"| PROMPT["buildUltraplanPrompt(blurb, seedPlan)\nwraps instructions + blurb\nin system-reminder tag"] PROMPT --> TELE["teleportToRemote({\n permissionMode: 'plan',\n ultraplan: true,\n model: getUltraplanModel()\n})\nteleport.tsx"] TELE --> BUNDLE["generateTitleAndBranch() via Haiku\ncreateAndUploadGitBundle()\nPOST /v1/sessions"] BUNDLE --> SESSION["session.id returned\nURL = getRemoteSessionUrl(id)"] SESSION --> STATEURL["setAppState: ultraplanSessionUrl=url\nultraplanLaunching=undefined"] STATEURL --> REGIST["registerRemoteAgentTask(\n remoteTaskType: 'ultraplan',\n isUltraplan: true\n)\n→ pill shown in REPL"] REGIST --> POLL["startDetachedPoll(taskId, sessionId, url)"] POLL --> LOOP["pollForApprovedExitPlanMode()\nccrSession.ts\n3s interval, 30min timeout\nMAX_CONSECUTIVE_FAILURES=5"] LOOP --> SCAN["ExitPlanModeScanner.ingest(newEvents)\nclassifies: approved / teleport /\nrejected / pending / terminated"] SCAN -->|"pending"| PHASE["onPhaseChange('plan_ready')\nupdateTaskState: ultraplanPhase"] SCAN -->|"needs_input (quiet idle)"| NI["onPhaseChange('needs_input')\nUser must reply in browser"] PHASE --> SCAN NI --> SCAN SCAN -->|"terminated"| FAIL["UltraplanPollError(reason='terminated')\narchiveRemoteSession()\nclear ultraplanSessionUrl"] SCAN -->|"approved (in-CCR execute)"| REMOTE["executionTarget='remote'\ntask→completed\nenqueuePendingNotification\n'executing in CCR'"] SCAN -->|"teleport (local execute)"| LOCAL["executionTarget='local'\nsetAppState: ultraplanPendingChoice\n{plan, sessionId, taskId}"] LOCAL --> DIALOG["UltraplanChoiceDialog mounts\nUser chooses: execute here or open in browser"] DIALOG --> ARCHIVE["archiveRemoteSession(sessionId)\nclear ultraplanSessionUrl + pendingChoice"] style A fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style REMOTE fill:#1f241d,stroke:#6e9468,color:#b8b0a4 style LOCAL fill:#1f241d,stroke:#6e9468,color:#b8b0a4 style FAIL fill:#241b19,stroke:#c47a50,color:#b8b0a4 style POLL fill:#221f28,stroke:#8e82ad,color:#b8b0a4
03 Keyword Trigger: Smart Disambiguation

Most Claude features require an explicit /command. ULTRAPLAN is unusual: it fires from freeform text. The word ultraplan anywhere in your prompt triggers the launch — unless context makes clear it is not a directive.

The logic lives entirely in utils/ultraplan/keyword.ts in the findKeywordTriggerPositions() function. It builds a list of "quoted ranges" by walking the input character-by-character, then filters word-boundary matches against those ranges plus several additional guards:

// keyword.ts — what is filtered OUT (never triggers)

// 1. Slash-command input — /ultraplan is routed to the command handler, not here
if (text.startsWith('/')) return []

// 2. Inside paired delimiters: `backticks`, "quotes", <tags>, {braces}, [brackets], (parens)
// e.g.  `src/ultraplan/foo.ts`  or  <ultraplan> in HTML  do NOT trigger

// 3. Path / identifier context: preceded or followed by  /  \  -
// e.g.  src/ultraplan/  or  --ultraplan-mode  do NOT trigger

// 4. Followed by ? — a question about the feature shouldn't invoke it
// e.g.  "what is ultraplan?"  does NOT trigger

// 5. Followed by . + word char (file extension)
// e.g.  ultraplan.tsx  does NOT trigger

The same engine handles ultrareview. Both export a typed TriggerPosition[] so the PromptInput component can highlight the matched word with a rainbow effect and show a tooltip "will launch ultraplan" before the user even submits.

When the keyword is detected and the prompt forwarded to the remote session, the local prompt is rewritten so the word "ultraplan" becomes "plan" — keeping the forwarded prompt grammatical:

// keyword.ts
export function replaceUltraplanKeyword(text: string): string {
  const [trigger] = findUltraplanTriggerPositions(text)
  if (!trigger) return text
  const before = text.slice(0, trigger.start)
  const after  = text.slice(trigger.end)
  if (!(before + after).trim()) return ''
  // Preserves user casing: "ultraplan" → "plan", "Ultraplan" → "Plan"
  return before + trigger.word.slice('ultra'.length) + after
}
04 Launch Sequence Deep Dive

The "detached" pattern

launchUltraplan() returns a user-facing message immediately, before the remote session exists. All the async work — eligibility check, Haiku title generation, git bundle creation, POST to CCR — runs in a void async closure called launchDetached(). The caller never awaits it.

// ultraplan.tsx — returns to the REPL in milliseconds
export async function launchUltraplan(opts): Promise<string> {
  // Synchronously set ultraplanLaunching to block duplicate launches
  setAppState(prev => prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true })

  void launchDetached({ blurb, seedPlan, getAppState, setAppState, signal, onSessionReady })

  return buildLaunchMessage(disconnectedBridge)
  // ↑ "◇ ultraplan\nStarting Claude Code on the web…"
}
Design insight
The ultraplanLaunching flag is set synchronously before the detached flow even starts. This closes the window where two rapid keypresses could both pass the guard check before either has called teleportToRemote(). The flag is cleared as soon as the session URL arrives (or on any error).

Prompt construction and the system-reminder trick

The initial CCR message has a deliberate layering. The blurb and seed plan go outside the <system-reminder> tag so the CCR browser renders them visibly to the user. The scaffolding instructions (loaded from utils/ultraplan/prompt.txt at build time) go inside the tag, so the remote model sees them but the browser UI hides them. And the word "ultraplan" is deliberately absent from the prompt text to avoid the remote CCR CLI self-triggering another session:

// ultraplan.tsx
export function buildUltraplanPrompt(blurb: string, seedPlan?: string): string {
  const parts: string[] = []
  if (seedPlan) {
    parts.push('Here is a draft plan to refine:', '', seedPlan, '')
  }
  parts.push(ULTRAPLAN_INSTRUCTIONS) // from prompt.txt, wrapped in <system-reminder>
  if (blurb) {
    parts.push('', blurb)
  }
  return parts.join('\n')
}

Eligibility checks

Before spending time on network calls, checkRemoteAgentEligibility() validates several preconditions. A failure surfaces as a notification (not a thrown error) so the terminal doesn't crash:

// RemoteAgentTask.tsx — formatted error messages
case 'not_logged_in':
  return 'Please run /login and sign in with your Claude.ai account (not Console).'
case 'no_remote_environment':
  return 'No cloud environment available. Set one up at https://claude.ai/code/onboarding?magic=env-setup'
case 'not_in_git_repo':
  return 'Background tasks require a git repository.'
case 'no_git_remote':
  return 'Background tasks require a GitHub remote.'
case 'github_app_not_installed':
  return 'The Claude GitHub app must be installed on this repository first.'
case 'policy_blocked':
  return "Remote sessions are disabled by your organization's policy."

teleportToRemote() — the CCR session factory

The heavy lifting of creating the remote session is done by teleportToRemote() in utils/teleport.tsx. For ULTRAPLAN specifically, it is called with permissionMode: 'plan' and ultraplan: true. This causes the CCR session to be created in plan mode — the remote agent can only plan, not execute. The session also gets the local git repo cloned in via a git bundle:

// ultraplan.tsx — call site
const session = await teleportToRemote({
  initialMessage: prompt,
  description:   blurb || 'Refine local plan',
  model:         getUltraplanModel(),    // opus4.6 from GrowthBook flag
  permissionMode: 'plan',
  ultraplan:      true,
  signal,
  useDefaultEnvironment: true,
  onBundleFail:  msg => { bundleFailMsg = msg }
})

Inside teleportToRemote(), Claude Haiku generates a short session title and claude/<slug> branch name from the description. Then it POSTs to /v1/sessions with OAuth headers, the git source, and the initial message. The returned session.id is the anchor for everything that follows.

05 The Polling Engine

Once the session is live, startDetachedPoll() calls pollForApprovedExitPlanMode() — a 30-minute cursor-based event poll. The function lives in utils/ultraplan/ccrSession.ts and is deliberately free of side effects: it is a pure async loop that delegates all classification to ExitPlanModeScanner.

Cursor-based pagination

Every tick calls pollRemoteSessionEvents(sessionId, cursor) which hits GET /v1/sessions/{id}/events?after_id={cursor}, fetching up to 50 pages of events in a single call. The cursor advances to response.lastEventId. This means the poller never re-reads events — each tick only fetches new activity.

// ccrSession.ts
const POLL_INTERVAL_MS = 3000
const MAX_CONSECUTIVE_FAILURES = 5  // ~600 calls over 30min; tolerate transient 5xx

while (Date.now() < deadline) {
  if (shouldStop?.()) throw new UltraplanPollError('poll stopped by caller', 'stopped', ...)

  const resp = await pollRemoteSessionEvents(sessionId, cursor)
  cursor = resp.lastEventId
  const result = scanner.ingest(resp.newEvents)
  // ... classify result and update phase ...
  await sleep(POLL_INTERVAL_MS)
}

ExitPlanModeScanner — pure stateful classifier

The scanner accumulates event batches and classifies the session's ExitPlanMode state. It tracks three internal collections: exitPlanCalls (tool_use IDs for ExitPlanMode), results (tool_result blocks keyed by ID), and rejectedIds (IDs the user rejected in the browser). On each ingest it scans from newest to oldest to find the first non-rejected call and its resolution:

// ccrSession.ts — ScanResult kinds
// 'approved'   → plan in tool_result with is_error=false, marker "## Approved Plan:"
// 'teleport'   → is_error=true but contains ULTRAPLAN_TELEPORT_SENTINEL marker
// 'rejected'   → is_error=true, no sentinel — user said "revise this"
// 'pending'    → tool_use seen, no tool_result yet (browser showing approval dialog)
// 'terminated' → result(non-success) — remote session crashed or hit max turns
// 'unchanged'  → no new relevant events
Edge case: batch ordering
A single ingest can span seconds of session activity (up to 50 pages of events). The scanner is designed so that if a batch contains both an approval AND a subsequent crash event (type:'result', subtype:'error_during_execution'), the approved plan is returned — the code comment explicitly documents this precedence: "approved > terminated > rejected > pending > unchanged".

Phase transitions and the pill badge

The poller surfaces three phases to the UI via onPhaseChange():

Phase

running

Default. Remote is executing turns. No special badge.

Phase

needs_input

Remote asked a clarifying question and is idle. Badge: "needs input". User must reply in browser.

Phase

plan_ready

ExitPlanMode tool_use exists with no tool_result. Badge: "plan ready". Browser is showing the approval dialog.

The transition to needs_input uses a careful heuristic: sessionStatus === 'idle' AND newEvents.length === 0. The second condition is crucial — CCR flips to "idle" briefly between tool turns, so the poller only trusts it when there is no activity on the same tick:

// ccrSession.ts — quiet-idle heuristic
const quietIdle =
  (sessionStatus === 'idle' || sessionStatus === 'requires_action') &&
  newEvents.length === 0

const phase: UltraplanPhase = scanner.hasPendingPlan
  ? 'plan_ready'
  : quietIdle
    ? 'needs_input'
    : 'running'
06 Plan Delivery: Two Paths

When the poll resolves, the executionTarget field determines where the plan runs. There are exactly two outcomes:

Path A: Remote execution ("approved" in CCR)

The user clicked "Execute" inside the CCR browser PlanModal. The remote session is already in coding mode. The local CLI must NOT archive the session (archiving stops it), and must NOT show a choice dialog. It simply marks the task completed and enqueues a notification:

// ultraplan.tsx — remote execution path
if (executionTarget === 'remote') {
  updateTaskState(taskId, setAppState, t => ({
    ...t, status: 'completed', endTime: Date.now()
  }))
  enqueuePendingNotification({
    value: [
      `Ultraplan approved — executing in Claude Code on the web. Follow along at: ${url}`,
      '',
      'Results will land as a pull request when the remote session finishes.',
      'There is nothing to do here.'
    ].join('\n'),
    mode: 'task-notification'
  })
}

Path B: Teleport ("teleport back to terminal")

The user clicked "Teleport back to terminal" in the PlanModal browser. The browser sends an is_error=true tool_result with the sentinel string __ULTRAPLAN_TELEPORT_LOCAL__ as a prefix, followed by the plan text. The scanner detects this and returns { kind: 'teleport', plan }. The poll resolves with executionTarget: 'local'.

The local CLI then sets ultraplanPendingChoice in AppState, which causes the REPL to mount the UltraplanChoiceDialog. The dialog owns archiving the remote session and clearing state on user choice:

// ccrSession.ts — teleport plan extraction
export const ULTRAPLAN_TELEPORT_SENTINEL = '__ULTRAPLAN_TELEPORT_LOCAL__'

function extractTeleportPlan(content): string | null {
  const text = contentToText(content)
  const marker = `${ULTRAPLAN_TELEPORT_SENTINEL}\n`
  const idx = text.indexOf(marker)
  if (idx === -1) return null         // no sentinel → normal rejection
  return text.slice(idx + marker.length).trimEnd()
}

// approved path uses a different extractor
function extractApprovedPlan(content): string {
  // Checks "## Approved Plan (edited by user):\n" first,
  // then "## Approved Plan:\n"
}
07 Stopping and Cleanup

Stopping ULTRAPLAN is coordinated: the CLI archives the remote session (which halts it but keeps the URL viewable), kills the local task entry, and clears all related AppState fields. The detached poll's shouldStop callback detects the killed status on its next tick and throws a UltraplanPollError with reason 'stopped' — which the poll's catch block handles by early-returning (no extra notification).

// ultraplan.tsx
export async function stopUltraplan(taskId, sessionId, setAppState): Promise<void> {
  await RemoteAgentTask.kill(taskId, setAppState) // archives session internally
  setAppState(prev => ({
    ...prev,
    ultraplanSessionUrl:     undefined,
    ultraplanPendingChoice:  undefined,
    ultraplanLaunching:      undefined
  }))
  // Enqueue two notifications: one for the user, one meta-instruction
  // for the model so it doesn't try to respond to the stop event
  enqueuePendingNotification({ value: `Ultraplan stopped.\n\nSession: ${url}`, ... })
  enqueuePendingNotification({
    value: 'The user stopped the ultraplan session above. Do not respond...',
    mode: 'task-notification',
    isMeta: true  // ← model-only instruction, not shown to user
  })
}
Orphan prevention
If an error occurs after teleportToRemote() succeeds but before the poll loop is healthy, the catch block archives the session explicitly. Without this, the remote container would sit running for 30 minutes with no poller watching it. The local sessionId variable is hoisted above the try block precisely so the catch can reference it.
08 RemoteAgentTask Integration

ULTRAPLAN sessions are registered as RemoteAgentTaskState entries in the unified task framework. This is what drives the pill in the REPL status bar. The isUltraplan: true flag distinguishes them from regular remote-agent tasks so the generic poller (startRemoteSessionPolling) knows not to declare completion on its own — ULTRAPLAN lifecycle is owned by startDetachedPoll:

// RemoteAgentTask.tsx — state shape (relevant fields)
type RemoteAgentTaskState = TaskStateBase & {
  type:             'remote_agent'
  remoteTaskType:   RemoteTaskType   // 'ultraplan' | 'ultrareview' | 'remote-agent' | ...
  sessionId:        string
  isUltraplan?:     boolean
  // Scanner-derived pill badge state
  ultraplanPhase?:  Exclude<UltraplanPhase, 'running'>  // 'needs_input' | 'plan_ready'
  log:              SDKMessage[]   // populated by the generic poller for the detail view
  todoList:         TodoList
}

The session is also persisted to a sidecar file on disk via writeRemoteAgentMetadata(). This means if you close the terminal and reopen it with claude --resume, the ULTRAPLAN session is restored: the sidecar is read, CCR status is fetched, and polling restarts if the session is still running.

09 Model Selection and Feature Flags

The remote model is not hardcoded. It is read at call time from a GrowthBook feature flag, falling back to the Opus 4.6 first-party ID:

// ultraplan.tsx
function getUltraplanModel(): string {
  return getFeatureValue_CACHED_MAY_BE_STALE(
    'tengu_ultraplan_model',
    ALL_MODEL_CONFIGS.opus46.firstParty   // fallback
  )
}

Note the comment in the source: "Read at call time, not module load: the GrowthBook cache is empty at import and /config Gates can flip it between invocations." This is why the function is not a module-level constant.

There is also a dev-only prompt override: when built as an ant (internal) build and the ULTRAPLAN_PROMPT_FILE env var points to a file, that file replaces the bundled prompt.txt. This path is dead-code-eliminated from external builds at compile time.

10 Phase State Machine

The three-phase state machine for a running ULTRAPLAN session is documented in the source comments and is worth visualising explicitly:

stateDiagram-v2 [*] --> running : Session created\npolling starts running --> needs_input : quietIdle\n(sessionStatus=idle AND\nnewEvents=0) needs_input --> running : User replies\nin browser\n(new events arrive) running --> plan_ready : ExitPlanMode tool_use\nseen, no tool_result yet plan_ready --> running : User rejects plan\n(is_error=true, no sentinel) plan_ready --> approved : User approves\n(is_error=false)\nexecutionTarget=remote plan_ready --> teleport : User clicks\n"back to terminal"\n(sentinel found)\nexecutionTarget=local running --> terminated : result(non-success)\nsubtype=error_during_execution\nor error_max_turns running --> timeout : 30 minutes elapsed\nno approval seen approved --> [*] teleport --> [*] terminated --> [*] timeout --> [*]
Deep dive: ExitPlanModeScanner ingest() logic step by step

The scanner processes event batches in two passes. First pass: it walks every event in the batch and updates three internal data structures:

  • For type:'assistant' messages — any tool_use block with name exit_plan_mode_v2 has its ID pushed to exitPlanCalls[]
  • For type:'user' messages — tool_result blocks are stored in results map keyed by tool_use_id
  • For type:'result' with a non-success subtype — sets terminated flag with the subtype string

Second pass (scan): iterates exitPlanCalls from newest to oldest, skipping rejected IDs. For each candidate:

  • No tool_result yet → { kind: 'pending' }
  • tool_result with is_error=true + sentinel → { kind: 'teleport' }
  • tool_result with is_error=true, no sentinel → { kind: 'rejected' }
  • tool_result with is_error=false{ kind: 'approved' }

Precedence resolution: approved/teleport are returned immediately (no terminated check). Rejected IDs are added to the set and a rescan is scheduled for the next tick. Terminated takes precedence over rejected and pending but not over approved/teleport.

The rescanAfterRejection flag is a performance optimisation: when nothing happened (no new events, no rejection last tick) the scan is skipped entirely — the result cannot have changed.

11 Key Takeaways

What this lesson covered

  • ULTRAPLAN offloads multi-agent planning to a remote CCR session (plan-mode only, Opus model, 30-min window) while keeping the local terminal fully free.
  • The keyword scanner in keyword.ts filters quoted contexts, file paths, slash commands, and question marks before firing the trigger — freeform text just works.
  • The "detached" pattern in launchUltraplan() returns a message immediately and runs all async work in a void closure — the terminal never blocks.
  • The ultraplanLaunching flag is set synchronously before any async call, closing the double-launch race window.
  • The poll loop uses cursor-based pagination (up to 50 pages per tick), tolerates up to 5 consecutive network failures, and drives a three-state phase machine (running → needs_input → plan_ready).
  • The ExitPlanModeScanner is a side-effect-free stateful classifier: it can be replayed offline from recorded event logs for debugging.
  • Two delivery paths: remote (user approves in browser, CCR executes, result lands as a PR) and teleport (user clicks "back to terminal", plan is embedded in the rejection tool_result via a sentinel string).
  • Stopping clears three AppState fields, archives the remote session, and sends a isMeta:true model-only notification so the local model doesn't try to respond to the stop event.
  • Sessions survive --resume: metadata is persisted to a sidecar file and restored on restart if the CCR session is still live.

Knowledge Check

1. Which of the following would NOT trigger the ULTRAPLAN keyword scanner?
2. Why is ultraplanLaunching set synchronously before the detached async flow starts?
3. What does the quietIdle heuristic require before transitioning to needs_input?
4. How does the teleport path signal "send the plan back to the local terminal" via the CCR event stream?
5. When the user approves execution in the CCR browser (remote path), what should the local CLI do with the remote session?