import { readFileSync } from 'fs'; import { REMOTE_CONTROL_DISCONNECTED_MSG } from '../bridge/types.js'; import type { Command } from '../commands.js'; import { DIAMOND_OPEN } from '../constants/figures.js'; import { getRemoteSessionUrl } from '../constants/product.js'; import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'; import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from '../services/analytics/index.js'; import type { AppState } from '../state/AppStateStore.js'; import { checkRemoteAgentEligibility, formatPreconditionError, RemoteAgentTask, type RemoteAgentTaskState, registerRemoteAgentTask, } from '../tasks/RemoteAgentTask/RemoteAgentTask.js'; import type { LocalJSXCommandCall } from '../types/command.js'; import { logForDebugging } from '../utils/debug.js'; import { errorMessage } from '../utils/errors.js'; import { logError } from '../utils/log.js'; import { enqueuePendingNotification } from '../utils/messageQueueManager.js'; import { ALL_MODEL_CONFIGS } from '../utils/model/configs.js'; import { updateTaskState } from '../utils/task/framework.js'; import { archiveRemoteSession, teleportToRemote } from '../utils/teleport.js'; import { pollForApprovedExitPlanMode, UltraplanPollError } from '../utils/ultraplan/ccrSession.js'; import { getPromptText, getDialogConfig, getPromptIdentifier, type PromptIdentifier } from '../utils/ultraplan/prompt.js'; import { registerCleanup } from '../utils/cleanupRegistry.js'; // TODO(prod-hardening): OAuth token may go stale over the 30min poll; // consider refresh. /** * Multi-agent exploration is slow; 30min timeout. * * @deprecated use getUltraplanTimeoutMs() */ const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000; export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the-web'; export function getUltraplanTimeoutMs(): number { return getFeatureValue_CACHED_MAY_BE_STALE('tengu_ultraplan_timeout_seconds', 1800) * 1000 } /** * 是否启用 ultraplan, 默认启用 * * @returns */ export function isUltraplanEnabled(): boolean { return getFeatureValue_CACHED_MAY_BE_STALE<{enabled: boolean} | null>('tengu_ultraplan_config', { enabled: true })?.enabled === true } // CCR runs against the first-party API — use the canonical ID, not the // provider-specific string getModelStrings() would return (which may be a // Bedrock ARN or Vertex ID on the local CLI). Read at call time, not module // load: the GrowthBook cache is empty at import and `/config` Gates can flip // it between invocations. function getUltraplanModel(): string { return getFeatureValue_CACHED_MAY_BE_STALE('tengu_ultraplan_model', ALL_MODEL_CONFIGS.opus47.firstParty); } // prompt.txt is wrapped in so the CCR browser hides // scaffolding (CLI_BLOCK_TAGS dropped by stripSystemNotifications) // while the model still sees full text. // Phrasing deliberately avoids the feature name because // the remote CCR CLI runs keyword detection on raw input before // any tag stripping, and a bare "ultraplan" in the prompt would self-trigger as // /ultraplan, which is filtered out of headless mode as "Unknown skill" // // Bundler inlines .txt as a string; the test runner wraps it as {default}. /* eslint-disable @typescript-eslint/no-require-imports */ const _rawPrompt = require('../utils/ultraplan/prompt.txt'); /* eslint-enable @typescript-eslint/no-require-imports */ const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPrompt : _rawPrompt.default).trimEnd(); // Dev-only prompt override resolved eagerly at module load. // Gated to ant builds (USER_TYPE is a build-time define, // so the override path is DCE'd from external builds). // Shell-set env only, so top-level process.env read is fine // — settings.env never injects this. // @deprecated use buildUltraplanPrompt() /* eslint-disable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs -- ant-only dev override; eager top-level read is the point (crash at startup, not silently inside the slash-command try/catch) */ const ULTRAPLAN_INSTRUCTIONS: string = process.env.USER_TYPE === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE ? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd() : DEFAULT_INSTRUCTIONS; /* eslint-enable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs */ /** * Assemble the initial CCR user message. seedPlan and blurb stay outside the * system-reminder so the browser renders them; scaffolding is hidden. */ export function buildUltraplanPrompt(blurb: string, seedPlan?: string, promptId?: PromptIdentifier): string { const parts: string[] = []; if (seedPlan) { parts.push('Here is a draft plan to refine:', '', seedPlan, ''); } // parts.push(ULTRAPLAN_INSTRUCTIONS) parts.push(getPromptText(promptId!)); if (blurb) { parts.push('', blurb); } return parts.join('\n'); } function startDetachedPoll( taskId: string, sessionId: string, url: string, getAppState: () => AppState, setAppState: (f: (prev: AppState) => AppState) => void, ): void { const started = Date.now(); let failed = false; void (async () => { try { const { plan, rejectCount, executionTarget } = await pollForApprovedExitPlanMode( sessionId, getUltraplanTimeoutMs(), phase => { if (phase === 'needs_input') logEvent('tengu_ultraplan_awaiting_input', {}); updateTaskState(taskId, setAppState, t => { if (t.status !== 'running') return t; const next = phase === 'running' ? undefined : phase; return t.ultraplanPhase === next ? t : { ...t, ultraplanPhase: next }; }); }, () => getAppState().tasks?.[taskId]?.status !== 'running', ); logEvent('tengu_ultraplan_approved', { duration_ms: Date.now() - started, plan_length: plan.length, reject_count: rejectCount, execution_target: executionTarget as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); if (executionTarget === 'remote') { // User chose "execute in CCR" in the browser PlanModal — the remote // session is now coding. Skip archive (ARCHIVE has no running-check, // would kill mid-execution) and skip the choice dialog (already chose). // Guard on task status so a poll that resolves after stopUltraplan // doesn't notify for a killed session. const task = getAppState().tasks?.[taskId]; if (task?.status !== 'running') return; updateTaskState(taskId, setAppState, t => t.status !== 'running' ? t : { ...t, status: 'completed', endTime: Date.now() }, ); setAppState(prev => (prev.ultraplanSessionUrl === url ? { ...prev, ultraplanSessionUrl: undefined } : prev)); 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', }); } else { // Teleport: set pendingChoice so REPL mounts UltraplanChoiceDialog. // The dialog owns archive + URL clear on choice. Guard on task status // so a poll that resolves after stopUltraplan doesn't resurrect the // dialog for a killed session. setAppState(prev => { const task = prev.tasks?.[taskId]; if (!task || task.status !== 'running') return prev; return { ...prev, ultraplanPendingChoice: { plan, sessionId, taskId }, }; }); } } catch (e) { // If the task was stopped (stopUltraplan sets status=killed), the poll // erroring is expected — skip the failure notification and cleanup // (kill() already archived; stopUltraplan cleared the URL). const task = getAppState().tasks?.[taskId]; if (task?.status !== 'running') return; failed = true; logEvent('tengu_ultraplan_failed', { duration_ms: Date.now() - started, reason: (e instanceof UltraplanPollError ? e.reason : 'network_or_unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, reject_count: e instanceof UltraplanPollError ? e.rejectCount : undefined, }); enqueuePendingNotification({ value: `Ultraplan failed: ${errorMessage(e)}\n\nSession: ${url}`, mode: 'task-notification', }); // Error path owns cleanup; teleport path defers to the dialog; remote // path handled its own cleanup above. void archiveRemoteSession(sessionId).catch(e => logForDebugging(`ultraplan archive failed: ${String(e)}`)); setAppState(prev => // Compare against this poll's URL so a newer relaunched session's // URL isn't cleared by a stale poll erroring out. prev.ultraplanSessionUrl === url ? { ...prev, ultraplanSessionUrl: undefined } : prev, ); } finally { // Remote path already set status=completed above; teleport path // leaves status=running so the pill shows the ultraplanPhase state // until UltraplanChoiceDialog completes the task after the user's // choice. Setting completed here would filter the task out of // isBackgroundTask before the pill can render the phase state. // Failure path has no dialog, so it owns the status transition here. if (failed) { updateTaskState(taskId, setAppState, t => t.status !== 'running' ? t : { ...t, status: 'failed', endTime: Date.now() }, ); } } })(); } // Renders immediately so the terminal doesn't appear hung during the // multi-second teleportToRemote round-trip. function buildLaunchMessage(disconnectedBridge?: boolean): string { const prefix = disconnectedBridge ? `${REMOTE_CONTROL_DISCONNECTED_MSG} ` : ''; return `${DIAMOND_OPEN} ultraplan\n${prefix}Starting Claude Code on the web…`; } function buildSessionReadyMessage(url: string): string { return `${DIAMOND_OPEN} ultraplan · Monitor progress in Claude Code on the web ${url}\nYou can continue working — when the ${DIAMOND_OPEN} fills, press ↓ to view results`; } function buildAlreadyActiveMessage(url: string | undefined): string { return url ? `ultraplan: already polling. Open ${url} to check status, or wait for the plan to land here.` : 'ultraplan: already launching. Please wait for the session to start.'; } /** * Stop a running ultraplan: archive the remote session (halts it but keeps the * URL viewable), kill the local task entry (clears the pill), and clear * ultraplanSessionUrl (re-arms the keyword trigger). startDetachedPoll's * shouldStop callback sees the killed status on its next tick and throws; * the catch block early-returns when status !== 'running'. */ export async function stopUltraplan( taskId: string, sessionId: string, setAppState: (f: (prev: AppState) => AppState) => void, ): Promise { // RemoteAgentTask.kill archives the session (with .catch) — no separate // archive call needed here. await RemoteAgentTask.kill(taskId, setAppState); setAppState(prev => prev.ultraplanSessionUrl || prev.ultraplanPendingChoice || prev.ultraplanLaunching ? { ...prev, ultraplanSessionUrl: undefined, ultraplanPendingChoice: undefined, ultraplanLaunching: undefined, } : prev, ); const url = getRemoteSessionUrl(sessionId, process.env.SESSION_INGRESS_URL); enqueuePendingNotification({ value: `Ultraplan stopped.\n\nSession: ${url}`, mode: 'task-notification', }); enqueuePendingNotification({ value: 'The user stopped the ultraplan session above. Do not respond to the stop notification — wait for their next message.', mode: 'task-notification', isMeta: true, }); } /** * Shared entry for the slash command, keyword trigger, and the plan-approval * dialog's "Ultraplan" button. When seedPlan is present (dialog path), it is * prepended as a draft to refine; blurb may be empty in that case. * * Resolves immediately with the user-facing message. Eligibility check, * session creation, and task registration run detached and failures surface via * enqueuePendingNotification. */ export async function launchUltraplan(opts: { blurb: string; seedPlan?: string; promptIdentifier?: PromptIdentifier; getAppState: () => AppState; setAppState: (f: (prev: AppState) => AppState) => void; signal: AbortSignal; /** True if the caller disconnected Remote Control before launching. */ disconnectedBridge?: boolean; /** * Called once teleportToRemote resolves with a session URL. Callers that * have setMessages (REPL) append this as a second transcript message so the * URL is visible without opening the ↓ detail view. Callers without * transcript access (ExitPlanModePermissionRequest) omit this — the pill * still shows live status. */ onSessionReady?: (msg: string) => void; }): Promise { const { blurb, seedPlan, promptIdentifier, getAppState, setAppState, signal, disconnectedBridge, onSessionReady } = opts; const { ultraplanSessionUrl: active, ultraplanLaunching } = getAppState(); if (active || ultraplanLaunching) { logEvent('tengu_ultraplan_create_failed', { reason: (active ? 'already_polling' : 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); return buildAlreadyActiveMessage(active); } if (!blurb && !seedPlan) { // No event — bare /ultraplan is a usage query, not an attempt. return [ // Rendered via ; raw is tokenized as HTML // and dropped. Backslash-escape the brackets. 'Usage: /ultraplan \\, or include "ultraplan" anywhere', 'in your prompt', '', // 'Advanced multi-agent plan mode with our most powerful model', // '(Opus). Runs in Claude Code on the web. When the plan is ready,', // 'you can execute it in the web session or send it back here.', // 'Terminal stays free while the remote plans.', // 'Requires /login.', ...getDialogConfig().usageBlurb, '', `Terms: ${CCR_TERMS_URL}`, ].join('\n'); } // Set synchronously before the detached flow to prevent duplicate launches // during the teleportToRemote window. setAppState(prev => prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true }); void launchDetached({ blurb, seedPlan, promptIdentifier, getAppState, setAppState, signal, onSessionReady, }); return buildLaunchMessage(disconnectedBridge); } async function launchDetached(opts: { blurb: string; seedPlan?: string; promptIdentifier?: PromptIdentifier; getAppState: () => AppState; setAppState: (f: (prev: AppState) => AppState) => void; signal: AbortSignal; onSessionReady?: (msg: string) => void; }): Promise { const { blurb, seedPlan, promptIdentifier = getPromptIdentifier(), getAppState, setAppState, signal, onSessionReady } = opts; // Hoisted so the catch block can archive the remote session if an error // occurs after teleportToRemote succeeds (avoids 30min orphan). let sessionId: string | undefined; try { // const model = getUltraplanModel() const eligibility = await checkRemoteAgentEligibility(); if (!eligibility.eligible) { logEvent('tengu_ultraplan_create_failed', { reason: 'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, precondition_errors: eligibility.errors .map(e => e.type) .join(',') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); const reasons = eligibility.errors.map(formatPreconditionError).join('\n'); enqueuePendingNotification({ value: `ultraplan: cannot launch remote session —\n${reasons}`, mode: 'task-notification', }); return; } const prompt = buildUltraplanPrompt(blurb, seedPlan, promptIdentifier); let bundleFailMsg: string | undefined; let createFailMsg: string | undefined; const session = await teleportToRemote({ initialMessage: prompt, description: blurb || 'Refine local plan', // model, permissionMode: 'plan', ultraplan: true, signal, useDefaultEnvironment: true, onBundleFail: msg => { bundleFailMsg = msg; }, onCreateFail: msg => { createFailMsg = msg; }, }) if (!session) { let failMsg = bundleFailMsg ?? createFailMsg; logEvent('tengu_ultraplan_create_failed', { reason: (bundleFailMsg ? 'bundle_fail' : createFailMsg ? 'create_api_fail' : 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); enqueuePendingNotification({ value: `ultraplan: session creation failed${failMsg ? ` — ${failMsg}` : ''}. See --debug for details.`, mode: 'task-notification', }); return; } sessionId = session.id; const url = getRemoteSessionUrl(session.id, process.env.SESSION_INGRESS_URL); setAppState(prev => ({ ...prev, ultraplanSessionUrl: url, ultraplanLaunching: undefined, })); onSessionReady?.(buildSessionReadyMessage(url)); logEvent('tengu_ultraplan_launched', { has_seed_plan: Boolean(seedPlan), prompt_identifier: promptIdentifier as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS // model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); // TODO(#23985): replace registerRemoteAgentTask + startDetachedPoll with // ExitPlanModeScanner inside startRemoteSessionPolling. const { taskId } = registerRemoteAgentTask({ remoteTaskType: 'ultraplan', session: { id: session.id, title: blurb || 'Ultraplan' }, command: blurb, context: { abortController: new AbortController(), getAppState, setAppState, }, isUltraplan: true, }); startDetachedPoll(taskId, session.id, url, getAppState, setAppState); registerCleanup(async()=>{ if(getAppState().ultraplanSessionUrl === url) { await archiveRemoteSession(session.id, 1500) } }); } catch (e) { logError(e); logEvent('tengu_ultraplan_create_failed', { reason: 'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); enqueuePendingNotification({ value: `ultraplan: unexpected error — ${errorMessage(e)}`, mode: 'task-notification', }); enqueuePendingNotification({ value: `Ultraplan hit an unexpected error during launch. Wait for the user's next instructions.`, mode: 'task-notification', isMeta: true }); if (sessionId) { // Error after teleport succeeded — archive so the remote doesn't sit // running for 30min with nobody polling it. void archiveRemoteSession(sessionId).catch(err => logForDebugging('ultraplan: failed to archive orphaned session', err), ); // ultraplanSessionUrl may have been set before the throw; clear it so // the "already polling" guard doesn't block future launches. setAppState(prev => prev.ultraplanSessionUrl ? { ...prev, ultraplanSessionUrl: undefined } : prev); } } finally { // No-op on success: the url-setting setAppState already cleared this. setAppState(prev => prev.ultraplanLaunching ? { ...prev, ultraplanLaunching: undefined } : prev); } } const call: LocalJSXCommandCall = async (onDone, context, args) => { const blurb = args.trim(); // Bare /ultraplan (no args, no seed plan) just shows usage — no dialog. if (!blurb) { const msg = await launchUltraplan({ blurb, getAppState: context.getAppState, setAppState: context.setAppState, signal: context.abortController.signal, }); onDone(msg, { display: 'system' }); return null; } // Guard matches launchUltraplan's own check — showing the dialog when a // session is already active or launching would waste the user's click and set // hasSeenUltraplanTerms before the launch fails. const { ultraplanSessionUrl: active, ultraplanLaunching } = context.getAppState(); if (active || ultraplanLaunching) { logEvent('tengu_ultraplan_create_failed', { reason: (active ? 'already_polling' : 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); onDone(buildAlreadyActiveMessage(active), { display: 'system' }); return null; } // Mount the pre-launch dialog via focusedInputDialog (bottom region, like // permission dialogs) rather than returning JSX (transcript area, anchors // at top of scrollback). REPL.tsx handles launch/clear/cancel on choice. context.setAppState(prev => ({ ...prev, ultraplanLaunchPending: { blurb } })); // 'skip' suppresses the (no content) echo — the dialog's choice handler // adds the real /ultraplan echo + launch confirmation. onDone(undefined, { display: 'skip' }); return null; }; export default { type: 'local-jsx', name: 'ultraplan', description: `~10–30 min · Claude Code on the web drafts an advanced plan you can edit and approve. See ${CCR_TERMS_URL}`, argumentHint: '', // isEnabled: () => process.env.USER_TYPE === 'ant', isEnabled: () => isUltraplanEnabled(), load: () => Promise.resolve({ call }), } satisfies Command;