From 335dfd7aaa772409c794cce64839111c07a7a4fb Mon Sep 17 00:00:00 2001 From: parsakhaz Date: Wed, 22 Jul 2026 14:51:43 -0700 Subject: [PATCH 1/4] feat: semantic panel state model for RunPane orchestration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of the event-driven RunPane orchestration epic (#356). Adds a canonical, agent-agnostic panel state layer in the main process and surfaces it additively in the existing `panels screen` / `panels wait` JSON responses, so an orchestrator can finally distinguish "the CLI has answered once" (terminalReady) from "the agent is working right now" (agentActivity). - New state fields: terminalReady, agentActivity (unknown|starting|active|idle|exited), inputRequired, blocked, hasNewOutput, outputGeneration, lastMeaningfulEventAt. - Idle detection is a configurable byte-silence debounce (agentIdleDebounceMs, default 60s) running parallel to — not replacing — the existing fixed 30s activityStatus timer, so human notification timing is unchanged. - Exit state is persisted onto the panel's customState so agentActivity reports "exited" after the live terminal record is torn down; all six terminal-map removal paths clear the semantic timer, and stale PTY callbacks are rejected by record identity. - Blocker detection moves into the screen result over a fixed 80-line window, independent of the caller's --limit; `panels wait` reuses it and its matching semantics are unchanged. - Contract schemas extended and regenerated; npm, shared, and Python artifacts stay in parity. Refs #356 --- contracts/runpane/contract.json | 82 ++++ frontend/src/types/config.ts | 3 + main/src/ipc/config.test.ts | 12 + main/src/ipc/runpane.test.ts | 83 +++- main/src/ipc/runpane.ts | 21 +- main/src/services/configManager.ts | 1 + .../src/services/terminalPanelManager.test.ts | 439 +++++++++++++++++- main/src/services/terminalPanelManager.ts | 134 +++++- main/src/types/config.ts | 3 + .../src/runpane/generated_contract.py | 2 +- packages/runpane/src/generated/contract.ts | 82 ++++ shared/types/generatedRunpaneContract.ts | 82 ++++ shared/types/panels.ts | 3 + shared/types/runpaneOrchestration.ts | 9 + 14 files changed, 945 insertions(+), 11 deletions(-) diff --git a/contracts/runpane/contract.json b/contracts/runpane/contract.json index e7174610..52391021 100644 --- a/contracts/runpane/contract.json +++ b/contracts/runpane/contract.json @@ -3266,6 +3266,34 @@ }, "lastActivity": { "type": "string" + }, + "terminalReady": { + "type": "boolean" + }, + "agentActivity": { + "enum": [ + "unknown", + "starting", + "active", + "idle", + "exited" + ] + }, + "inputRequired": { + "type": "boolean" + }, + "blocked": { + "type": "boolean", + "description": "Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details." + }, + "hasNewOutput": { + "type": "boolean" + }, + "outputGeneration": { + "type": "number" + }, + "lastMeaningfulEventAt": { + "type": "string" } }, "additionalProperties": false @@ -3273,6 +3301,31 @@ "initialInput": { "$ref": "#/jsonSchemas/paneCreateResult/properties/items/items/oneOf/0/properties/initialInput" }, + "blocked": { + "type": "object", + "description": "Structured blocker details. This object is distinct from state.blocked, which is only a boolean.", + "required": [ + "kind", + "message" + ], + "properties": { + "kind": { + "enum": [ + "codex-update", + "agent-prompt", + "submission_unverified", + "unknown" + ] + }, + "message": { + "type": "string" + }, + "suggestedCommand": { + "type": "string" + } + }, + "additionalProperties": false + }, "nextCommand": { "type": "string" } @@ -3400,12 +3453,41 @@ }, "lastActivity": { "type": "string" + }, + "terminalReady": { + "type": "boolean" + }, + "agentActivity": { + "enum": [ + "unknown", + "starting", + "active", + "idle", + "exited" + ] + }, + "inputRequired": { + "type": "boolean" + }, + "blocked": { + "type": "boolean", + "description": "Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details." + }, + "hasNewOutput": { + "type": "boolean" + }, + "outputGeneration": { + "type": "number" + }, + "lastMeaningfulEventAt": { + "type": "string" } }, "additionalProperties": false }, "blocked": { "type": "object", + "description": "Structured blocker details. This object is distinct from state.blocked, which is only a boolean.", "required": [ "kind", "message" diff --git a/frontend/src/types/config.ts b/frontend/src/types/config.ts index 77a90727..b2c0dd2c 100644 --- a/frontend/src/types/config.ts +++ b/frontend/src/types/config.ts @@ -77,6 +77,8 @@ export interface AppConfig { autoStartOnBoot?: boolean; // Keep the computer awake while any session is active keepAwakeWhileSessionsActive?: boolean; + // Silence interval before agent activity is considered idle + agentIdleDebounceMs?: number; // Stravu MCP integration stravuApiKey?: string; stravuServerUrl?: string; @@ -167,6 +169,7 @@ export interface UpdateConfigRequest { autoCheckUpdates?: boolean; autoStartOnBoot?: boolean; keepAwakeWhileSessionsActive?: boolean; + agentIdleDebounceMs?: number; stravuApiKey?: string; stravuServerUrl?: string; theme?: AppConfig['theme']; diff --git a/main/src/ipc/config.test.ts b/main/src/ipc/config.test.ts index f90fc67e..aad5cbcd 100644 --- a/main/src/ipc/config.test.ts +++ b/main/src/ipc/config.test.ts @@ -97,6 +97,18 @@ describe('config IPC handlers', () => { } }); + it('round-trips agentIdleDebounceMs through config:update', async () => { + const ipcMain = createIpcMainStub(); + registerConfigHandlers(ipcMain as unknown as IpcMain, createServicesStub([])); + + const updateConfig = ipcMain.handlers.get('config:update'); + expect(updateConfig).toBeDefined(); + await expect(updateConfig?.({}, { agentIdleDebounceMs: 12_345 })).resolves.toEqual({ + success: true, + data: expect.objectContaining({ agentIdleDebounceMs: 12_345 }), + }); + }); + it('removes managed AGENTS blocks from all saved projects when disabled', async () => { const activeProject = await createTempProject(1); const inactiveProject = await createTempProject(2); diff --git a/main/src/ipc/runpane.test.ts b/main/src/ipc/runpane.test.ts index d575a78f..256bf7b6 100644 --- a/main/src/ipc/runpane.test.ts +++ b/main/src/ipc/runpane.test.ts @@ -84,6 +84,7 @@ function terminalSnapshot( activityStatus: 'active' | 'idle', agentType: 'claude' | 'codex' = 'codex', lastActivityTime = '2026-01-01T00:02:00.000Z', + isCliReady = true, ) { return { initialized: true, @@ -95,8 +96,13 @@ function terminalSnapshot( lastActivityTime, currentCommand: agentType, isCliPanel: true, - isCliReady: true, + isCliReady, agentType, + agentActivity: activityStatus, + terminalReady: isCliReady, + lastMeaningfulEventAt: lastActivityTime, + outputGeneration: 3, + outputGenerationAtQuiescence: activityStatus === 'idle' ? 3 : 2, } as const; } @@ -981,6 +987,56 @@ describe('runpane IPC handlers', () => { }); }); + it('AC1 reports terminal readiness and active agent activity as distinct screen fields', async () => { + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockReturnValue( + terminalSnapshot('agent is still working\n', 'active'), + ); + const registry = createRegistry(); + + const result = await registry.invoke('runpane:panels:screen', [{ + panelId: terminalPanel.id, + limit: 80, + }]); + + expect(result).toMatchObject({ + state: { + terminalReady: true, + agentActivity: 'active', + hasNewOutput: true, + outputGeneration: 3, + }, + }); + }); + + it('detects blockers independently of a small screen limit', async () => { + const text = [ + 'Codex update available', + '2. Skip', + ...Array.from({ length: 10 }, (_, index) => `later line ${index}`), + ].join('\n'); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockReturnValue( + terminalSnapshot(text, 'idle'), + ); + const registry = createRegistry(); + + const result = await registry.invoke('runpane:panels:screen', [{ + panelId: terminalPanel.id, + limit: 5, + }]); + + expect(result).toMatchObject({ + returnedLineCount: 5, + state: { + blocked: true, + inputRequired: true, + }, + blocked: { + kind: 'codex-update', + }, + }); + expect(result.text).not.toContain('update available'); + }); + it('waits for ready terminal state with bounded screen output', async () => { vi.mocked(terminalPanelManager.getTerminalSnapshot).mockReturnValue({ initialized: true, @@ -1015,6 +1071,31 @@ describe('runpane IPC handlers', () => { }); }); + it('D2 freeze keeps wait --for ready matched while semantic activity is active', async () => { + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockReturnValue( + terminalSnapshot('agent is still working\n', 'active'), + ); + const registry = createRegistry(); + + const result = await registry.invoke('runpane:panels:wait', [{ + panelId: terminalPanel.id, + condition: 'ready', + timeoutMs: 10, + }]); + + expect(result).toMatchObject({ + ok: true, + matched: true, + timedOut: false, + blocked: undefined, + state: { + terminalReady: true, + agentActivity: 'active', + }, + nextCommand: `runpane panels screen --panel ${terminalPanel.id} --limit 80 --json`, + }); + }); + it('does not treat persisted terminal state as live wait readiness', async () => { vi.mocked(terminalPanelManager.isTerminalInitialized).mockReturnValue(false); vi.mocked(terminalPanelManager.getTerminalSnapshot).mockReturnValue(null); diff --git a/main/src/ipc/runpane.ts b/main/src/ipc/runpane.ts index 254e0a0c..04745711 100644 --- a/main/src/ipc/runpane.ts +++ b/main/src/ipc/runpane.ts @@ -1027,8 +1027,15 @@ async function buildPanelScreenResult(panel: ToolPanel, limit: number): Promise< await terminalPanelManager.waitForTerminalState(panel.id); const liveSnapshot = terminalPanelManager.getTerminalSnapshot(panel.id); const customState = getTerminalCustomState(panel); - const state = panelStateSummary(panel, liveSnapshot, customState); + const baseState = panelStateSummary(panel, liveSnapshot, customState); const { source, rawText } = selectPanelScreenText(liveSnapshot, customState); + const blockerWindow = boundSanitizedLines(rawText, DEFAULT_PANEL_SCREEN_LIMIT); + const blocked = detectPanelBlocker(blockerWindow.text, baseState.agentType, panel.id); + const state: RunpanePanelStateSummary = { + ...baseState, + blocked: blocked !== undefined, + inputRequired: blocked?.kind === 'agent-prompt' || blocked?.kind === 'codex-update', + }; const bounded = boundSanitizedLines(rawText, limit); return { @@ -1041,6 +1048,7 @@ async function buildPanelScreenResult(panel: ToolPanel, limit: number): Promise< hasMore: bounded.hasMore, text: bounded.text, state, + blocked, nextCommand: bounded.hasMore ? panelOutputCommand(panel.id) : panelWaitCommand(panel.id), }; } @@ -1087,6 +1095,8 @@ function panelStateSummary( ? customState.agentType as RunpaneAgentId : undefined; const hasLiveTerminal = Boolean(snapshot || terminalPanelManager.isTerminalInitialized(panel.id)); + const agentActivity = snapshot?.agentActivity + ?? (customState.exitedAt ? 'exited' : 'unknown'); return { initialized: hasLiveTerminal, @@ -1096,6 +1106,13 @@ function panelStateSummary( isCliPanel: snapshot?.isCliPanel ?? customState.isCliPanel, agentType: snapshot?.agentType ?? customAgentType, lastActivity: snapshot?.lastActivityTime ?? customState.lastActivityTime ?? toIsoString(panel.metadata.lastActiveAt), + terminalReady: snapshot?.terminalReady, + agentActivity, + hasNewOutput: snapshot + ? snapshot.outputGeneration > snapshot.outputGenerationAtQuiescence + : false, + outputGeneration: snapshot?.outputGeneration, + lastMeaningfulEventAt: snapshot?.lastMeaningfulEventAt ?? customState.exitedAt, }; } @@ -1139,7 +1156,7 @@ async function waitForPanel(panel: ToolPanel, request: RunpanePanelWaitRequest): while (Date.now() - startedAt <= timeoutMs) { lastScreen = await buildPanelScreenResult(panel, DEFAULT_PANEL_SCREEN_LIMIT); condition = request.condition ?? defaultWaitCondition(lastScreen.state); - const blocked = detectPanelBlocker(lastScreen.text, lastScreen.state.agentType, panel.id); + const blocked = lastScreen.blocked; const matched = isWaitConditionMatched(condition, lastScreen, request.contains, blocked); if (matched) { diff --git a/main/src/services/configManager.ts b/main/src/services/configManager.ts index f163df84..55022625 100644 --- a/main/src/services/configManager.ts +++ b/main/src/services/configManager.ts @@ -57,6 +57,7 @@ export class ConfigManager extends EventEmitter { defaultOrchestratorAgent: DEFAULT_PANE_CHAT_AGENT, autoStartOnBoot: true, keepAwakeWhileSessionsActive: true, + agentIdleDebounceMs: 60_000, stravuApiKey: undefined, stravuServerUrl: '', // Stravu integration disabled notifications: { diff --git a/main/src/services/terminalPanelManager.test.ts b/main/src/services/terminalPanelManager.test.ts index 8707fc84..54156c2f 100644 --- a/main/src/services/terminalPanelManager.test.ts +++ b/main/src/services/terminalPanelManager.test.ts @@ -3,8 +3,10 @@ import type { ConfigManager } from './configManager'; import { resetPaneRuntimeForTests, setPaneRuntime } from '../core/runtime'; import { createFlowControlRecord, disposeFlowControlRecord, type FlowControlRecord } from '../ptyHost/flowControl'; import { TerminalStateEmulator } from './terminalStateEmulator'; +import type { ToolPanel } from '../../../shared/types/panels'; -vi.mock('@lydell/node-pty', () => ({})); +const ptySpawn = vi.hoisted(() => vi.fn()); +vi.mock('@lydell/node-pty', () => ({ spawn: ptySpawn })); vi.mock('./panelManager', () => ({ panelManager: { @@ -34,9 +36,11 @@ vi.mock('../utils/attribution', () => ({ getGitAttributionEnv: vi.fn(() => ({})), })); -import { TerminalPanelManager } from './terminalPanelManager'; +import { normalizeAgentIdleDebounceMs, TerminalPanelManager } from './terminalPanelManager'; import { panelManager } from './panelManager'; +type ExitEvent = { exitCode: number; signal?: number }; + type TerminalUnderTest = { pty: { cols: number; @@ -45,7 +49,13 @@ type TerminalUnderTest = { resume: ReturnType; resize: ReturnType; write: ReturnType; + kill: ReturnType; + onData(listener: (data: string) => void): { dispose(): void }; + onExit(listener: (event: ExitEvent) => void): { dispose(): void }; + emitData(data: string): void; + emitExit(event: ExitEvent): void; }; + ptyId?: string; isPtyHost: boolean; panelId: string; sessionId: string; @@ -57,6 +67,7 @@ type TerminalUnderTest = { lastActivity: Date; lastOutputAt?: Date; outputGeneration: number; + isWSL?: boolean; wslContext: null; flowControl: FlowControlRecord; outputBuffer: string; @@ -65,6 +76,12 @@ type TerminalUnderTest = { isAlternateScreen: boolean; activityStatus: 'active' | 'idle'; idleTimer: ReturnType | null; + agentActivity: 'unknown' | 'starting' | 'active' | 'idle' | 'exited'; + agentIdleTimer: ReturnType | null; + lastMeaningfulEventAt: string; + outputGenerationAtQuiescence: number; + exitEventHandled: boolean; + suppressSemanticExitPersistence: boolean; inSyncBlock: boolean; codexResumeOutputBuffer: string; codexAgentSessionId?: string; @@ -113,7 +130,21 @@ type LaunchCommandAccess = { }; }; +type SemanticStateAccess = { + terminals: Map; + setupTerminalHandlers(terminal: TerminalUnderTest): void; + stampMeaningfulEvent(terminal: TerminalUnderTest, at?: Date): void; + getTerminalSnapshot(panelId: string): ReturnType; + writeToTerminal(panelId: string, data: string): void; + destroyTerminal(panelId: string): void; + destroyAllTerminals(): void; + respawnAll(): Promise; + initializeTerminal: TerminalPanelManager['initializeTerminal']; +}; + function createTerminal(overrides: Partial = {}): TerminalUnderTest { + const dataListeners = new Set<(data: string) => void>(); + const exitListeners = new Set<(event: ExitEvent) => void>(); return { pty: { cols: 80, @@ -122,6 +153,21 @@ function createTerminal(overrides: Partial = {}): TerminalUnd resume: vi.fn(), resize: vi.fn(), write: vi.fn(), + kill: vi.fn(), + onData(listener) { + dataListeners.add(listener); + return { dispose: () => dataListeners.delete(listener) }; + }, + onExit(listener) { + exitListeners.add(listener); + return { dispose: () => exitListeners.delete(listener) }; + }, + emitData(data) { + for (const listener of [...dataListeners]) listener(data); + }, + emitExit(event) { + for (const listener of [...exitListeners]) listener(event); + }, }, isPtyHost: false, panelId: 'panel-1', @@ -140,6 +186,12 @@ function createTerminal(overrides: Partial = {}): TerminalUnd isAlternateScreen: false, activityStatus: 'idle', idleTimer: null, + agentActivity: 'starting', + agentIdleTimer: null, + lastMeaningfulEventAt: new Date().toISOString(), + outputGenerationAtQuiescence: 0, + exitEventHandled: false, + suppressSemanticExitPersistence: false, inSyncBlock: false, codexResumeOutputBuffer: '', ...overrides, @@ -173,17 +225,398 @@ describe('TerminalPanelManager terminal resize', () => { }); }); -function createConfigManagerStub(): ConfigManager { +function createConfigManagerStub(agentIdleDebounceMs?: unknown): ConfigManager { return { getUsePtyHost: () => false, + getPreferredShell: () => 'auto', + getConfig: () => ({ agentIdleDebounceMs }), } as ConfigManager; } +function createPanel(customState: Record = {}): ToolPanel { + return { + id: 'panel-1', + sessionId: 'session-1', + type: 'terminal', + title: 'Terminal', + state: { isActive: true, customState }, + metadata: { + createdAt: '2026-01-01T00:00:00.000Z', + lastActiveAt: '2026-01-01T00:00:00.000Z', + position: 0, + }, + }; +} + +function installRuntime(agentIdleDebounceMs?: unknown): { send: ReturnType } { + const eventSink = { send: vi.fn() }; + setPaneRuntime({ + eventSink, + daemonEventSink: { send: vi.fn() }, + getConfigManager: () => createConfigManagerStub(agentIdleDebounceMs), + getPtyHostRuntime: () => null, + getWebviewContextMap: () => new Map(), + }); + return eventSink; +} + async function flushPromises(): Promise { await Promise.resolve(); await Promise.resolve(); } +describe('TerminalPanelManager semantic agent state', () => { + afterEach(() => { + resetPaneRuntimeForTests(); + vi.mocked(panelManager.getPanel).mockReset(); + vi.mocked(panelManager.updatePanel).mockReset(); + ptySpawn.mockReset(); + vi.clearAllTimers(); + vi.useRealTimers(); + }); + + it.each([1_000, 45_000])( + 'AC2/AC3 transitions active to idle at configured debounce %dms, not N-1', + async (debounceMs) => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); + installRuntime(debounceMs); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const terminal = createTerminal({ outputBuffer: '' }); + manager.terminals.set(terminal.panelId, terminal); + manager.setupTerminalHandlers(terminal); + + terminal.pty.emitData('working'); + const activeAt = terminal.lastMeaningfulEventAt; + expect(terminal.agentActivity).toBe('active'); + + await vi.advanceTimersByTimeAsync(debounceMs - 1); + expect(terminal.agentActivity).toBe('active'); + + await vi.advanceTimersByTimeAsync(1); + expect(terminal.agentActivity).toBe('idle'); + expect(terminal.lastMeaningfulEventAt).not.toBe(activeAt); + expect(terminal.outputGenerationAtQuiescence).toBe(1); + disposeFlowControlRecord(terminal.flowControl); + }, + ); + + it.each([undefined, -1, 0, 1.5, Number.NaN, '60000'])( + 'normalizes invalid agent idle debounce %s to 60000', + (value) => { + expect(normalizeAgentIdleDebounceMs(value)).toBe(60_000); + }, + ); + + it('preserves output freshness across output, real readiness latch, quiescence, and exit', async () => { + vi.useFakeTimers(); + installRuntime(1_000); + const panel = createPanel({ + initialCommand: 'codex', + isCliPanel: true, + isCliReady: false, + agentType: 'codex', + }); + vi.mocked(panelManager.getPanel).mockReturnValue(panel); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const spawnedPty = createTerminal({ outputBuffer: '' }).pty; + ptySpawn.mockReturnValue(spawnedPty); + + await manager.initializeTerminal(panel, process.cwd()); + const terminal = manager.terminals.get(panel.id); + expect(terminal).toBeDefined(); + const hasNewOutput = () => terminal!.outputGeneration > terminal!.outputGenerationAtQuiescence; + + expect(hasNewOutput()).toBe(false); + terminal!.pty.emitData('$ '); + await vi.advanceTimersByTimeAsync(50); + expect(terminal!.pty.write).toHaveBeenCalledWith('codex\r'); + expect(hasNewOutput()).toBe(true); + const beforeReady = terminal!.lastMeaningfulEventAt; + terminal!.pty.emitData('codex first frame'); + await vi.advanceTimersByTimeAsync(300); + await flushPromises(); + expect(panel.state.customState).toMatchObject({ isCliReady: true }); + expect(terminal!.lastMeaningfulEventAt).not.toBe(beforeReady); + expect(hasNewOutput()).toBe(true); + + await vi.advanceTimersByTimeAsync(1_000); + expect(hasNewOutput()).toBe(false); + terminal!.pty.emitData('three'); + expect(hasNewOutput()).toBe(true); + terminal!.pty.emitExit({ exitCode: 0 }); + expect(hasNewOutput()).toBe(false); + disposeFlowControlRecord(terminal!.flowControl); + }); + + it('AC4 persists exited activity without an intermediate semantic idle and preserves the legacy idle edge', () => { + vi.useFakeTimers(); + const eventSink = installRuntime(5_000); + const panel = createPanel({ isCliPanel: true, isCliReady: true }); + vi.mocked(panelManager.getPanel).mockReturnValue(panel); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const terminal = createTerminal({ + outputBuffer: '', + activityStatus: 'active', + agentActivity: 'active', + }); + manager.terminals.set(terminal.panelId, terminal); + manager.setupTerminalHandlers(terminal); + + terminal.pty.emitExit({ exitCode: 7, signal: 15 }); + + expect(terminal.agentActivity).toBe('exited'); + expect(terminal.outputGenerationAtQuiescence).toBe(terminal.outputGeneration); + expect(manager.terminals.has(terminal.panelId)).toBe(false); + expect(panel.state.customState).toMatchObject({ + exitedAt: expect.any(String), + exitCode: 7, + exitSignal: 15, + }); + expect(eventSink.send).toHaveBeenCalledWith('panel:activityStatus', expect.objectContaining({ + status: 'idle', + })); + disposeFlowControlRecord(terminal.flowControl); + }); + + it('ignores a late exit callback from a replaced process', () => { + installRuntime(); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const stale = createTerminal({ agentActivity: 'active' }); + const replacement = createTerminal({ agentActivity: 'starting' }); + manager.terminals.set(stale.panelId, stale); + manager.setupTerminalHandlers(stale); + manager.terminals.set(replacement.panelId, replacement); + + stale.pty.emitExit({ exitCode: 9 }); + + expect(manager.terminals.get(stale.panelId)).toBe(replacement); + expect(stale.agentActivity).toBe('active'); + expect(panelManager.updatePanel).not.toHaveBeenCalled(); + disposeFlowControlRecord(stale.flowControl); + disposeFlowControlRecord(replacement.flowControl); + }); + + it('MF-1 ignores late output after removal instead of arming a semantic idle timer', async () => { + vi.useFakeTimers(); + installRuntime(100); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const terminal = createTerminal({ outputBuffer: '' }); + manager.terminals.set(terminal.panelId, terminal); + manager.setupTerminalHandlers(terminal); + manager.terminals.delete(terminal.panelId); + + terminal.pty.emitData('late WSL shutdown output'); + + expect(terminal.agentActivity).toBe('starting'); + expect(terminal.outputGeneration).toBe(0); + expect(terminal.agentIdleTimer).toBeNull(); + + await vi.advanceTimersByTimeAsync(100); + + expect(terminal.agentActivity).toBe('starting'); + disposeFlowControlRecord(terminal.flowControl); + }); + + it('MF-1 ignores late output from a replaced process instead of mutating stale semantic state', async () => { + vi.useFakeTimers(); + installRuntime(100); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const stale = createTerminal({ outputBuffer: '' }); + const replacement = createTerminal({ outputBuffer: '', agentActivity: 'starting' }); + manager.terminals.set(stale.panelId, stale); + manager.setupTerminalHandlers(stale); + manager.terminals.set(replacement.panelId, replacement); + + stale.pty.emitData('late stale output'); + + expect(stale.agentActivity).toBe('starting'); + expect(stale.outputGeneration).toBe(0); + expect(stale.agentIdleTimer).toBeNull(); + + await vi.advanceTimersByTimeAsync(100); + + expect(stale.agentActivity).toBe('starting'); + expect(manager.terminals.get(stale.panelId)).toBe(replacement); + disposeFlowControlRecord(stale.flowControl); + disposeFlowControlRecord(replacement.flowControl); + }); + + it('MF-2 suppresses old exit persistence while respawn replacement initialization is pending', async () => { + installRuntime(); + const panel = createPanel({ cwd: process.cwd() }); + vi.mocked(panelManager.getPanel).mockReturnValue(panel); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const stale = createTerminal({ isPtyHost: true, ptyId: 'pty-restart', agentActivity: 'active' }); + manager.terminals.set(stale.panelId, stale); + manager.setupTerminalHandlers(stale); + let rejectReplacement!: (error: Error) => void; + manager.initializeTerminal = vi.fn().mockImplementation(() => new Promise((_resolve, reject) => { + rejectReplacement = reject; + })); + + const respawn = manager.respawnAll(); + stale.pty.emitExit({ exitCode: 1, signal: 15 }); + await flushPromises(); + + expect(panel.state.customState).not.toMatchObject({ exitedAt: expect.any(String) }); + expect(panel.state.customState).not.toHaveProperty('exitCode'); + expect(panel.state.customState).not.toHaveProperty('exitSignal'); + rejectReplacement(new Error('replacement spawn failed')); + await respawn; + expect(panel.state.customState).not.toMatchObject({ exitedAt: expect.any(String) }); + disposeFlowControlRecord(stale.flowControl); + }); + + it('MF-2 suppresses old exit persistence on respawnAll when the panel no longer exists', async () => { + installRuntime(); + vi.mocked(panelManager.getPanel).mockReturnValue(undefined); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const missing = createTerminal({ isPtyHost: true, ptyId: 'pty-missing', agentActivity: 'active' }); + manager.terminals.set(missing.panelId, missing); + manager.setupTerminalHandlers(missing); + + await manager.respawnAll(); + missing.pty.emitExit({ exitCode: 1, signal: 15 }); + await flushPromises(); + + expect(panelManager.updatePanel).not.toHaveBeenCalled(); + disposeFlowControlRecord(missing.flowControl); + }); + + it('re-initializing a previously exited panel clears exit facts and starts semantic activity', async () => { + installRuntime(); + const panel = createPanel({ + exitedAt: '2025-12-31T00:00:00.000Z', + exitCode: 1, + exitSignal: 9, + }); + vi.mocked(panelManager.getPanel).mockReturnValue(panel); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + const spawnedPty = createTerminal({ outputBuffer: '' }).pty; + ptySpawn.mockReturnValue(spawnedPty); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + + await manager.initializeTerminal(panel, process.cwd()); + + expect(panel.state.customState).toMatchObject({ + exitedAt: undefined, + exitCode: undefined, + exitSignal: undefined, + }); + expect(manager.getTerminalSnapshot(panel.id)?.agentActivity).toBe('starting'); + manager.destroyAllTerminals(); + }); + + it('clears semantic timers armed by output across all six terminal-map removal paths', async () => { + vi.useFakeTimers(); + installRuntime(60_000); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + + const naturalManager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const natural = createTerminal({ outputBuffer: '' }); + vi.mocked(panelManager.getPanel).mockReturnValue(createPanel()); + naturalManager.terminals.set(natural.panelId, natural); + naturalManager.setupTerminalHandlers(natural); + natural.pty.emitData('working'); + expect(natural.agentIdleTimer).not.toBeNull(); + natural.pty.emitExit({ exitCode: 0 }); + expect(natural.agentIdleTimer).toBeNull(); + + const writeManager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const failedWrite = createTerminal({ outputBuffer: '' }); + failedWrite.pty.write.mockImplementation(() => { throw new Error('dead'); }); + writeManager.terminals.set(failedWrite.panelId, failedWrite); + writeManager.setupTerminalHandlers(failedWrite); + failedWrite.pty.emitData('working'); + expect(failedWrite.agentIdleTimer).not.toBeNull(); + writeManager.writeToTerminal(failedWrite.panelId, 'x'); + expect(failedWrite.agentIdleTimer).toBeNull(); + + const destroyManager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const destroyed = createTerminal({ outputBuffer: '' }); + destroyManager.terminals.set(destroyed.panelId, destroyed); + destroyManager.setupTerminalHandlers(destroyed); + destroyed.pty.emitData('working'); + expect(destroyed.agentIdleTimer).not.toBeNull(); + destroyManager.destroyTerminal(destroyed.panelId); + expect(destroyed.agentIdleTimer).toBeNull(); + + const missingManager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const missing = createTerminal({ isPtyHost: true, ptyId: 'pty-1', outputBuffer: '' }); + vi.mocked(panelManager.getPanel).mockReturnValue(undefined); + missingManager.terminals.set(missing.panelId, missing); + missingManager.setupTerminalHandlers(missing); + missing.pty.emitData('working'); + expect(missing.agentIdleTimer).not.toBeNull(); + await missingManager.respawnAll(); + expect(missing.agentIdleTimer).toBeNull(); + + const staleManager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const stale = createTerminal({ isPtyHost: true, ptyId: 'pty-2', outputBuffer: '' }); + vi.mocked(panelManager.getPanel).mockReturnValue(createPanel({ cwd: process.cwd() })); + staleManager.initializeTerminal = vi.fn().mockResolvedValue(undefined); + staleManager.terminals.set(stale.panelId, stale); + staleManager.setupTerminalHandlers(stale); + stale.pty.emitData('working'); + expect(stale.agentIdleTimer).not.toBeNull(); + await staleManager.respawnAll(); + expect(stale.agentIdleTimer).toBeNull(); + + const allManager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const all = createTerminal({ outputBuffer: '' }); + const shutdownPanel = createPanel(); + vi.mocked(panelManager.getPanel).mockReturnValue(shutdownPanel); + allManager.terminals.set(all.panelId, all); + allManager.setupTerminalHandlers(all); + all.pty.emitData('working'); + expect(all.agentIdleTimer).not.toBeNull(); + all.pty.kill.mockImplementation(() => all.pty.emitExit({ exitCode: 0 })); + allManager.destroyAllTerminals(); + expect(all.agentIdleTimer).toBeNull(); + expect(shutdownPanel.state.customState).not.toMatchObject({ exitedAt: expect.any(String) }); + + for (const terminal of [natural, failedWrite, destroyed, missing, stale, all]) { + disposeFlowControlRecord(terminal.flowControl); + } + }); + + it('destroyTerminal persists exited state for WSL and ignores late shutdown output while merging deferred exit facts', async () => { + vi.useFakeTimers(); + installRuntime(60_000); + const panel = createPanel(); + vi.mocked(panelManager.getPanel).mockReturnValue(panel); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const terminal = createTerminal({ isWSL: true, agentActivity: 'active', outputBuffer: '' }); + manager.terminals.set(terminal.panelId, terminal); + manager.setupTerminalHandlers(terminal); + + manager.destroyTerminal(terminal.panelId); + terminal.pty.emitData('logout'); + + expect(panel.state.customState).toMatchObject({ exitedAt: expect.any(String) }); + expect(terminal.agentActivity).toBe('exited'); + expect(terminal.agentIdleTimer).toBeNull(); + expect(manager.terminals.has(terminal.panelId)).toBe(false); + + await vi.advanceTimersByTimeAsync(500); + expect(terminal.pty.kill).toHaveBeenCalled(); + terminal.pty.emitExit({ exitCode: 0, signal: 15 }); + await flushPromises(); + expect(panel.state.customState).toMatchObject({ + exitCode: 0, + exitSignal: 15, + }); + disposeFlowControlRecord(terminal.flowControl); + }); +}); + describe('TerminalPanelManager hidden output delivery', () => { afterEach(() => { resetPaneRuntimeForTests(); diff --git a/main/src/services/terminalPanelManager.ts b/main/src/services/terminalPanelManager.ts index 475480d1..ce0e4d30 100644 --- a/main/src/services/terminalPanelManager.ts +++ b/main/src/services/terminalPanelManager.ts @@ -18,6 +18,7 @@ import { onPtyBytes as flowControlOnPtyBytes, } from '../ptyHost/flowControl'; import { TerminalStateEmulator } from './terminalStateEmulator'; +import type { RunpaneAgentActivity } from '../../../shared/types/runpaneOrchestration'; const OUTPUT_BATCH_INTERVAL = 32; // ms (~30fps) — wider window reduces TUI flicker const OUTPUT_BATCH_INTERVAL_HIDDEN = 250; // ms — background / hidden cadence to cut IPC wake-up cost @@ -25,6 +26,7 @@ const OUTPUT_BATCH_SIZE = 131072; // 128KB — timer-based flush preferred; size const OUTPUT_BATCH_SIZE_HIDDEN = 80_000; // 80KB — cap hidden flush size to avoid foreground backpressure churn const MAX_CONCURRENT_SPAWNS = 3; const IDLE_THRESHOLD_MS = 30_000; // 30s — mark panel idle after no PTY output +const DEFAULT_AGENT_IDLE_DEBOUNCE_MS = 60_000; const MAX_SCROLLBACK_BUFFER_SIZE = 500_000; // 500KB of normal shell history const MAX_ALTERNATE_SCREEN_BUFFER_SIZE = 100_000; // 100KB of recent TUI redraw state const MIN_PTY_COLS = 20; @@ -45,6 +47,12 @@ function isValidUuid(value: unknown): value is string { return typeof value === 'string' && UUID_PATTERN.test(value); } +export function normalizeAgentIdleDebounceMs(value: unknown): number { + return typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value) && value > 0 + ? value + : DEFAULT_AGENT_IDLE_DEBOUNCE_MS; +} + export interface TerminalPanelSnapshot { initialized: true; scrollbackBuffer: string; @@ -59,6 +67,11 @@ export interface TerminalPanelSnapshot { isCliReady?: boolean; agentType?: CliAgentType; agentSessionId?: string; + agentActivity: RunpaneAgentActivity; + terminalReady: boolean; + lastMeaningfulEventAt: string; + outputGeneration: number; + outputGenerationAtQuiescence: number; } /** @@ -195,6 +208,12 @@ interface TerminalProcess { isAlternateScreen: boolean; activityStatus: 'active' | 'idle'; idleTimer: ReturnType | null; + agentActivity: RunpaneAgentActivity; + agentIdleTimer: ReturnType | null; + lastMeaningfulEventAt: string; + outputGenerationAtQuiescence: number; + exitEventHandled: boolean; + suppressSemanticExitPersistence: boolean; // DEC Mode 2026 synchronized-output block tracking — persists across chunks inSyncBlock: boolean; codexAgentSessionId?: string; @@ -912,7 +931,16 @@ export class TerminalPanelManager { }); } + const freshCustomState = { + ...((panel.state.customState || {}) as TerminalPanelState), + exitedAt: undefined, + exitCode: undefined, + exitSignal: undefined, + } satisfies TerminalPanelState; + panel.state = { ...panel.state, customState: freshCustomState }; + // Create terminal process object + const initializedAt = new Date(); const terminalProcess: TerminalProcess = { pty: ptyProcess, ptyId: ptyHostId, @@ -924,7 +952,7 @@ export class TerminalPanelManager { screenEmulator: new TerminalStateEmulator(spawnCols, spawnRows), commandHistory: [], currentCommand: '', - lastActivity: new Date(), + lastActivity: initializedAt, outputGeneration: 0, isWSL: !!(wslContext && process.platform === 'win32'), // Capture wslContext so `respawnAll` can re-inject the same WSLENV / @@ -938,6 +966,12 @@ export class TerminalPanelManager { isAlternateScreen: false, activityStatus: 'idle', idleTimer: null, + agentActivity: 'starting', + agentIdleTimer: null, + lastMeaningfulEventAt: initializedAt.toISOString(), + outputGenerationAtQuiescence: 0, + exitEventHandled: false, + suppressSemanticExitPersistence: false, inSyncBlock: false, codexResumeOutputBuffer: '' }; @@ -1005,6 +1039,11 @@ export class TerminalPanelManager { cliReadySignaled = true; if (onCliOutput) onCliOutput.dispose(); + const currentTerminal = this.terminals.get(panelId); + if (currentTerminal === terminalProcess) { + this.stampMeaningfulEvent(currentTerminal); + } + // Persist isCliReady on panel state (best-effort, fire-and-forget) const currentPanel = panelManager.getPanel(panelId); if (currentPanel) { @@ -1123,9 +1162,27 @@ export class TerminalPanelManager { terminal.pty.onData((data: string) => { // Update last activity const outputAt = new Date(); - terminal.lastActivity = outputAt; - terminal.lastOutputAt = outputAt; - terminal.outputGeneration += 1; + if (this.terminals.get(terminal.panelId) === terminal) { + terminal.lastActivity = outputAt; + terminal.lastOutputAt = outputAt; + terminal.outputGeneration += 1; + + if (terminal.agentActivity !== 'active') { + terminal.agentActivity = 'active'; + this.stampMeaningfulEvent(terminal, outputAt); + } + this.clearAgentIdleTimer(terminal); + const agentIdleDebounceMs = normalizeAgentIdleDebounceMs( + getRuntimeConfigManager().getConfig().agentIdleDebounceMs, + ); + terminal.agentIdleTimer = setTimeout(() => { + if (this.terminals.get(terminal.panelId) !== terminal) return; + terminal.agentActivity = 'idle'; + terminal.outputGenerationAtQuiescence = terminal.outputGeneration; + terminal.agentIdleTimer = null; + this.stampMeaningfulEvent(terminal); + }, agentIdleDebounceMs); + } // Activity status transition: mark active on first byte after idle if (terminal.activityStatus !== 'active') { @@ -1225,6 +1282,17 @@ export class TerminalPanelManager { // Handle terminal exit terminal.pty.onExit((exitCode: { exitCode: number; signal?: number }) => { + const currentTerminal = this.terminals.get(terminal.panelId); + if (currentTerminal && currentTerminal !== terminal) return; + if (terminal.exitEventHandled) return; + terminal.exitEventHandled = true; + if (!terminal.suppressSemanticExitPersistence) { + this.markTerminalExited(terminal, { + exitCode: exitCode.exitCode, + exitSignal: exitCode.signal, + }); + } + // Clear idle timer and mark as idle on exit if (terminal.idleTimer) { clearTimeout(terminal.idleTimer); @@ -1311,6 +1379,7 @@ export class TerminalPanelManager { } catch (err) { // PTY may have exited between the map lookup and the write call console.warn(`[TerminalPanelManager] Failed to write to terminal ${panelId}:`, err); + this.markTerminalExited(terminal); this.terminals.delete(panelId); this.visibleViewersByPanel.delete(panelId); return; @@ -1544,6 +1613,11 @@ export class TerminalPanelManager { screenText: terminal.screenEmulator?.getScreenText(), isAlternateScreen: terminal.screenEmulator?.isAlternateScreen ?? terminal.isAlternateScreen, activityStatus: terminal.activityStatus, + agentActivity: terminal.agentActivity, + terminalReady: customState.isCliPanel ? customState.isCliReady === true : true, + lastMeaningfulEventAt: terminal.lastMeaningfulEventAt, + outputGeneration: terminal.outputGeneration, + outputGenerationAtQuiescence: terminal.outputGenerationAtQuiescence, lastActivityTime: terminal.lastActivity.toISOString(), currentCommand: terminal.currentCommand, isCliPanel: customState.isCliPanel, @@ -1583,6 +1657,51 @@ export class TerminalPanelManager { }); } + private stampMeaningfulEvent(terminal: TerminalProcess, at: Date = new Date()): void { + terminal.lastMeaningfulEventAt = at.toISOString(); + } + + private clearAgentIdleTimer(terminal: TerminalProcess): void { + if (terminal.agentIdleTimer) { + clearTimeout(terminal.agentIdleTimer); + terminal.agentIdleTimer = null; + } + } + + private persistExitFacts( + panelId: string, + facts: { exitedAt: string; exitCode?: number; exitSignal?: number }, + ): void { + const panel = panelManager.getPanel(panelId); + if (!panel) return; + const customState = (panel.state.customState || {}) as TerminalPanelState; + const state = { + ...panel.state, + customState: { + ...customState, + exitedAt: customState.exitedAt ?? facts.exitedAt, + exitCode: facts.exitCode ?? customState.exitCode, + exitSignal: facts.exitSignal ?? customState.exitSignal, + } satisfies TerminalPanelState, + }; + panel.state = state; + panelManager.updatePanel(panelId, { state }).catch((error: unknown) => { + console.warn(`[TerminalPanelManager] Failed to persist exit facts for panel ${panelId}:`, error); + }); + } + + private markTerminalExited( + terminal: TerminalProcess, + facts: { exitCode?: number; exitSignal?: number } = {}, + ): void { + this.clearAgentIdleTimer(terminal); + terminal.agentActivity = 'exited'; + terminal.outputGenerationAtQuiescence = terminal.outputGeneration; + const exitedAt = new Date(); + this.stampMeaningfulEvent(terminal, exitedAt); + this.persistExitFacts(terminal.panelId, { exitedAt: exitedAt.toISOString(), ...facts }); + } + destroyTerminal(panelId: string): void { const terminal = this.terminals.get(panelId); if (!terminal) { @@ -1602,6 +1721,7 @@ export class TerminalPanelManager { clearTimeout(terminal.idleTimer); terminal.idleTimer = null; } + this.markTerminalExited(terminal); this.flushOutputBuffer(terminal); terminal.screenEmulator?.dispose(); @@ -1702,7 +1822,9 @@ export class TerminalPanelManager { const panel = panelManager.getPanel(panelId); if (!panel) { console.warn(`[ptyHost] respawnAll: panel ${panelId} no longer exists, skipping`); + terminal.suppressSemanticExitPersistence = true; terminal.screenEmulator?.dispose(); + this.clearAgentIdleTimer(terminal); this.terminals.delete(panelId); this.visibleViewersByPanel.delete(panelId); continue; @@ -1739,6 +1861,8 @@ export class TerminalPanelManager { clearTimeout(terminal.idleTimer); terminal.idleTimer = null; } + this.clearAgentIdleTimer(terminal); + terminal.suppressSemanticExitPersistence = true; terminal.screenEmulator?.dispose(); this.terminals.delete(panelId); this.visibleViewersByPanel.delete(panelId); @@ -1849,9 +1973,11 @@ export class TerminalPanelManager { clearTimeout(terminal.idleTimer); terminal.idleTimer = null; } + this.clearAgentIdleTimer(terminal); this.flushOutputBuffer(terminal); terminal.screenEmulator?.dispose(); + terminal.suppressSemanticExitPersistence = true; terminal.pty.kill(); } catch (error) { console.error(`[TerminalPanelManager] Error killing terminal ${panelId}:`, error); diff --git a/main/src/types/config.ts b/main/src/types/config.ts index 2b93aa00..fe7c99d0 100644 --- a/main/src/types/config.ts +++ b/main/src/types/config.ts @@ -77,6 +77,8 @@ export interface AppConfig { autoStartOnBoot?: boolean; // Prevent idle sleep while Pane sessions are active keepAwakeWhileSessionsActive?: boolean; + // Silence interval before agent activity is considered idle + agentIdleDebounceMs?: number; // Stravu MCP integration stravuApiKey?: string; stravuServerUrl?: string; @@ -163,6 +165,7 @@ export interface UpdateConfigRequest { autoCheckUpdates?: boolean; autoStartOnBoot?: boolean; keepAwakeWhileSessionsActive?: boolean; + agentIdleDebounceMs?: number; stravuApiKey?: string; stravuServerUrl?: string; theme?: 'light' | 'light-rounded' | 'dark' | 'oled' | 'dusk' | 'dusk-oled' | 'forge' | 'ember' | 'aurora' | 'night-owl' | 'night-owl-oled' | 'terracotta'; diff --git a/packages/runpane-py/src/runpane/generated_contract.py b/packages/runpane-py/src/runpane/generated_contract.py index 68912c35..bdf57806 100644 --- a/packages/runpane-py/src/runpane/generated_contract.py +++ b/packages/runpane-py/src/runpane/generated_contract.py @@ -1,4 +1,4 @@ # Generated by scripts/generate-runpane-contract.js. Do not edit by hand. import json -RUNPANE_CONTRACT = json.loads("{\n \"$schema\": \"./schema.json\",\n \"schemaVersion\": 1,\n \"name\": \"runpane\",\n \"description\": \"Thin installer and local control CLI for Pane\",\n \"packageInstallPolicy\": [\n \"The packages must not download, install, or configure Pane during package installation.\",\n \"Work starts only when a user runs `runpane ...`.\"\n ],\n \"compatibility\": {\n \"node\": \">=18.17.0\",\n \"python\": \">=3.8\"\n },\n \"terminology\": {\n \"repo\": \"Saved Pane repository/project record\",\n \"pane\": \"User-visible Pane session\",\n \"tool\": \"Terminal-backed tab\",\n \"agent\": \"Built-in agent command template\"\n },\n \"defaults\": {\n \"target\": \"client\",\n \"paneVersion\": \"latest\",\n \"channel\": \"stable\",\n \"format\": \"auto\",\n \"dryRun\": false,\n \"yes\": false,\n \"verbose\": false\n },\n \"enums\": {\n \"installTargets\": [\n \"client\",\n \"daemon\"\n ],\n \"artifactFormats\": [\n \"auto\",\n \"appimage\",\n \"deb\",\n \"dmg\",\n \"zip\",\n \"exe\"\n ],\n \"channels\": [\n \"stable\",\n \"nightly\"\n ],\n \"agents\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"agentTemplates\": {\n \"codex\": {\n \"title\": \"Codex\",\n \"command\": \"codex --yolo\",\n \"description\": \"Open a Codex terminal tab and allow the initial input to drive the agent.\"\n },\n \"claude\": {\n \"title\": \"Claude Code\",\n \"command\": \"claude --dangerously-skip-permissions\",\n \"description\": \"Open a Claude Code terminal tab and allow the initial input to drive the agent.\"\n }\n },\n \"commands\": [\n {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"usage\": [\n \"runpane help [command]\"\n ]\n },\n {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"usage\": [\n \"runpane setup\"\n ],\n \"interactiveEntrypoint\": true\n },\n {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"usage\": [\n \"runpane install [client|daemon] [options]\"\n ],\n \"defaultTarget\": \"client\",\n \"targets\": [\n \"client\",\n \"daemon\"\n ],\n \"unknownDaemonFlagsForwarded\": true\n },\n {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"usage\": [\n \"runpane update [options]\"\n ],\n \"target\": \"client\"\n },\n {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"usage\": [\n \"runpane version\",\n \"runpane --version\"\n ]\n },\n {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"usage\": [\n \"runpane doctor [--json] [--pane-dir ] [--pane-path ] [--format ] [--verbose]\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"usage\": [\n \"runpane agents doctor --agent [--repo ] [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"usage\": [\n \"runpane agent-context [--json]\",\n \"runpane agent-context --command [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"usage\": [\n \"runpane repos list [--json] [--pane-dir ]\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"usage\": [\n \"runpane repos add --path [--name ] [--json] [--yes]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions in a saved repository.\",\n \"usage\": [\n \"runpane panes list [--repo ] [--json]\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"usage\": [\n \"runpane panes create --repo --name --agent [--source user|agent] [--focus|--no-focus] [options]\",\n \"runpane panes create --from-json [--yes] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"usage\": [\n \"runpane panes archive --pane [--source user|agent] [--force] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes pin --pane --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes unpin --pane --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"usage\": [\n \"runpane panels create --pane --agent [--source user|agent] [--focus|--no-focus] [--wait-ready] --yes [--json]\",\n \"runpane panels create --pane --tool-command [--title ] [--focus|--no-focus] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"usage\": [\n \"runpane panels list --pane <pane-id> [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"usage\": [\n \"runpane panels output --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"usage\": [\n \"runpane panels screen --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"usage\": [\n \"runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"usage\": [\n \"runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"usage\": [\n \"runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"usage\": [\n \"runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ]\n }\n ],\n \"flags\": {\n \"wrapper\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"description\": \"Pane release to install or inspect.\"\n },\n {\n \"name\": \"--download-dir\",\n \"value\": \"<path>\",\n \"description\": \"Directory for downloaded Pane release artifacts.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"description\": \"Use or inspect an existing Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"description\": \"Pane release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"description\": \"Print the plan without downloading or installing.\"\n },\n {\n \"name\": \"--yes\",\n \"aliases\": [\n \"-y\"\n ],\n \"description\": \"Skip interactive prompts where possible.\"\n },\n {\n \"name\": \"--verbose\",\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"remoteValue\": [\n {\n \"name\": \"--label\",\n \"value\": \"<name>\"\n },\n {\n \"name\": \"--prefer-tunnel\",\n \"value\": \"<tailscale|ssh|manual|auto>\"\n },\n {\n \"name\": \"--channel\",\n \"value\": \"<stable|nightly>\"\n },\n {\n \"name\": \"--base-url\",\n \"value\": \"<url>\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\"\n },\n {\n \"name\": \"--listen-port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--repo-ref\",\n \"value\": \"<ref>\"\n }\n ],\n \"remoteBoolean\": [\n {\n \"name\": \"--auto-listen-port\"\n },\n {\n \"name\": \"--interactive-tailscale-setup\"\n },\n {\n \"name\": \"--no-install-service\"\n },\n {\n \"name\": \"--no-tailscale-serve\"\n },\n {\n \"name\": \"--print-only\"\n }\n ],\n \"localValue\": [\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"description\": \"Connect to a Pane daemon using this Pane data directory.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"description\": \"Pane/session id to inspect.\"\n },\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"description\": \"Tool panel id to inspect or control.\"\n },\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"description\": \"Name for the registered repository or created pane/session.\"\n },\n {\n \"name\": \"--worktree-name\",\n \"value\": \"<name>\",\n \"description\": \"Worktree name to request. Defaults to --name.\"\n },\n {\n \"name\": \"--base-branch\",\n \"value\": \"<branch>\",\n \"description\": \"Base branch for the created worktree.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"description\": \"Terminal tab title. Defaults to the selected agent title or Terminal.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"aliases\": [\n \"--prompt\"\n ],\n \"description\": \"Text to send to the terminal after the command is ready. --prompt is an alias.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"description\": \"Read a full panes.create request JSON payload from a file or stdin.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Maximum time to wait for each pane creation job.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Readiness wait timeout for panes create --wait-ready.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"description\": \"Accepted for forward compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"description\": \"Maximum recent output lines or records to read.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"description\": \"Panel wait condition.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"description\": \"Text to wait for with panels wait --for text.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Polling interval for panels wait.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"description\": \"Text bytes to send to a terminal panel.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read panel input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"description\": \"Identify whether a local-control mutation is user-initiated or agent/orchestrator-initiated.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"description\": \"Composer submit key sequence strategy for panels submit-composer.\"\n }\n ],\n \"localBoolean\": [\n {\n \"name\": \"--json\",\n \"description\": \"Print machine-readable JSON output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"description\": \"Wait for created terminal panels to be ready before returning.\"\n },\n {\n \"name\": \"--no-focus\",\n \"description\": \"Create the pane or panel in the background without changing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"description\": \"Explicitly focus the created pane or panel.\"\n },\n {\n \"name\": \"--pinned\",\n \"description\": \"Create the pane already pinned (the UI's favorite/pin star).\"\n },\n {\n \"name\": \"--force\",\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n }\n ]\n },\n \"help\": {\n \"npm\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z> Pane release to install\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path> Use an existing Pane executable\",\n \" --dry-run Print the plan without downloading\",\n \" --yes Skip interactive prompts where possible\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [options]\",\n \"\",\n \"Updates Pane using the same artifact selection as \\\"runpane install client\\\".\",\n \"\",\n \"Options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json Print a machine-readable environment report\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --pane-path <path> Inspect a specific Pane executable\",\n \" --format <format> Release artifact format to inspect\",\n \" --verbose Print extra diagnostics\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json Print machine-readable output\",\n \" --pane-dir <path> Connect to a specific Pane data directory\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path> Existing git repository path\",\n \" --name <name> Saved repository name; defaults to the directory name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without adding the repo\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to archive\",\n \" --source <user|agent> Mark mutation source\",\n \" --force Archive even if the pane has uncommitted, untracked, or unpushed changes\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to pin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without pinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to unpin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without unpinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads bounded recent terminal output from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Tool panel id\",\n \" --limit <count> Maximum recent output lines or records to read\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text bytes to send\",\n \" --input-file <path|-> Read input from a file or stdin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command> Print the full definition for one runpane command\",\n \" --json Print machine-readable output\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n },\n \"pip\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" pipx run runpane install client\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \" uvx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [--version <latest|vX.Y.Z>] [--dry-run] [--yes]\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\",\n \" --pane-path <path>\",\n \" --format <format>\",\n \" --verbose\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path>\",\n \" --name <name>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --source <user|agent>\",\n \" --force\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads recent terminal output records from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --limit <count>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --text <text>\",\n \" --input-file <path|->\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command>\",\n \" --json\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" python -m runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n }\n },\n \"docs\": {\n \"maintainerRules\": [\n \"Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\",\n \"Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\",\n \"The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\",\n \"Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\",\n \"Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\",\n \"`pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\",\n \"Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\"\n ],\n \"recommendedQuickStarts\": [\n \"npx --yes runpane@latest\",\n \"npm i -g runpane && runpane setup\",\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"npmCommands\": [\n \"npx --yes runpane@latest\",\n \"npx --yes runpane@latest setup\",\n \"npx --yes runpane@latest install client\",\n \"npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \"pnpm dlx runpane@latest\",\n \"pnpm dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"npm i -g runpane && runpane\",\n \"npm i -g runpane && runpane setup\",\n \"npm i -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"pnpm add -g runpane && runpane\",\n \"pnpm add -g runpane && runpane setup\",\n \"pnpm add -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"yarn dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"bunx runpane@latest install daemon --label \\\"My Server\\\"\"\n ],\n \"pythonCommands\": [\n \"pipx run runpane\",\n \"pipx run runpane setup\",\n \"python -m pip install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"python -m runpane setup\",\n \"runpane install daemon --label \\\"My Server\\\"\",\n \"pipx install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"uvx runpane@latest\",\n \"uvx runpane@latest setup\",\n \"uvx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"python -m runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"packageManagerNotes\": [\n \"Use `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\",\n \"Do not document `pnpm install runpane` as the public CLI install path.\"\n ],\n \"commandUsages\": [\n \"runpane\",\n \"runpane setup\",\n \"runpane install\",\n \"runpane install client\",\n \"runpane install daemon\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane doctor --json\",\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\",\n \"runpane repos list --json\",\n \"runpane repos add --path /path/to/repo --name Pane --yes --json\",\n \"runpane panes list --repo active --json\",\n \"runpane panes create --repo active --name issue-252 --agent <agent> --prompt \\\"Kick off the discussion skill for issue 252\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panels list --pane <pane-id> --json\",\n \"runpane panels output --panel <panel-id> --limit 200 --json\",\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes --json\",\n \"runpane help\",\n \"runpane <command> --help\"\n ],\n \"commandDescriptions\": [\n \"`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. In non-interactive shells or CI, both forms must print help, common commands, and agent discovery hints, then exit successfully instead of waiting for input.\",\n \"`runpane install` is an alias for `runpane install client`.\",\n \"`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\",\n \"`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\",\n \"The wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\",\n \"`runpane update` uses the same release resolution and installer path as `install client`.\",\n \"`runpane version` prints only wrapper package metadata and does not contact, launch, or focus the Pane app or daemon.\",\n \"`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, daemon reachability, and remote-daemon hints. Add `--json` for a machine-readable report that agents should run before mutating Pane state. Installed app version detection must be best-effort and must not launch, focus, or configure Pane; macOS wrappers read app bundle metadata instead of executing `Pane --version`.\",\n \"`runpane agent-context` prints a brief, token-efficient command schema for coding agents without connecting to the Pane daemon.\",\n \"`runpane agent-context --command \\\"panes create\\\"` prints the detailed definition for one command. Add `--json` for machine-readable output.\",\n \"`runpane repos list` connects to the running local Pane daemon and prints saved repository records.\",\n \"`runpane repos add` registers an existing git repository with the running local Pane daemon. It does not create directories or initialize git repositories by default.\",\n \"`runpane panes list` lists Pane sessions, optionally scoped to one saved repository.\",\n \"`runpane panes create` connects to the running local Pane daemon, resolves the requested saved base repository, creates user-visible Pane sessions backed by Pane-managed worktrees/branches, opens terminal-backed tool tabs, and optionally sends initial input to the started tool. Built-in agent panes and `--source agent` default to background/no-focus unless `--focus` is passed.\",\n \"For `panes create --wait-ready`, `initialInput.verifiedSubmitted: true` is reported only after argument attachment or composer-clear plus activity evidence. Routing input does not by itself verify submission.\",\n \"`runpane panes archive` archives a Pane exactly like the UI Archive action, including removal of its Pane-managed git worktree, and refuses (unless `--force`) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes. It waits for worktree removal to finish before returning and reports the outcome in `worktreeCleanup`.\",\n \"`runpane panels list` lists tool panels inside one Pane session.\",\n \"`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.\",\n \"`runpane panels input` sends exact input bytes to one terminal panel. Prefer `--input-file` for newlines, Ctrl-C, quotes, or shell-sensitive text.\",\n \"`runpane panes create --prompt` is an alias for `--initial-input`; request JSON and daemon payloads should use the canonical `initialInput` field.\",\n \"If composer submission cannot be verified without risking a duplicate, the create item is unsuccessful with `initialInput.staged`, `initialInput.attempts`, `initialInput.blocked.kind: submission_unverified`, and an actionable `nextCommand`. The CLI-facing `--prompt` alias maps to this canonical `initialInput` result.\",\n \"When running from WSL while Pane is installed on Windows, the Linux wrapper may look for a missing `/tmp/pane-daemon.../daemon.sock` or resolve to a Windows shim such as Volta. In that case invoke the Windows wrapper through PowerShell from a Windows cwd, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\"\n ],\n \"wrapperFlagNote\": \"The top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\",\n \"localControlFlagNote\": \"`runpane doctor --json`, `runpane repos list`, `runpane panes list`, `runpane panes create`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues.\",\n \"daemonFlagNote\": \"Unknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\",\n \"downloadAttribution\": [\n \"The npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\",\n \"The PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\",\n \"Wrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\",\n \"If the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\",\n \"Wrappers also emit best-effort lifecycle telemetry to `https://runpane.com/api/runpane/telemetry` for command start/success/failure, download request/success/failure, and GitHub fallback usage.\",\n \"Wrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so distinct wrapper users can be counted with `count(DISTINCT properties.install_id)`.\",\n \"Wrapper telemetry must be disabled in CI and when `RUNPANE_TELEMETRY_DISABLED` is set, and must not include raw paths, labels, prompts, raw error text, or environment values.\"\n ],\n \"publishingCredentials\": [\n \"Local implementation, build, and dry-run validation do not need npm or PyPI API tokens.\",\n \"Release publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\",\n \"Fallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\"\n ]\n },\n \"testFixtures\": {\n \"parserSamples\": [\n [\n \"setup\"\n ],\n [\n \"install\"\n ],\n [\n \"install\",\n \"client\",\n \"--version\",\n \"v2.2.8\",\n \"--format\",\n \"dmg\",\n \"--download-dir\",\n \"/tmp/pane-downloads\",\n \"--dry-run\",\n \"--yes\"\n ],\n [\n \"install\",\n \"daemon\",\n \"--label\",\n \"VM\",\n \"--prefer-tunnel\",\n \"ssh\",\n \"--channel\",\n \"nightly\",\n \"--base-url\",\n \"https://example.test\",\n \"--pane-dir\",\n \"/tmp/pane\",\n \"--listen-port\",\n \"4555\",\n \"--auto-listen-port\",\n \"--print-only\",\n \"--repo-ref\",\n \"main\",\n \"--unknown-future-flag\",\n \"future-value\",\n \"--dry-run\",\n \"--verbose\"\n ],\n [\n \"update\",\n \"--version\",\n \"latest\",\n \"--format\",\n \"appimage\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--dry-run\"\n ],\n [\n \"doctor\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--format\",\n \"zip\",\n \"--verbose\"\n ],\n [\n \"doctor\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"agent-context\"\n ],\n [\n \"agent-context\",\n \"--command\",\n \"panes create\",\n \"--json\"\n ],\n [\n \"repos\",\n \"list\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"repos\",\n \"add\",\n \"--path\",\n \"/tmp/repo\",\n \"--name\",\n \"Repo\",\n \"--dry-run\",\n \"--yes\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"panes\",\n \"list\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--repo\",\n \"active\",\n \"--name\",\n \"issue-252\",\n \"--agent\",\n \"codex\",\n \"--prompt\",\n \"Kick off discussion\",\n \"--source\",\n \"agent\",\n \"--pinned\",\n \"--dry-run\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"archive\",\n \"--pane\",\n \"session-1\",\n \"--force\",\n \"--source\",\n \"agent\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"pin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"unpin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"list\",\n \"--pane\",\n \"session-1\",\n \"--json\"\n ],\n [\n \"panels\",\n \"output\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"200\",\n \"--json\"\n ],\n [\n \"panels\",\n \"input\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"Continue\\\\n\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"--version\"\n ],\n [\n \"agents\",\n \"doctor\",\n \"--agent\",\n \"codex\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--source\",\n \"agent\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"45000\",\n \"--concurrency\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"screen\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"80\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"wait\",\n \"--panel\",\n \"panel-1\",\n \"--for\",\n \"text\",\n \"--contains\",\n \"ready\",\n \"--timeout-ms\",\n \"30000\",\n \"--interval-ms\",\n \"500\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--source\",\n \"agent\",\n \"--no-focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"15000\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--focus\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit-composer\",\n \"--panel\",\n \"panel-1\",\n \"--strategy\",\n \"auto\",\n \"--yes\",\n \"--json\"\n ]\n ],\n \"topLevelHelpIncludes\": [\n \"runpane setup\",\n \"runpane install\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane agent-context\",\n \"runpane repos list\",\n \"runpane repos add\",\n \"runpane panes list\",\n \"runpane panes create\",\n \"runpane panes archive\",\n \"runpane panels create\",\n \"runpane panels list\",\n \"runpane panels output\",\n \"runpane panels input\",\n \"runpane agents doctor\",\n \"runpane panels screen\",\n \"runpane panels submit\",\n \"runpane panels submit-composer\",\n \"runpane panels wait\",\n \"runpane doctor --json\",\n \"runpane agent-context --json\",\n \"Agent discovery:\",\n \"Common commands:\"\n ],\n \"npmHelpIncludes\": [\n \"pnpm dlx runpane@latest\",\n \"npx --yes runpane@latest\"\n ],\n \"pipHelpIncludes\": [\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"installHelpIncludes\": [\n \"--version <latest|vX.Y.Z>\",\n \"--format <auto|appimage|deb|dmg|zip|exe>\",\n \"--download-dir <path>\",\n \"--pane-path <path>\",\n \"--label <name>\",\n \"--prefer-tunnel <tailscale|ssh|manual|auto>\",\n \"--repo-ref <ref>\"\n ]\n },\n \"jsonSchemas\": {\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"doctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"source\",\n \"wrapper\",\n \"release\",\n \"installedPane\",\n \"daemon\",\n \"remoteSetup\",\n \"nextCommands\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"npm\",\n \"pip\"\n ]\n },\n \"wrapper\": {\n \"type\": \"object\",\n \"required\": [\n \"runtime\",\n \"version\",\n \"paneDir\",\n \"endpoint\"\n ],\n \"properties\": {\n \"runtime\": {\n \"enum\": [\n \"node\",\n \"python\"\n ]\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"paneDir\": {\n \"type\": \"string\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"platform\": {\n \"type\": \"object\",\n \"properties\": {\n \"os\": {\n \"type\": \"string\"\n },\n \"arch\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"release\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"installedPane\": {\n \"type\": \"object\",\n \"required\": [\n \"found\"\n ],\n \"properties\": {\n \"found\": {\n \"type\": \"boolean\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"daemon\": {\n \"type\": \"object\",\n \"required\": [\n \"reachable\",\n \"endpoint\"\n ],\n \"properties\": {\n \"reachable\": {\n \"type\": \"boolean\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"result\": {\n \"type\": \"object\"\n },\n \"error\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"remoteSetup\": {\n \"type\": \"object\",\n \"required\": [\n \"ready\",\n \"displayAvailable\",\n \"headlessEnvironmentApplied\",\n \"diagnostics\"\n ],\n \"properties\": {\n \"ready\": {\n \"type\": \"boolean\"\n },\n \"displayAvailable\": {\n \"type\": \"boolean\"\n },\n \"headlessEnvironmentApplied\": {\n \"type\": \"boolean\"\n },\n \"diagnostics\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"severity\",\n \"message\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"PANE_APPIMAGE_FUSE_MISSING\",\n \"PANE_ELECTRON_SANDBOX_ROOT\",\n \"PANE_ELECTRON_SANDBOX_UNAVAILABLE\",\n \"PANE_USER_SERVICE_UNAVAILABLE\"\n ]\n },\n \"severity\": {\n \"enum\": [\n \"warning\",\n \"error\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"recoveryCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommands\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repos\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repos\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"name\",\n \"path\",\n \"active\",\n \"sessionCount\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n },\n \"sessionCount\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"path\"\n ],\n \"properties\": {\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"created\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"created\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"preview\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"path\",\n \"alreadyExists\",\n \"wouldCreate\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"alreadyExists\": {\n \"type\": \"boolean\"\n },\n \"wouldCreate\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"repo\",\n \"panes\"\n ],\n \"properties\": {\n \"repo\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"const\": true\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panes\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"tool\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"worktreeName\": {\n \"type\": \"string\"\n },\n \"baseBranch\": {\n \"type\": \"string\"\n },\n \"sessionPrompt\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"agent\"\n ],\n \"properties\": {\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"command\"\n ],\n \"properties\": {\n \"command\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"timeoutMs\": {\n \"type\": \"number\"\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n },\n \"concurrency\": {\n \"type\": \"number\"\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repo\",\n \"items\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"name\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"sessionId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"type\": \"object\",\n \"required\": [\n \"delivered\",\n \"submitted\",\n \"inputBytes\"\n ],\n \"properties\": {\n \"delivered\": {\n \"type\": \"boolean\"\n },\n \"submitted\": {\n \"type\": \"boolean\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\",\n \"argument\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\",\n \"argument\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"staged\": {\n \"type\": \"boolean\"\n },\n \"attempts\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panes\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"panes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"paneId\",\n \"name\",\n \"status\",\n \"worktreePath\",\n \"repoId\",\n \"panelCount\",\n \"pinned\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"repoId\": {\n \"type\": \"number\"\n },\n \"repoName\": {\n \"type\": \"string\"\n },\n \"panelCount\": {\n \"type\": \"number\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"force\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panePinRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"panePinResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"const\": true\n },\n \"favoritePinnedAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveResult\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"archived\",\n \"forced\",\n \"worktreeCleanup\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"const\": true\n },\n \"forced\": {\n \"type\": \"boolean\"\n },\n \"worktreeCleanup\": {\n \"enum\": [\n \"completed\",\n \"failed\",\n \"timeout\",\n \"not-applicable\"\n ]\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"blocked\",\n \"nextCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"message\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"uncommitted-changes\",\n \"unpushed-commits\",\n \"uncommitted-and-unpushed\",\n \"status-unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panelListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panels\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panels\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"panelId\",\n \"paneId\",\n \"type\",\n \"title\",\n \"active\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"type\": \"string\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"position\": {\n \"type\": \"number\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActiveAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelOutputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"limit\",\n \"returnedCount\",\n \"hasMore\",\n \"outputs\",\n \"text\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"outputs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"type\",\n \"data\",\n \"timestamp\"\n ],\n \"properties\": {\n \"type\": {\n \"type\": \"string\"\n },\n \"data\": {},\n \"timestamp\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextBriefResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"summary\",\n \"rules\",\n \"tools\",\n \"detailCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"brief\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"rules\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"tools\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"arguments\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"detailCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextCommandResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"command\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"command\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"command\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"details\",\n \"arguments\",\n \"examples\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"details\": {\n \"type\": \"string\"\n },\n \"requiresPaneDaemon\": {\n \"type\": \"boolean\"\n },\n \"mutates\": {\n \"type\": \"boolean\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"examples\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"jsonSchemas\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"notes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"panelScreenResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"source\",\n \"limit\",\n \"returnedLineCount\",\n \"hasMore\",\n \"text\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedLineCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"$ref\": \"#/jsonSchemas/paneCreateResult/properties/items/items/oneOf/0/properties/initialInput\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"enter\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"enter\": {\n \"const\": \"cr\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelWaitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\",\n \"screen\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"screen\": {\n \"type\": \"object\",\n \"required\": [\n \"source\",\n \"text\",\n \"hasMore\"\n ],\n \"properties\": {\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentDoctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"agent\",\n \"command\",\n \"available\",\n \"checks\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"environment\": {\n \"enum\": [\n \"wsl\",\n \"windows\",\n \"linux\",\n \"macos\"\n ]\n },\n \"available\": {\n \"type\": \"boolean\"\n },\n \"executablePath\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"checks\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"ok\",\n \"message\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"tool\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"const\": \"terminal\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panelId\",\n \"title\",\n \"active\",\n \"focused\",\n \"tool\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"strategy\": {\n \"enum\": [\n \"auto\",\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"strategy\",\n \"sequenceName\",\n \"verifiedSubmitted\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"agentContext\": {\n \"brief\": {\n \"title\": \"Pane agent context\",\n \"summary\": \"Pane lets a developer manage saved base repositories, user-visible Panes (Pane sessions) for feature/PR work, and terminal-backed panel tabs. A Pane is a visible workspace that normally maps to one Pane-managed git worktree and branch; a panel is a terminal tab inside one Pane and shares that Pane's worktree; an agent is a CLI process running inside a panel.\",\n \"rules\": [\n \"Start with `runpane doctor --json` to understand wrapper, platform, daemon reachability, and the next safe commands before mutating Pane state.\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"Happy path for any user request to use Pane/RunPane: run `runpane doctor --json`, read `runpane agent-context --json`, resolve or add the saved base repo, create the requested visible Pane with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --pinned --no-focus --wait-ready --yes --json` when it should stay visible (or omit `--pinned`, or use `--tool-command <command>` instead of `--agent <agent>`), then validate with `panels wait` or `panels screen`.\",\n \"Treat Pane as the user's visible cockpit for watching/co-driving work. Do not create Panes or panels for private delegation unless the user asked for visible Pane orchestration or the result should appear in the Pane app.\",\n \"Register the saved base repository once with `repos add`; do not register a pre-created worktree as a separate repo unless the user explicitly asks.\",\n \"Use `panes create` for separate visible Panes (Pane sessions) for feature/PR work. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` for reviewer/helper/clean-context tabs that should stay inside an existing Pane and share that Pane's worktree.\",\n \"For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"Skill routing: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for those behaviors, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the workflow map at https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png. For where the local cache and Pane Chat bootstrap live in Pane, reference https://github.com/dcouple/Pane/pull/291. Use GitHub reads against https://github.com/dcouple/skills/tree/main/parsa only to inspect or refresh referenced skill files; do not clone or install it unless the user asks.\",\n \"When an agent creates Panes or panels, pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when declaring a Pane that should remain in the UI's favorite/pin set.\",\n \"Use `runpane agent-context --json` for the full agent-facing CLI context, or `runpane agent-context --command <command> --json` for one detailed command definition.\",\n \"For `agent-context --command`, use canonical spaced names like `panes create`; copied forms like `panes.create` or `runpane panes create` are accepted too.\",\n \"Use `runpane repos list --json` to find the saved repository when unsure after doctor shows the daemon is reachable.\",\n \"If WSL cannot reach Pane or `runpane` resolves to a broken Windows shim, the user may be running Windows Pane; try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`.\",\n \"If the repository exists on disk but is not saved in Pane, use `runpane repos add --path <repo> --yes --json` before creating panes.\",\n \"Use `runpane agents doctor --agent <codex|claude> --repo <selector> --json` when agent availability differs across host, Windows, WSL, or repo environments.\",\n \"Use `runpane panes create --wait-ready` to create Panes and validate initial terminal readiness in one call.\",\n \"Use `runpane panels screen` for compact current state, `panels wait` for readiness/text checks, `panels submit` for ordinary Enter-submitted input, and `panels submit-composer --strategy auto` for agent composers.\",\n \"Use `runpane panels input` only when exact bytes are required, such as Ctrl-C or handcrafted terminal input.\",\n \"After creating Panes or sending terminal input, validate with `panels wait` or bounded `panels screen` before reporting success.\"\n ],\n \"detailCommand\": \"runpane agent-context --command <command> [--json]\",\n \"tools\": [\n {\n \"name\": \"doctor\",\n \"summary\": \"Report wrapper, platform, installed Pane, and daemon reachability before an agent mutates Pane state.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\",\n \"--pane-path <path>\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"arguments\": [\n \"--command <command>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Check whether Codex or Claude is available in the repo environment Pane will use.\",\n \"arguments\": [\n \"--agent <codex|claude>\",\n \"--repo <selector>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"arguments\": [\n \"--path <path>\",\n \"--name <name>\",\n \"--yes\",\n \"--json\",\n \"--dry-run\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--name <name>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--prompt <text>\",\n \"--initial-input-file <path|->\",\n \"--from-json <path|->\",\n \"--source <user|agent>\",\n \"--pinned\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms <ms>\",\n \"--concurrency <count>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--source <user|agent>\",\n \"--force\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--source <user|agent>\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text plus terminal Enter to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer with the correct key sequence, including Ctrl+Enter for Codex.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--strategy <auto|codex-ctrl-enter|enter>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for terminal initialized, ready, idle, or text state with compact output.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--for <condition>\",\n \"--contains <text>\",\n \"--timeout-ms <ms>\",\n \"--interval-ms <ms>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n }\n ]\n },\n \"commands\": {\n \"help\": {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"details\": \"Use this when you need human-readable usage text for a command.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Optional command topic such as \\\"panes create\\\".\"\n }\n ],\n \"examples\": [\n \"runpane help\",\n \"runpane help panes create\"\n ],\n \"notes\": [\n \"Help text is generated from the runpane contract.\"\n ]\n },\n \"setup\": {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"details\": \"Use this for a human-driven Pane setup flow, not for unattended agent orchestration.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [],\n \"examples\": [\n \"runpane setup\"\n ],\n \"notes\": [\n \"In non-interactive shells, setup prints help and exits successfully.\"\n ]\n },\n \"install\": {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"details\": \"Installs or launches Pane release artifacts at command runtime. `install daemon` forwards remote setup flags to Pane.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"target\",\n \"value\": \"<client|daemon>\",\n \"required\": false,\n \"description\": \"Install the desktop client or configure a remote daemon host.\"\n },\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to install.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip interactive prompts where possible.\"\n }\n ],\n \"examples\": [\n \"runpane install client\",\n \"runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"notes\": [\n \"Package install itself is inert; work begins only when runpane is executed.\"\n ]\n },\n \"update\": {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"details\": \"Resolves and installs the selected Pane client release.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to update to.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Print the update plan without downloading.\"\n }\n ],\n \"examples\": [\n \"runpane update\",\n \"runpane update --version latest\"\n ],\n \"notes\": [\n \"Equivalent artifact selection to `runpane install client`.\"\n ]\n },\n \"version\": {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"details\": \"Use this to confirm which wrapper is available. App, daemon, and release diagnostics belong in doctor.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Ignored for version; retained only for parser compatibility.\"\n }\n ],\n \"examples\": [\n \"runpane version\",\n \"runpane --version\"\n ],\n \"notes\": []\n },\n \"doctor\": {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"details\": \"Use this first when an agent needs to understand whether Pane is installed, which wrapper/runtime is running, what daemon endpoint is expected, and whether the running Pane app is reachable. JSON mode should return a report even when the daemon is unreachable.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print a machine-readable environment report.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Inspect a specific Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<format>\",\n \"required\": false,\n \"description\": \"Release artifact format to inspect.\"\n },\n {\n \"name\": \"--verbose\",\n \"required\": false,\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"examples\": [\n \"runpane doctor --json\",\n \"runpane doctor\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ],\n \"notes\": [\n \"Agents should run `runpane doctor --json` before mutating Pane state.\",\n \"The JSON report includes daemon reachability as data; an unreachable daemon is not a reason to skip the rest of the report.\",\n \"Use `runpane agent-context --json` after doctor when full CLI context is needed.\"\n ]\n },\n \"agent-context\": {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"details\": \"Use this first when an agent needs to discover Pane primitives. It is local/offline and does not require a running Pane app.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Print the detailed definition for one command, for example \\\"panes create\\\".\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ],\n \"notes\": [\n \"Default output is brief so AGENTS.md can point here without bloating context.\",\n \"`--command` accepts canonical spaced names and common copied forms, including `panes.create` and `runpane panes create`.\"\n ]\n },\n \"repos list\": {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"details\": \"Use this to find the right Pane-managed repository before creating panes. If the repo is missing, use `repos add` first.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable repository records.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos list --json\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ],\n \"notes\": [\n \"Requires a running Pane app or daemon for the selected Pane data directory.\",\n \"If this fails from WSL with a missing `/tmp/pane-daemon.../daemon.sock`, `volta: command not found`, or a Windows shim error, the user may be running Windows Pane. Retry via `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\",\n \"When using PowerShell from WSL, set a Windows cwd such as `$env:TEMP` or `$env:USERPROFILE` before running runpane to avoid UNC cwd issues.\"\n ]\n },\n \"repos add\": {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"details\": \"Use this when the repository exists on disk but is not saved in Pane yet. It validates the path as a git repository.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"required\": true,\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": false,\n \"description\": \"Saved repository name; defaults to the directory name.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without adding the repo.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos add --path /path/to/repo --yes --json\"\n ],\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ],\n \"notes\": [\n \"It does not create directories or initialize git repositories by default.\",\n \"For a Windows Pane app managing WSL repositories, prefer a saved WSL repo from `repos list` when possible. Adding a brand-new WSL repo from Windows may require WSL-aware path handling.\"\n ]\n },\n \"panes list\": {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"details\": \"Use this after selecting a repository to find existing Pane sessions and their ids before inspecting panels.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panes list --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ],\n \"notes\": [\n \"Without --repo, this lists sessions across saved Pane repositories.\"\n ]\n },\n \"panes create\": {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"details\": \"Use this when the user wants separate visible Panes (Pane sessions) for feature or PR work. Select the saved base repository, choose a built-in agent or custom terminal command, and optionally send initial input after the tool starts. Pane creates and owns a git worktree/branch for each new Pane; do not pre-create a git worktree and register it as a separate repo unless the user explicitly asks. For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": true,\n \"description\": \"Pane/session name.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--prompt\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Alias for --initial-input; sends text after the command is ready.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read a full panes.create request JSON payload.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background/no-focus creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the pane in the background without stealing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created pane.\"\n },\n {\n \"name\": \"--pinned\",\n \"required\": false,\n \"description\": \"Create the pane already pinned (the Pane UI's favorite/pin star); does not imply focus.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait for each created terminal panel to be ready before returning.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness timeout per pane; defaults to 30000.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Accepted for compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n }\n ],\n \"examples\": [\n \"runpane panes create --repo active --name issue-257 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes create --repo active --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ],\n \"notes\": [\n \"At least one of --agent or --tool-command is required unless --from-json is used.\",\n \"`panes create` is for user-visible Pane orchestration, not the agent's default private delegation mechanism.\",\n \"Register the saved base repository once. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` instead when a reviewer/helper should share an existing Pane's worktree.\",\n \"Agent-created Panes should pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when the Pane should remain in the UI's favorite/pin set.\",\n \"The built-in agent templates come from the runpane contract; custom terminal commands can pass agent-specific flags when requested by the user.\",\n \"Use --initial-input-file for multi-line prompts or shell-sensitive initial input.\",\n \"With --wait-ready, verifiedSubmitted is earned from delivery evidence; routing initial input alone does not imply verified submission.\",\n \"If initialInput.blocked.kind is submission_unverified, do not submit again automatically. Inspect initialInput.staged and attempts, then run nextCommand to resolve the ambiguous composer state.\",\n \"When the JSON result includes nextCommand, run it to validate that the terminal produced output before reporting success.\",\n \"Multi-pane requests are created sequentially today. The --concurrency flag is accepted for compatibility, but agents should not rely on parallel creation.\",\n \"For POSIX or WSL command chaining, use a custom terminal command like `bash -lc 'cmd1 && cmd2 && cmd3'`.\",\n \"For Windows PowerShell command chaining, use a custom terminal command like `powershell -NoProfile -Command \\\"cmd1; if ($LASTEXITCODE) { exit $LASTEXITCODE }; cmd2\\\"`.\",\n \"From WSL with Windows Pane, invoke through PowerShell and select the saved WSL repo by name or id, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane panes create --repo \\\"WSL Pane\\\" --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json'`.\",\n \"Use --wait-ready when an agent needs to verify that an agent terminal started instead of only creating a pane.\",\n \"If readiness returns blocked, inspect blocked.suggestedCommand rather than guessing which prompt to answer.\"\n ]\n },\n \"panes archive\": {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"details\": \"Use this to close out a Pane once its PR has merged. Refuses to archive (unless --force) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes, so an agent cannot silently discard work. A merged and pushed branch has no unpushed commits, so it archives cleanly. Waits for worktree removal to finish before returning.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to archive.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; does not change archive safety behavior.\"\n },\n {\n \"name\": \"--force\",\n \"required\": false,\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panes archive --pane <pane-id> --force --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ],\n \"notes\": [\n \"If the result has ok:false with a blocked field, the pane was NOT archived; inspect blocked.code and rerun with --force if discarding the flagged work is intentional.\",\n \"A successful archive waits for the Pane-managed worktree to be removed before returning; check worktreeCleanup in the result for the final outcome.\",\n \"Archiving a main-repo Pane (no Pane-managed worktree) always succeeds immediately since nothing is deleted from disk.\"\n ]\n },\n \"panes pin\": {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to true without reading and toggling it. Repeating the command is idempotent and pinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to pin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes pin --pane <pane-id> --yes --json\",\n \"runpane panes pin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-pinned Pane pinned.\",\n \"Pinning does not focus the Pane.\"\n ]\n },\n \"panes unpin\": {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to false without reading and toggling it. Repeating the command is idempotent and unpinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to unpin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes unpin --pane <pane-id> --yes --json\",\n \"runpane panes unpin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-unpinned Pane unpinned.\",\n \"Unpinning does not focus the Pane.\"\n ]\n },\n \"panels list\": {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"details\": \"Use this to find the terminal panel id to inspect or control after creating or selecting a Pane session.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels list --pane <pane-id> --json\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ],\n \"notes\": [\n \"The ids returned here are stable inputs for `panels output` and `panels input`.\"\n ]\n },\n \"panels output\": {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"details\": \"Use this to inspect recent terminal output from a terminal-backed panel without loading the full history by default. Live terminal scrollback is returned as bounded recent lines; persisted output fallback is returned as bounded records. Terminal control noise is stripped before returning text.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Tool panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum recent output lines or records to read. Defaults to 200.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels output --panel <panel-id> --limit 200 --json\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ],\n \"notes\": [\n \"Default output is bounded to the latest 200 lines or records. Use a larger --limit only when hasMore is true and more history is needed.\",\n \"Use --json when an agent needs timestamps, record types, returnedCount, or hasMore.\",\n \"The output is intended to be agent-readable, but terminal prompts and shell echoes may still appear; use the newest relevant lines before concluding success.\",\n \"Prefer `panels screen` for a smaller current-state read before increasing output limits.\"\n ]\n },\n \"panels input\": {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"details\": \"Use this to answer prompts or continue an agent inside an existing Pane terminal panel. Input is byte-oriented; choose --input-file for exact control over newlines and control characters.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text bytes to send.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read input from a file or stdin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"printf '\\\\003' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"runpane panels input --panel <panel-id> --text \\\"simple text\\\" --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ],\n \"notes\": [\n \"Input is sent exactly as provided. Include a real newline byte when the terminal should receive Enter; across shells, `--input-file` is safer than `--text \\\"...\\\\n\\\"`.\",\n \"Use `--input-file -` or a temp file for multi-line input, quotes, Ctrl-C, or shell-sensitive text.\",\n \"If interrupting a running process, send Ctrl-C first, validate/read output, then send the next command in a separate `panels input` call so bytes are not dropped.\",\n \"After sending input, validate with `runpane panels output --panel <panel-id> --json` before reporting success.\",\n \"Runpane records action metadata and errors for observability, but should not log full input text by default.\",\n \"For ordinary text plus Enter, prefer `panels submit` so the terminal receives a CR Enter byte.\"\n ]\n },\n \"agents doctor\": {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"details\": \"Use this before creating Codex or Claude panes when PATH may differ between macOS/Linux/Windows/WSL or between the wrapper shell and Pane. The check runs through Pane project context, not the wrapper process.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": true,\n \"description\": \"Built-in agent command to diagnose.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Repository selector; defaults to the active Pane repo.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane agents doctor --agent <agent> --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ],\n \"notes\": [\n \"For WSL repos, install the agent inside the WSL distro Pane uses, not only on Windows.\",\n \"This diagnoses built-in agent templates only; custom commands should be validated by creating a pane and reading its screen.\"\n ]\n },\n \"panels screen\": {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"details\": \"Use this for token-safe current terminal state. It prefers active alternate-screen/TUI output, then live scrollback, then persisted output. Default output is bounded to 80 lines.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum lines to return; defaults to 80.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels screen --panel <panel-id> --limit 80 --json\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ],\n \"notes\": [\n \"Use this before `panels output` when an agent only needs the latest visible/current state.\",\n \"If hasMore is true and context is missing, rerun with a larger --limit or use `panels output`.\"\n ]\n },\n \"panels submit\": {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"details\": \"Use this for ordinary interactive submissions. The daemon normalizes a final LF or CRLF to CR, or appends CR if no newline is present. Exact byte workflows remain on `panels input`.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text to submit before Enter.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read text from a file or stdin before Enter.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit --panel <panel-id> --text \\\"2\\\" --yes --json\",\n \"printf \\\"echo hello\\\" | runpane panels submit --panel <panel-id> --input-file - --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ],\n \"notes\": [\n \"The response includes nextCommand for validation. Run it before reporting that the input worked.\",\n \"Use `panels input` for Ctrl-C, escape sequences, or any workflow requiring exact bytes.\"\n ]\n },\n \"panels wait\": {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"details\": \"Use this to validate asynchronous terminal behavior without pulling large scrollback. It returns brief readiness state, blocker hints, a compact screen, and a next command.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"required\": false,\n \"description\": \"Condition to wait for. Defaults to ready for CLI panels and idle otherwise.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text required for --for text; implies --for text when --for is omitted.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Wait timeout; defaults to 30000.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Polling interval; defaults to 500.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json\",\n \"runpane panels wait --panel <panel-id> --contains \\\"Ready\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ],\n \"notes\": [\n \"If blocked is present, do not assume success. Use blocked.suggestedCommand or inspect `panels screen`.\",\n \"The default timeout and screen are intentionally small for agent context safety.\"\n ]\n },\n \"panels create\": {\n \"name\": \"panels create\",\n \"summary\": \"Create a reviewer/helper terminal tab inside an existing Pane.\",\n \"details\": \"Use this to add reviewer, helper, or clean-context tabs to an existing Pane. The new panel shares the existing Pane's worktree and branch; it does not create a separate Pane session. Use `panes create` instead when the user wants a separate visible Pane (Pane session) for feature/PR work. Agent/orchestrator-created panels should pass --source agent or --no-focus so the user stays in the implementation tab. The daemon initializes the terminal immediately and can wait for CLI readiness.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Existing Pane session id to add the panel to.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent command template to launch.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to launch instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"required\": false,\n \"description\": \"Panel title override.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Initial input to send after the tool starts.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the panel in the background without changing active panel.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created panel.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait until the terminal tool is ready.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness wait timeout.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panels create --pane <pane-id> --tool-command <command> --title <title> --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ],\n \"notes\": [\n \"Use this for same-pane reviewer loops after PR creation/testing.\",\n \"Panels share the existing Pane's worktree and branch.\",\n \"`panels create` is for visible helper/reviewer tabs, not the agent's default private delegation mechanism.\",\n \"For agent-created panels, prefer --source agent or --no-focus to avoid stealing focus from the user or implementation tab.\"\n ]\n },\n \"panels submit-composer\": {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"details\": \"Use this after sending text into an agent composer when plain Enter is not the right submit action. Agents should use --strategy auto; Pane owns per-agent submit sequences internally and verifies visible composer state when possible.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"required\": false,\n \"description\": \"Composer submit key sequence strategy.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit-composer --panel <codex-panel-id> --strategy auto --yes --json\",\n \"runpane panels submit-composer --panel <panel-id> --strategy enter --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ],\n \"notes\": [\n \"Use `panels input` or `panels submit` to write prompt text first; this command only submits the current composer.\",\n \"Use --strategy auto for agent workflows; explicit strategies are diagnostic escape hatches.\",\n \"The JSON result includes sequenceName and verifiedSubmitted. If ok is false, follow blocked.suggestedCommand instead of assuming submission happened.\"\n ]\n }\n },\n \"managedBlock\": [\n \"## Pane\",\n \"\",\n \"The developer is using Pane for this repository. Pane can manage saved repositories and create user-visible Panes with terminal-backed tools for planning, discussion, implementation, and review work.\",\n \"\",\n \"This managed guidance was created by [runpane.com](https://runpane.com) for the [Pane repository](https://github.com/dcouple/Pane). Do not delete or overwrite this block; the developer uses it for their workflow and it should remain committed to the main repository unless they explicitly ask to remove it.\",\n \"\",\n \"Pane mental model: a repository is the saved base repo; a Pane is a user-visible feature/PR workspace (Pane session) that normally maps to one Pane-managed git worktree and branch; a panel/tab is a terminal inside one Pane and shares that Pane's worktree; an agent is the CLI process running in a panel.\",\n \"\",\n \"Default happy path when the user asks you to use Pane or RunPane: run `runpane doctor --json`; read `runpane agent-context --json`; resolve the saved base repository with `runpane repos list --json` or add it once with `runpane repos add --path <repo> --yes --json`; create one visible Pane (Pane session) for the requested feature/PR with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json` or the equivalent `--tool-command <command>` form; then validate with `runpane panels wait` or `runpane panels screen` before reporting progress.\",\n \"\",\n \"Use Pane when the user wants visible Panes or co-drivable parallel feature/PR workspaces. Do not use Pane as your default private delegation mechanism; for private background decomposition, use your normal subagent/worktree workflow.\",\n \"\",\n \"Register the main/base repository once. Do not register pre-created git worktrees as separate Pane repositories unless the user explicitly asks.\",\n \"\",\n \"Use `runpane panes create` for separate visible Panes (Pane sessions) for feature/PR work. Use `runpane panels create` for reviewer/helper tabs inside an existing Pane that should share that Pane's worktree.\",\n \"\",\n \"Typical workflow: register the saved base repository once; create one Pane (Pane session) per feature/PR; use panels/tabs inside that Pane for helper or reviewer agents that should share the worktree; archive the Pane after the PR is done to remove it from active Panes and clean up its managed worktree when applicable.\",\n \"\",\n \"Skill routing reference: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for the behavior those words imply, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the [workflow map](https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png).\",\n \"Use those peer references together to choose the phase: discuss/investigate until the work is clear enough to delegate, then ticket/plan/implement/review/PR-test/teach-back as appropriate. The orchestrator and workflow map may point to different skills; reconcile them with the user's request instead of hardcoding a skill list or treating one reference as subordinate.\",\n \"For the Pane implementation source of truth for where the skill cache, cached workflow assets, and Pane Chat bootstrap live, reference [PR #291](https://github.com/dcouple/Pane/pull/291): `main/src/services/skillCacheManager.ts` owns `<PANE_DIR>/skills/`, `.sources/dcouple-skills`, and `pane-chat/runpane-orchestrator.md`; `main/src/services/paneChatManager.ts` owns the tiny bootstrap prompt that tells the selected Pane Chat agent to read that guide.\",\n \"Use GitHub reads against the [Parsa skills folder](https://github.com/dcouple/skills/tree/main/parsa) only to inspect or refresh referenced skill files; do not clone/install the repo unless the user asks.\",\n \"Do not hardcode a specific assistant brand in workflow guidance. Use the Pane agent or custom tool command the user selected, and use `runpane agents doctor --agent <agent> --repo <selector> --json` only when checking a built-in agent template.\",\n \"\",\n \"Start with `runpane doctor --json` before taking Pane actions. Use it to understand wrapper/runtime details, daemon reachability, and the next safe commands.\",\n \"\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22: `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"\",\n \"Use `runpane agent-context --json` for full Pane CLI context. Use `runpane agent-context --command \\\"panels wait\\\" --json` or another command name for detailed schema only when needed.\",\n \"\",\n \"Default to context-safe validation: after creating Panes or sending terminal input, run `runpane panels wait` or `runpane panels screen` before reporting success. Prefer `runpane panels submit` for normal text plus Enter; use `runpane panels input` only for exact bytes such as Ctrl-C or escape sequences.\",\n \"\",\n \"Common commands:\",\n \"- `runpane doctor --json`\",\n \"- `runpane agent-context --json`\",\n \"- `runpane repos list --json`\",\n \"- `runpane repos add --path <repo> --yes --json`\",\n \"- `runpane agents doctor --agent <agent> --repo active --json`\",\n \"- `runpane panes create --repo active --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels list --pane <pane-id> --json`\",\n \"- `runpane panels screen --panel <panel-id> --limit 80 --json`\",\n \"- `runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json`\",\n \"- `runpane panels submit --panel <panel-id> --text \\\"<answer>\\\" --yes --json`\",\n \"- `runpane panels input --panel <panel-id> --input-file <path|-> --yes --json`\",\n \"\",\n \"WSL note: if `runpane doctor --json` cannot find `/tmp/pane-daemon.../daemon.sock` or `runpane` resolves to a broken Windows shim, Pane may be running on Windows. Try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`, then create Panes through the same PowerShell form using the saved WSL repo name or id. Use `runpane agents doctor --agent <agent> --repo <selector> --json` to diagnose the repo environment Pane will actually use.\"\n ]\n }\n}") +RUNPANE_CONTRACT = json.loads("{\n \"$schema\": \"./schema.json\",\n \"schemaVersion\": 1,\n \"name\": \"runpane\",\n \"description\": \"Thin installer and local control CLI for Pane\",\n \"packageInstallPolicy\": [\n \"The packages must not download, install, or configure Pane during package installation.\",\n \"Work starts only when a user runs `runpane ...`.\"\n ],\n \"compatibility\": {\n \"node\": \">=18.17.0\",\n \"python\": \">=3.8\"\n },\n \"terminology\": {\n \"repo\": \"Saved Pane repository/project record\",\n \"pane\": \"User-visible Pane session\",\n \"tool\": \"Terminal-backed tab\",\n \"agent\": \"Built-in agent command template\"\n },\n \"defaults\": {\n \"target\": \"client\",\n \"paneVersion\": \"latest\",\n \"channel\": \"stable\",\n \"format\": \"auto\",\n \"dryRun\": false,\n \"yes\": false,\n \"verbose\": false\n },\n \"enums\": {\n \"installTargets\": [\n \"client\",\n \"daemon\"\n ],\n \"artifactFormats\": [\n \"auto\",\n \"appimage\",\n \"deb\",\n \"dmg\",\n \"zip\",\n \"exe\"\n ],\n \"channels\": [\n \"stable\",\n \"nightly\"\n ],\n \"agents\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"agentTemplates\": {\n \"codex\": {\n \"title\": \"Codex\",\n \"command\": \"codex --yolo\",\n \"description\": \"Open a Codex terminal tab and allow the initial input to drive the agent.\"\n },\n \"claude\": {\n \"title\": \"Claude Code\",\n \"command\": \"claude --dangerously-skip-permissions\",\n \"description\": \"Open a Claude Code terminal tab and allow the initial input to drive the agent.\"\n }\n },\n \"commands\": [\n {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"usage\": [\n \"runpane help [command]\"\n ]\n },\n {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"usage\": [\n \"runpane setup\"\n ],\n \"interactiveEntrypoint\": true\n },\n {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"usage\": [\n \"runpane install [client|daemon] [options]\"\n ],\n \"defaultTarget\": \"client\",\n \"targets\": [\n \"client\",\n \"daemon\"\n ],\n \"unknownDaemonFlagsForwarded\": true\n },\n {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"usage\": [\n \"runpane update [options]\"\n ],\n \"target\": \"client\"\n },\n {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"usage\": [\n \"runpane version\",\n \"runpane --version\"\n ]\n },\n {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"usage\": [\n \"runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"usage\": [\n \"runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"usage\": [\n \"runpane agent-context [--json]\",\n \"runpane agent-context --command <command> [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"usage\": [\n \"runpane repos list [--json] [--pane-dir <path>]\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"usage\": [\n \"runpane repos add --path <path> [--name <name>] [--json] [--yes]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions in a saved repository.\",\n \"usage\": [\n \"runpane panes list [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"usage\": [\n \"runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [options]\",\n \"runpane panes create --from-json <path|-> [--yes] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"usage\": [\n \"runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"usage\": [\n \"runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready] --yes [--json]\",\n \"runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--focus|--no-focus] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"usage\": [\n \"runpane panels list --pane <pane-id> [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"usage\": [\n \"runpane panels output --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"usage\": [\n \"runpane panels screen --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"usage\": [\n \"runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"usage\": [\n \"runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"usage\": [\n \"runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"usage\": [\n \"runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ]\n }\n ],\n \"flags\": {\n \"wrapper\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"description\": \"Pane release to install or inspect.\"\n },\n {\n \"name\": \"--download-dir\",\n \"value\": \"<path>\",\n \"description\": \"Directory for downloaded Pane release artifacts.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"description\": \"Use or inspect an existing Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"description\": \"Pane release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"description\": \"Print the plan without downloading or installing.\"\n },\n {\n \"name\": \"--yes\",\n \"aliases\": [\n \"-y\"\n ],\n \"description\": \"Skip interactive prompts where possible.\"\n },\n {\n \"name\": \"--verbose\",\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"remoteValue\": [\n {\n \"name\": \"--label\",\n \"value\": \"<name>\"\n },\n {\n \"name\": \"--prefer-tunnel\",\n \"value\": \"<tailscale|ssh|manual|auto>\"\n },\n {\n \"name\": \"--channel\",\n \"value\": \"<stable|nightly>\"\n },\n {\n \"name\": \"--base-url\",\n \"value\": \"<url>\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\"\n },\n {\n \"name\": \"--listen-port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--repo-ref\",\n \"value\": \"<ref>\"\n }\n ],\n \"remoteBoolean\": [\n {\n \"name\": \"--auto-listen-port\"\n },\n {\n \"name\": \"--interactive-tailscale-setup\"\n },\n {\n \"name\": \"--no-install-service\"\n },\n {\n \"name\": \"--no-tailscale-serve\"\n },\n {\n \"name\": \"--print-only\"\n }\n ],\n \"localValue\": [\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"description\": \"Connect to a Pane daemon using this Pane data directory.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"description\": \"Pane/session id to inspect.\"\n },\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"description\": \"Tool panel id to inspect or control.\"\n },\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"description\": \"Name for the registered repository or created pane/session.\"\n },\n {\n \"name\": \"--worktree-name\",\n \"value\": \"<name>\",\n \"description\": \"Worktree name to request. Defaults to --name.\"\n },\n {\n \"name\": \"--base-branch\",\n \"value\": \"<branch>\",\n \"description\": \"Base branch for the created worktree.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"description\": \"Terminal tab title. Defaults to the selected agent title or Terminal.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"aliases\": [\n \"--prompt\"\n ],\n \"description\": \"Text to send to the terminal after the command is ready. --prompt is an alias.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"description\": \"Read a full panes.create request JSON payload from a file or stdin.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Maximum time to wait for each pane creation job.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Readiness wait timeout for panes create --wait-ready.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"description\": \"Accepted for forward compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"description\": \"Maximum recent output lines or records to read.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"description\": \"Panel wait condition.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"description\": \"Text to wait for with panels wait --for text.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Polling interval for panels wait.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"description\": \"Text bytes to send to a terminal panel.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read panel input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"description\": \"Identify whether a local-control mutation is user-initiated or agent/orchestrator-initiated.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"description\": \"Composer submit key sequence strategy for panels submit-composer.\"\n }\n ],\n \"localBoolean\": [\n {\n \"name\": \"--json\",\n \"description\": \"Print machine-readable JSON output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"description\": \"Wait for created terminal panels to be ready before returning.\"\n },\n {\n \"name\": \"--no-focus\",\n \"description\": \"Create the pane or panel in the background without changing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"description\": \"Explicitly focus the created pane or panel.\"\n },\n {\n \"name\": \"--pinned\",\n \"description\": \"Create the pane already pinned (the UI's favorite/pin star).\"\n },\n {\n \"name\": \"--force\",\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n }\n ]\n },\n \"help\": {\n \"npm\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z> Pane release to install\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path> Use an existing Pane executable\",\n \" --dry-run Print the plan without downloading\",\n \" --yes Skip interactive prompts where possible\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [options]\",\n \"\",\n \"Updates Pane using the same artifact selection as \\\"runpane install client\\\".\",\n \"\",\n \"Options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json Print a machine-readable environment report\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --pane-path <path> Inspect a specific Pane executable\",\n \" --format <format> Release artifact format to inspect\",\n \" --verbose Print extra diagnostics\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json Print machine-readable output\",\n \" --pane-dir <path> Connect to a specific Pane data directory\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path> Existing git repository path\",\n \" --name <name> Saved repository name; defaults to the directory name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without adding the repo\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to archive\",\n \" --source <user|agent> Mark mutation source\",\n \" --force Archive even if the pane has uncommitted, untracked, or unpushed changes\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to pin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without pinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to unpin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without unpinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads bounded recent terminal output from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Tool panel id\",\n \" --limit <count> Maximum recent output lines or records to read\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text bytes to send\",\n \" --input-file <path|-> Read input from a file or stdin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command> Print the full definition for one runpane command\",\n \" --json Print machine-readable output\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n },\n \"pip\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" pipx run runpane install client\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \" uvx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [--version <latest|vX.Y.Z>] [--dry-run] [--yes]\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\",\n \" --pane-path <path>\",\n \" --format <format>\",\n \" --verbose\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path>\",\n \" --name <name>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --source <user|agent>\",\n \" --force\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads recent terminal output records from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --limit <count>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --text <text>\",\n \" --input-file <path|->\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command>\",\n \" --json\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" python -m runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n }\n },\n \"docs\": {\n \"maintainerRules\": [\n \"Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\",\n \"Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\",\n \"The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\",\n \"Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\",\n \"Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\",\n \"`pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\",\n \"Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\"\n ],\n \"recommendedQuickStarts\": [\n \"npx --yes runpane@latest\",\n \"npm i -g runpane && runpane setup\",\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"npmCommands\": [\n \"npx --yes runpane@latest\",\n \"npx --yes runpane@latest setup\",\n \"npx --yes runpane@latest install client\",\n \"npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \"pnpm dlx runpane@latest\",\n \"pnpm dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"npm i -g runpane && runpane\",\n \"npm i -g runpane && runpane setup\",\n \"npm i -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"pnpm add -g runpane && runpane\",\n \"pnpm add -g runpane && runpane setup\",\n \"pnpm add -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"yarn dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"bunx runpane@latest install daemon --label \\\"My Server\\\"\"\n ],\n \"pythonCommands\": [\n \"pipx run runpane\",\n \"pipx run runpane setup\",\n \"python -m pip install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"python -m runpane setup\",\n \"runpane install daemon --label \\\"My Server\\\"\",\n \"pipx install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"uvx runpane@latest\",\n \"uvx runpane@latest setup\",\n \"uvx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"python -m runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"packageManagerNotes\": [\n \"Use `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\",\n \"Do not document `pnpm install runpane` as the public CLI install path.\"\n ],\n \"commandUsages\": [\n \"runpane\",\n \"runpane setup\",\n \"runpane install\",\n \"runpane install client\",\n \"runpane install daemon\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane doctor --json\",\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\",\n \"runpane repos list --json\",\n \"runpane repos add --path /path/to/repo --name Pane --yes --json\",\n \"runpane panes list --repo active --json\",\n \"runpane panes create --repo active --name issue-252 --agent <agent> --prompt \\\"Kick off the discussion skill for issue 252\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panels list --pane <pane-id> --json\",\n \"runpane panels output --panel <panel-id> --limit 200 --json\",\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes --json\",\n \"runpane help\",\n \"runpane <command> --help\"\n ],\n \"commandDescriptions\": [\n \"`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. In non-interactive shells or CI, both forms must print help, common commands, and agent discovery hints, then exit successfully instead of waiting for input.\",\n \"`runpane install` is an alias for `runpane install client`.\",\n \"`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\",\n \"`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\",\n \"The wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\",\n \"`runpane update` uses the same release resolution and installer path as `install client`.\",\n \"`runpane version` prints only wrapper package metadata and does not contact, launch, or focus the Pane app or daemon.\",\n \"`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, daemon reachability, and remote-daemon hints. Add `--json` for a machine-readable report that agents should run before mutating Pane state. Installed app version detection must be best-effort and must not launch, focus, or configure Pane; macOS wrappers read app bundle metadata instead of executing `Pane --version`.\",\n \"`runpane agent-context` prints a brief, token-efficient command schema for coding agents without connecting to the Pane daemon.\",\n \"`runpane agent-context --command \\\"panes create\\\"` prints the detailed definition for one command. Add `--json` for machine-readable output.\",\n \"`runpane repos list` connects to the running local Pane daemon and prints saved repository records.\",\n \"`runpane repos add` registers an existing git repository with the running local Pane daemon. It does not create directories or initialize git repositories by default.\",\n \"`runpane panes list` lists Pane sessions, optionally scoped to one saved repository.\",\n \"`runpane panes create` connects to the running local Pane daemon, resolves the requested saved base repository, creates user-visible Pane sessions backed by Pane-managed worktrees/branches, opens terminal-backed tool tabs, and optionally sends initial input to the started tool. Built-in agent panes and `--source agent` default to background/no-focus unless `--focus` is passed.\",\n \"For `panes create --wait-ready`, `initialInput.verifiedSubmitted: true` is reported only after argument attachment or composer-clear plus activity evidence. Routing input does not by itself verify submission.\",\n \"`runpane panes archive` archives a Pane exactly like the UI Archive action, including removal of its Pane-managed git worktree, and refuses (unless `--force`) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes. It waits for worktree removal to finish before returning and reports the outcome in `worktreeCleanup`.\",\n \"`runpane panels list` lists tool panels inside one Pane session.\",\n \"`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.\",\n \"`runpane panels input` sends exact input bytes to one terminal panel. Prefer `--input-file` for newlines, Ctrl-C, quotes, or shell-sensitive text.\",\n \"`runpane panes create --prompt` is an alias for `--initial-input`; request JSON and daemon payloads should use the canonical `initialInput` field.\",\n \"If composer submission cannot be verified without risking a duplicate, the create item is unsuccessful with `initialInput.staged`, `initialInput.attempts`, `initialInput.blocked.kind: submission_unverified`, and an actionable `nextCommand`. The CLI-facing `--prompt` alias maps to this canonical `initialInput` result.\",\n \"When running from WSL while Pane is installed on Windows, the Linux wrapper may look for a missing `/tmp/pane-daemon.../daemon.sock` or resolve to a Windows shim such as Volta. In that case invoke the Windows wrapper through PowerShell from a Windows cwd, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\"\n ],\n \"wrapperFlagNote\": \"The top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\",\n \"localControlFlagNote\": \"`runpane doctor --json`, `runpane repos list`, `runpane panes list`, `runpane panes create`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues.\",\n \"daemonFlagNote\": \"Unknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\",\n \"downloadAttribution\": [\n \"The npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\",\n \"The PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\",\n \"Wrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\",\n \"If the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\",\n \"Wrappers also emit best-effort lifecycle telemetry to `https://runpane.com/api/runpane/telemetry` for command start/success/failure, download request/success/failure, and GitHub fallback usage.\",\n \"Wrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so distinct wrapper users can be counted with `count(DISTINCT properties.install_id)`.\",\n \"Wrapper telemetry must be disabled in CI and when `RUNPANE_TELEMETRY_DISABLED` is set, and must not include raw paths, labels, prompts, raw error text, or environment values.\"\n ],\n \"publishingCredentials\": [\n \"Local implementation, build, and dry-run validation do not need npm or PyPI API tokens.\",\n \"Release publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\",\n \"Fallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\"\n ]\n },\n \"testFixtures\": {\n \"parserSamples\": [\n [\n \"setup\"\n ],\n [\n \"install\"\n ],\n [\n \"install\",\n \"client\",\n \"--version\",\n \"v2.2.8\",\n \"--format\",\n \"dmg\",\n \"--download-dir\",\n \"/tmp/pane-downloads\",\n \"--dry-run\",\n \"--yes\"\n ],\n [\n \"install\",\n \"daemon\",\n \"--label\",\n \"VM\",\n \"--prefer-tunnel\",\n \"ssh\",\n \"--channel\",\n \"nightly\",\n \"--base-url\",\n \"https://example.test\",\n \"--pane-dir\",\n \"/tmp/pane\",\n \"--listen-port\",\n \"4555\",\n \"--auto-listen-port\",\n \"--print-only\",\n \"--repo-ref\",\n \"main\",\n \"--unknown-future-flag\",\n \"future-value\",\n \"--dry-run\",\n \"--verbose\"\n ],\n [\n \"update\",\n \"--version\",\n \"latest\",\n \"--format\",\n \"appimage\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--dry-run\"\n ],\n [\n \"doctor\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--format\",\n \"zip\",\n \"--verbose\"\n ],\n [\n \"doctor\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"agent-context\"\n ],\n [\n \"agent-context\",\n \"--command\",\n \"panes create\",\n \"--json\"\n ],\n [\n \"repos\",\n \"list\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"repos\",\n \"add\",\n \"--path\",\n \"/tmp/repo\",\n \"--name\",\n \"Repo\",\n \"--dry-run\",\n \"--yes\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"panes\",\n \"list\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--repo\",\n \"active\",\n \"--name\",\n \"issue-252\",\n \"--agent\",\n \"codex\",\n \"--prompt\",\n \"Kick off discussion\",\n \"--source\",\n \"agent\",\n \"--pinned\",\n \"--dry-run\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"archive\",\n \"--pane\",\n \"session-1\",\n \"--force\",\n \"--source\",\n \"agent\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"pin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"unpin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"list\",\n \"--pane\",\n \"session-1\",\n \"--json\"\n ],\n [\n \"panels\",\n \"output\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"200\",\n \"--json\"\n ],\n [\n \"panels\",\n \"input\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"Continue\\\\n\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"--version\"\n ],\n [\n \"agents\",\n \"doctor\",\n \"--agent\",\n \"codex\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--source\",\n \"agent\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"45000\",\n \"--concurrency\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"screen\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"80\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"wait\",\n \"--panel\",\n \"panel-1\",\n \"--for\",\n \"text\",\n \"--contains\",\n \"ready\",\n \"--timeout-ms\",\n \"30000\",\n \"--interval-ms\",\n \"500\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--source\",\n \"agent\",\n \"--no-focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"15000\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--focus\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit-composer\",\n \"--panel\",\n \"panel-1\",\n \"--strategy\",\n \"auto\",\n \"--yes\",\n \"--json\"\n ]\n ],\n \"topLevelHelpIncludes\": [\n \"runpane setup\",\n \"runpane install\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane agent-context\",\n \"runpane repos list\",\n \"runpane repos add\",\n \"runpane panes list\",\n \"runpane panes create\",\n \"runpane panes archive\",\n \"runpane panels create\",\n \"runpane panels list\",\n \"runpane panels output\",\n \"runpane panels input\",\n \"runpane agents doctor\",\n \"runpane panels screen\",\n \"runpane panels submit\",\n \"runpane panels submit-composer\",\n \"runpane panels wait\",\n \"runpane doctor --json\",\n \"runpane agent-context --json\",\n \"Agent discovery:\",\n \"Common commands:\"\n ],\n \"npmHelpIncludes\": [\n \"pnpm dlx runpane@latest\",\n \"npx --yes runpane@latest\"\n ],\n \"pipHelpIncludes\": [\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"installHelpIncludes\": [\n \"--version <latest|vX.Y.Z>\",\n \"--format <auto|appimage|deb|dmg|zip|exe>\",\n \"--download-dir <path>\",\n \"--pane-path <path>\",\n \"--label <name>\",\n \"--prefer-tunnel <tailscale|ssh|manual|auto>\",\n \"--repo-ref <ref>\"\n ]\n },\n \"jsonSchemas\": {\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"doctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"source\",\n \"wrapper\",\n \"release\",\n \"installedPane\",\n \"daemon\",\n \"remoteSetup\",\n \"nextCommands\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"npm\",\n \"pip\"\n ]\n },\n \"wrapper\": {\n \"type\": \"object\",\n \"required\": [\n \"runtime\",\n \"version\",\n \"paneDir\",\n \"endpoint\"\n ],\n \"properties\": {\n \"runtime\": {\n \"enum\": [\n \"node\",\n \"python\"\n ]\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"paneDir\": {\n \"type\": \"string\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"platform\": {\n \"type\": \"object\",\n \"properties\": {\n \"os\": {\n \"type\": \"string\"\n },\n \"arch\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"release\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"installedPane\": {\n \"type\": \"object\",\n \"required\": [\n \"found\"\n ],\n \"properties\": {\n \"found\": {\n \"type\": \"boolean\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"daemon\": {\n \"type\": \"object\",\n \"required\": [\n \"reachable\",\n \"endpoint\"\n ],\n \"properties\": {\n \"reachable\": {\n \"type\": \"boolean\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"result\": {\n \"type\": \"object\"\n },\n \"error\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"remoteSetup\": {\n \"type\": \"object\",\n \"required\": [\n \"ready\",\n \"displayAvailable\",\n \"headlessEnvironmentApplied\",\n \"diagnostics\"\n ],\n \"properties\": {\n \"ready\": {\n \"type\": \"boolean\"\n },\n \"displayAvailable\": {\n \"type\": \"boolean\"\n },\n \"headlessEnvironmentApplied\": {\n \"type\": \"boolean\"\n },\n \"diagnostics\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"severity\",\n \"message\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"PANE_APPIMAGE_FUSE_MISSING\",\n \"PANE_ELECTRON_SANDBOX_ROOT\",\n \"PANE_ELECTRON_SANDBOX_UNAVAILABLE\",\n \"PANE_USER_SERVICE_UNAVAILABLE\"\n ]\n },\n \"severity\": {\n \"enum\": [\n \"warning\",\n \"error\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"recoveryCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommands\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repos\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repos\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"name\",\n \"path\",\n \"active\",\n \"sessionCount\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n },\n \"sessionCount\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"path\"\n ],\n \"properties\": {\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"created\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"created\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"preview\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"path\",\n \"alreadyExists\",\n \"wouldCreate\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"alreadyExists\": {\n \"type\": \"boolean\"\n },\n \"wouldCreate\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"repo\",\n \"panes\"\n ],\n \"properties\": {\n \"repo\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"const\": true\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panes\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"tool\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"worktreeName\": {\n \"type\": \"string\"\n },\n \"baseBranch\": {\n \"type\": \"string\"\n },\n \"sessionPrompt\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"agent\"\n ],\n \"properties\": {\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"command\"\n ],\n \"properties\": {\n \"command\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"timeoutMs\": {\n \"type\": \"number\"\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n },\n \"concurrency\": {\n \"type\": \"number\"\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repo\",\n \"items\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"name\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"sessionId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"type\": \"object\",\n \"required\": [\n \"delivered\",\n \"submitted\",\n \"inputBytes\"\n ],\n \"properties\": {\n \"delivered\": {\n \"type\": \"boolean\"\n },\n \"submitted\": {\n \"type\": \"boolean\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\",\n \"argument\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\",\n \"argument\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"staged\": {\n \"type\": \"boolean\"\n },\n \"attempts\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panes\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"panes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"paneId\",\n \"name\",\n \"status\",\n \"worktreePath\",\n \"repoId\",\n \"panelCount\",\n \"pinned\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"repoId\": {\n \"type\": \"number\"\n },\n \"repoName\": {\n \"type\": \"string\"\n },\n \"panelCount\": {\n \"type\": \"number\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"force\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panePinRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"panePinResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"const\": true\n },\n \"favoritePinnedAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveResult\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"archived\",\n \"forced\",\n \"worktreeCleanup\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"const\": true\n },\n \"forced\": {\n \"type\": \"boolean\"\n },\n \"worktreeCleanup\": {\n \"enum\": [\n \"completed\",\n \"failed\",\n \"timeout\",\n \"not-applicable\"\n ]\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"blocked\",\n \"nextCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"message\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"uncommitted-changes\",\n \"unpushed-commits\",\n \"uncommitted-and-unpushed\",\n \"status-unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panelListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panels\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panels\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"panelId\",\n \"paneId\",\n \"type\",\n \"title\",\n \"active\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"type\": \"string\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"position\": {\n \"type\": \"number\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActiveAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelOutputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"limit\",\n \"returnedCount\",\n \"hasMore\",\n \"outputs\",\n \"text\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"outputs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"type\",\n \"data\",\n \"timestamp\"\n ],\n \"properties\": {\n \"type\": {\n \"type\": \"string\"\n },\n \"data\": {},\n \"timestamp\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextBriefResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"summary\",\n \"rules\",\n \"tools\",\n \"detailCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"brief\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"rules\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"tools\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"arguments\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"detailCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextCommandResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"command\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"command\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"command\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"details\",\n \"arguments\",\n \"examples\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"details\": {\n \"type\": \"string\"\n },\n \"requiresPaneDaemon\": {\n \"type\": \"boolean\"\n },\n \"mutates\": {\n \"type\": \"boolean\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"examples\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"jsonSchemas\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"notes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"panelScreenResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"source\",\n \"limit\",\n \"returnedLineCount\",\n \"hasMore\",\n \"text\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedLineCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"$ref\": \"#/jsonSchemas/paneCreateResult/properties/items/items/oneOf/0/properties/initialInput\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"enter\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"enter\": {\n \"const\": \"cr\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelWaitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\",\n \"screen\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"screen\": {\n \"type\": \"object\",\n \"required\": [\n \"source\",\n \"text\",\n \"hasMore\"\n ],\n \"properties\": {\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentDoctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"agent\",\n \"command\",\n \"available\",\n \"checks\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"environment\": {\n \"enum\": [\n \"wsl\",\n \"windows\",\n \"linux\",\n \"macos\"\n ]\n },\n \"available\": {\n \"type\": \"boolean\"\n },\n \"executablePath\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"checks\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"ok\",\n \"message\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"tool\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"const\": \"terminal\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panelId\",\n \"title\",\n \"active\",\n \"focused\",\n \"tool\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"strategy\": {\n \"enum\": [\n \"auto\",\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"strategy\",\n \"sequenceName\",\n \"verifiedSubmitted\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"agentContext\": {\n \"brief\": {\n \"title\": \"Pane agent context\",\n \"summary\": \"Pane lets a developer manage saved base repositories, user-visible Panes (Pane sessions) for feature/PR work, and terminal-backed panel tabs. A Pane is a visible workspace that normally maps to one Pane-managed git worktree and branch; a panel is a terminal tab inside one Pane and shares that Pane's worktree; an agent is a CLI process running inside a panel.\",\n \"rules\": [\n \"Start with `runpane doctor --json` to understand wrapper, platform, daemon reachability, and the next safe commands before mutating Pane state.\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"Happy path for any user request to use Pane/RunPane: run `runpane doctor --json`, read `runpane agent-context --json`, resolve or add the saved base repo, create the requested visible Pane with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --pinned --no-focus --wait-ready --yes --json` when it should stay visible (or omit `--pinned`, or use `--tool-command <command>` instead of `--agent <agent>`), then validate with `panels wait` or `panels screen`.\",\n \"Treat Pane as the user's visible cockpit for watching/co-driving work. Do not create Panes or panels for private delegation unless the user asked for visible Pane orchestration or the result should appear in the Pane app.\",\n \"Register the saved base repository once with `repos add`; do not register a pre-created worktree as a separate repo unless the user explicitly asks.\",\n \"Use `panes create` for separate visible Panes (Pane sessions) for feature/PR work. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` for reviewer/helper/clean-context tabs that should stay inside an existing Pane and share that Pane's worktree.\",\n \"For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"Skill routing: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for those behaviors, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the workflow map at https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png. For where the local cache and Pane Chat bootstrap live in Pane, reference https://github.com/dcouple/Pane/pull/291. Use GitHub reads against https://github.com/dcouple/skills/tree/main/parsa only to inspect or refresh referenced skill files; do not clone or install it unless the user asks.\",\n \"When an agent creates Panes or panels, pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when declaring a Pane that should remain in the UI's favorite/pin set.\",\n \"Use `runpane agent-context --json` for the full agent-facing CLI context, or `runpane agent-context --command <command> --json` for one detailed command definition.\",\n \"For `agent-context --command`, use canonical spaced names like `panes create`; copied forms like `panes.create` or `runpane panes create` are accepted too.\",\n \"Use `runpane repos list --json` to find the saved repository when unsure after doctor shows the daemon is reachable.\",\n \"If WSL cannot reach Pane or `runpane` resolves to a broken Windows shim, the user may be running Windows Pane; try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`.\",\n \"If the repository exists on disk but is not saved in Pane, use `runpane repos add --path <repo> --yes --json` before creating panes.\",\n \"Use `runpane agents doctor --agent <codex|claude> --repo <selector> --json` when agent availability differs across host, Windows, WSL, or repo environments.\",\n \"Use `runpane panes create --wait-ready` to create Panes and validate initial terminal readiness in one call.\",\n \"Use `runpane panels screen` for compact current state, `panels wait` for readiness/text checks, `panels submit` for ordinary Enter-submitted input, and `panels submit-composer --strategy auto` for agent composers.\",\n \"Use `runpane panels input` only when exact bytes are required, such as Ctrl-C or handcrafted terminal input.\",\n \"After creating Panes or sending terminal input, validate with `panels wait` or bounded `panels screen` before reporting success.\"\n ],\n \"detailCommand\": \"runpane agent-context --command <command> [--json]\",\n \"tools\": [\n {\n \"name\": \"doctor\",\n \"summary\": \"Report wrapper, platform, installed Pane, and daemon reachability before an agent mutates Pane state.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\",\n \"--pane-path <path>\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"arguments\": [\n \"--command <command>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Check whether Codex or Claude is available in the repo environment Pane will use.\",\n \"arguments\": [\n \"--agent <codex|claude>\",\n \"--repo <selector>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"arguments\": [\n \"--path <path>\",\n \"--name <name>\",\n \"--yes\",\n \"--json\",\n \"--dry-run\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--name <name>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--prompt <text>\",\n \"--initial-input-file <path|->\",\n \"--from-json <path|->\",\n \"--source <user|agent>\",\n \"--pinned\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms <ms>\",\n \"--concurrency <count>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--source <user|agent>\",\n \"--force\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--source <user|agent>\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text plus terminal Enter to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer with the correct key sequence, including Ctrl+Enter for Codex.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--strategy <auto|codex-ctrl-enter|enter>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for terminal initialized, ready, idle, or text state with compact output.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--for <condition>\",\n \"--contains <text>\",\n \"--timeout-ms <ms>\",\n \"--interval-ms <ms>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n }\n ]\n },\n \"commands\": {\n \"help\": {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"details\": \"Use this when you need human-readable usage text for a command.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Optional command topic such as \\\"panes create\\\".\"\n }\n ],\n \"examples\": [\n \"runpane help\",\n \"runpane help panes create\"\n ],\n \"notes\": [\n \"Help text is generated from the runpane contract.\"\n ]\n },\n \"setup\": {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"details\": \"Use this for a human-driven Pane setup flow, not for unattended agent orchestration.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [],\n \"examples\": [\n \"runpane setup\"\n ],\n \"notes\": [\n \"In non-interactive shells, setup prints help and exits successfully.\"\n ]\n },\n \"install\": {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"details\": \"Installs or launches Pane release artifacts at command runtime. `install daemon` forwards remote setup flags to Pane.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"target\",\n \"value\": \"<client|daemon>\",\n \"required\": false,\n \"description\": \"Install the desktop client or configure a remote daemon host.\"\n },\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to install.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip interactive prompts where possible.\"\n }\n ],\n \"examples\": [\n \"runpane install client\",\n \"runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"notes\": [\n \"Package install itself is inert; work begins only when runpane is executed.\"\n ]\n },\n \"update\": {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"details\": \"Resolves and installs the selected Pane client release.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to update to.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Print the update plan without downloading.\"\n }\n ],\n \"examples\": [\n \"runpane update\",\n \"runpane update --version latest\"\n ],\n \"notes\": [\n \"Equivalent artifact selection to `runpane install client`.\"\n ]\n },\n \"version\": {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"details\": \"Use this to confirm which wrapper is available. App, daemon, and release diagnostics belong in doctor.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Ignored for version; retained only for parser compatibility.\"\n }\n ],\n \"examples\": [\n \"runpane version\",\n \"runpane --version\"\n ],\n \"notes\": []\n },\n \"doctor\": {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"details\": \"Use this first when an agent needs to understand whether Pane is installed, which wrapper/runtime is running, what daemon endpoint is expected, and whether the running Pane app is reachable. JSON mode should return a report even when the daemon is unreachable.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print a machine-readable environment report.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Inspect a specific Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<format>\",\n \"required\": false,\n \"description\": \"Release artifact format to inspect.\"\n },\n {\n \"name\": \"--verbose\",\n \"required\": false,\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"examples\": [\n \"runpane doctor --json\",\n \"runpane doctor\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ],\n \"notes\": [\n \"Agents should run `runpane doctor --json` before mutating Pane state.\",\n \"The JSON report includes daemon reachability as data; an unreachable daemon is not a reason to skip the rest of the report.\",\n \"Use `runpane agent-context --json` after doctor when full CLI context is needed.\"\n ]\n },\n \"agent-context\": {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"details\": \"Use this first when an agent needs to discover Pane primitives. It is local/offline and does not require a running Pane app.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Print the detailed definition for one command, for example \\\"panes create\\\".\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ],\n \"notes\": [\n \"Default output is brief so AGENTS.md can point here without bloating context.\",\n \"`--command` accepts canonical spaced names and common copied forms, including `panes.create` and `runpane panes create`.\"\n ]\n },\n \"repos list\": {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"details\": \"Use this to find the right Pane-managed repository before creating panes. If the repo is missing, use `repos add` first.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable repository records.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos list --json\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ],\n \"notes\": [\n \"Requires a running Pane app or daemon for the selected Pane data directory.\",\n \"If this fails from WSL with a missing `/tmp/pane-daemon.../daemon.sock`, `volta: command not found`, or a Windows shim error, the user may be running Windows Pane. Retry via `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\",\n \"When using PowerShell from WSL, set a Windows cwd such as `$env:TEMP` or `$env:USERPROFILE` before running runpane to avoid UNC cwd issues.\"\n ]\n },\n \"repos add\": {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"details\": \"Use this when the repository exists on disk but is not saved in Pane yet. It validates the path as a git repository.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"required\": true,\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": false,\n \"description\": \"Saved repository name; defaults to the directory name.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without adding the repo.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos add --path /path/to/repo --yes --json\"\n ],\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ],\n \"notes\": [\n \"It does not create directories or initialize git repositories by default.\",\n \"For a Windows Pane app managing WSL repositories, prefer a saved WSL repo from `repos list` when possible. Adding a brand-new WSL repo from Windows may require WSL-aware path handling.\"\n ]\n },\n \"panes list\": {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"details\": \"Use this after selecting a repository to find existing Pane sessions and their ids before inspecting panels.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panes list --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ],\n \"notes\": [\n \"Without --repo, this lists sessions across saved Pane repositories.\"\n ]\n },\n \"panes create\": {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"details\": \"Use this when the user wants separate visible Panes (Pane sessions) for feature or PR work. Select the saved base repository, choose a built-in agent or custom terminal command, and optionally send initial input after the tool starts. Pane creates and owns a git worktree/branch for each new Pane; do not pre-create a git worktree and register it as a separate repo unless the user explicitly asks. For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": true,\n \"description\": \"Pane/session name.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--prompt\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Alias for --initial-input; sends text after the command is ready.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read a full panes.create request JSON payload.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background/no-focus creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the pane in the background without stealing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created pane.\"\n },\n {\n \"name\": \"--pinned\",\n \"required\": false,\n \"description\": \"Create the pane already pinned (the Pane UI's favorite/pin star); does not imply focus.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait for each created terminal panel to be ready before returning.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness timeout per pane; defaults to 30000.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Accepted for compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n }\n ],\n \"examples\": [\n \"runpane panes create --repo active --name issue-257 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes create --repo active --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ],\n \"notes\": [\n \"At least one of --agent or --tool-command is required unless --from-json is used.\",\n \"`panes create` is for user-visible Pane orchestration, not the agent's default private delegation mechanism.\",\n \"Register the saved base repository once. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` instead when a reviewer/helper should share an existing Pane's worktree.\",\n \"Agent-created Panes should pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when the Pane should remain in the UI's favorite/pin set.\",\n \"The built-in agent templates come from the runpane contract; custom terminal commands can pass agent-specific flags when requested by the user.\",\n \"Use --initial-input-file for multi-line prompts or shell-sensitive initial input.\",\n \"With --wait-ready, verifiedSubmitted is earned from delivery evidence; routing initial input alone does not imply verified submission.\",\n \"If initialInput.blocked.kind is submission_unverified, do not submit again automatically. Inspect initialInput.staged and attempts, then run nextCommand to resolve the ambiguous composer state.\",\n \"When the JSON result includes nextCommand, run it to validate that the terminal produced output before reporting success.\",\n \"Multi-pane requests are created sequentially today. The --concurrency flag is accepted for compatibility, but agents should not rely on parallel creation.\",\n \"For POSIX or WSL command chaining, use a custom terminal command like `bash -lc 'cmd1 && cmd2 && cmd3'`.\",\n \"For Windows PowerShell command chaining, use a custom terminal command like `powershell -NoProfile -Command \\\"cmd1; if ($LASTEXITCODE) { exit $LASTEXITCODE }; cmd2\\\"`.\",\n \"From WSL with Windows Pane, invoke through PowerShell and select the saved WSL repo by name or id, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane panes create --repo \\\"WSL Pane\\\" --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json'`.\",\n \"Use --wait-ready when an agent needs to verify that an agent terminal started instead of only creating a pane.\",\n \"If readiness returns blocked, inspect blocked.suggestedCommand rather than guessing which prompt to answer.\"\n ]\n },\n \"panes archive\": {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"details\": \"Use this to close out a Pane once its PR has merged. Refuses to archive (unless --force) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes, so an agent cannot silently discard work. A merged and pushed branch has no unpushed commits, so it archives cleanly. Waits for worktree removal to finish before returning.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to archive.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; does not change archive safety behavior.\"\n },\n {\n \"name\": \"--force\",\n \"required\": false,\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panes archive --pane <pane-id> --force --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ],\n \"notes\": [\n \"If the result has ok:false with a blocked field, the pane was NOT archived; inspect blocked.code and rerun with --force if discarding the flagged work is intentional.\",\n \"A successful archive waits for the Pane-managed worktree to be removed before returning; check worktreeCleanup in the result for the final outcome.\",\n \"Archiving a main-repo Pane (no Pane-managed worktree) always succeeds immediately since nothing is deleted from disk.\"\n ]\n },\n \"panes pin\": {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to true without reading and toggling it. Repeating the command is idempotent and pinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to pin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes pin --pane <pane-id> --yes --json\",\n \"runpane panes pin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-pinned Pane pinned.\",\n \"Pinning does not focus the Pane.\"\n ]\n },\n \"panes unpin\": {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to false without reading and toggling it. Repeating the command is idempotent and unpinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to unpin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes unpin --pane <pane-id> --yes --json\",\n \"runpane panes unpin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-unpinned Pane unpinned.\",\n \"Unpinning does not focus the Pane.\"\n ]\n },\n \"panels list\": {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"details\": \"Use this to find the terminal panel id to inspect or control after creating or selecting a Pane session.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels list --pane <pane-id> --json\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ],\n \"notes\": [\n \"The ids returned here are stable inputs for `panels output` and `panels input`.\"\n ]\n },\n \"panels output\": {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"details\": \"Use this to inspect recent terminal output from a terminal-backed panel without loading the full history by default. Live terminal scrollback is returned as bounded recent lines; persisted output fallback is returned as bounded records. Terminal control noise is stripped before returning text.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Tool panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum recent output lines or records to read. Defaults to 200.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels output --panel <panel-id> --limit 200 --json\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ],\n \"notes\": [\n \"Default output is bounded to the latest 200 lines or records. Use a larger --limit only when hasMore is true and more history is needed.\",\n \"Use --json when an agent needs timestamps, record types, returnedCount, or hasMore.\",\n \"The output is intended to be agent-readable, but terminal prompts and shell echoes may still appear; use the newest relevant lines before concluding success.\",\n \"Prefer `panels screen` for a smaller current-state read before increasing output limits.\"\n ]\n },\n \"panels input\": {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"details\": \"Use this to answer prompts or continue an agent inside an existing Pane terminal panel. Input is byte-oriented; choose --input-file for exact control over newlines and control characters.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text bytes to send.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read input from a file or stdin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"printf '\\\\003' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"runpane panels input --panel <panel-id> --text \\\"simple text\\\" --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ],\n \"notes\": [\n \"Input is sent exactly as provided. Include a real newline byte when the terminal should receive Enter; across shells, `--input-file` is safer than `--text \\\"...\\\\n\\\"`.\",\n \"Use `--input-file -` or a temp file for multi-line input, quotes, Ctrl-C, or shell-sensitive text.\",\n \"If interrupting a running process, send Ctrl-C first, validate/read output, then send the next command in a separate `panels input` call so bytes are not dropped.\",\n \"After sending input, validate with `runpane panels output --panel <panel-id> --json` before reporting success.\",\n \"Runpane records action metadata and errors for observability, but should not log full input text by default.\",\n \"For ordinary text plus Enter, prefer `panels submit` so the terminal receives a CR Enter byte.\"\n ]\n },\n \"agents doctor\": {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"details\": \"Use this before creating Codex or Claude panes when PATH may differ between macOS/Linux/Windows/WSL or between the wrapper shell and Pane. The check runs through Pane project context, not the wrapper process.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": true,\n \"description\": \"Built-in agent command to diagnose.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Repository selector; defaults to the active Pane repo.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane agents doctor --agent <agent> --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ],\n \"notes\": [\n \"For WSL repos, install the agent inside the WSL distro Pane uses, not only on Windows.\",\n \"This diagnoses built-in agent templates only; custom commands should be validated by creating a pane and reading its screen.\"\n ]\n },\n \"panels screen\": {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"details\": \"Use this for token-safe current terminal state. It prefers active alternate-screen/TUI output, then live scrollback, then persisted output. Default output is bounded to 80 lines.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum lines to return; defaults to 80.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels screen --panel <panel-id> --limit 80 --json\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ],\n \"notes\": [\n \"Use this before `panels output` when an agent only needs the latest visible/current state.\",\n \"If hasMore is true and context is missing, rerun with a larger --limit or use `panels output`.\"\n ]\n },\n \"panels submit\": {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"details\": \"Use this for ordinary interactive submissions. The daemon normalizes a final LF or CRLF to CR, or appends CR if no newline is present. Exact byte workflows remain on `panels input`.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text to submit before Enter.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read text from a file or stdin before Enter.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit --panel <panel-id> --text \\\"2\\\" --yes --json\",\n \"printf \\\"echo hello\\\" | runpane panels submit --panel <panel-id> --input-file - --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ],\n \"notes\": [\n \"The response includes nextCommand for validation. Run it before reporting that the input worked.\",\n \"Use `panels input` for Ctrl-C, escape sequences, or any workflow requiring exact bytes.\"\n ]\n },\n \"panels wait\": {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"details\": \"Use this to validate asynchronous terminal behavior without pulling large scrollback. It returns brief readiness state, blocker hints, a compact screen, and a next command.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"required\": false,\n \"description\": \"Condition to wait for. Defaults to ready for CLI panels and idle otherwise.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text required for --for text; implies --for text when --for is omitted.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Wait timeout; defaults to 30000.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Polling interval; defaults to 500.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json\",\n \"runpane panels wait --panel <panel-id> --contains \\\"Ready\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ],\n \"notes\": [\n \"If blocked is present, do not assume success. Use blocked.suggestedCommand or inspect `panels screen`.\",\n \"The default timeout and screen are intentionally small for agent context safety.\"\n ]\n },\n \"panels create\": {\n \"name\": \"panels create\",\n \"summary\": \"Create a reviewer/helper terminal tab inside an existing Pane.\",\n \"details\": \"Use this to add reviewer, helper, or clean-context tabs to an existing Pane. The new panel shares the existing Pane's worktree and branch; it does not create a separate Pane session. Use `panes create` instead when the user wants a separate visible Pane (Pane session) for feature/PR work. Agent/orchestrator-created panels should pass --source agent or --no-focus so the user stays in the implementation tab. The daemon initializes the terminal immediately and can wait for CLI readiness.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Existing Pane session id to add the panel to.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent command template to launch.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to launch instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"required\": false,\n \"description\": \"Panel title override.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Initial input to send after the tool starts.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the panel in the background without changing active panel.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created panel.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait until the terminal tool is ready.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness wait timeout.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panels create --pane <pane-id> --tool-command <command> --title <title> --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ],\n \"notes\": [\n \"Use this for same-pane reviewer loops after PR creation/testing.\",\n \"Panels share the existing Pane's worktree and branch.\",\n \"`panels create` is for visible helper/reviewer tabs, not the agent's default private delegation mechanism.\",\n \"For agent-created panels, prefer --source agent or --no-focus to avoid stealing focus from the user or implementation tab.\"\n ]\n },\n \"panels submit-composer\": {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"details\": \"Use this after sending text into an agent composer when plain Enter is not the right submit action. Agents should use --strategy auto; Pane owns per-agent submit sequences internally and verifies visible composer state when possible.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"required\": false,\n \"description\": \"Composer submit key sequence strategy.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit-composer --panel <codex-panel-id> --strategy auto --yes --json\",\n \"runpane panels submit-composer --panel <panel-id> --strategy enter --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ],\n \"notes\": [\n \"Use `panels input` or `panels submit` to write prompt text first; this command only submits the current composer.\",\n \"Use --strategy auto for agent workflows; explicit strategies are diagnostic escape hatches.\",\n \"The JSON result includes sequenceName and verifiedSubmitted. If ok is false, follow blocked.suggestedCommand instead of assuming submission happened.\"\n ]\n }\n },\n \"managedBlock\": [\n \"## Pane\",\n \"\",\n \"The developer is using Pane for this repository. Pane can manage saved repositories and create user-visible Panes with terminal-backed tools for planning, discussion, implementation, and review work.\",\n \"\",\n \"This managed guidance was created by [runpane.com](https://runpane.com) for the [Pane repository](https://github.com/dcouple/Pane). Do not delete or overwrite this block; the developer uses it for their workflow and it should remain committed to the main repository unless they explicitly ask to remove it.\",\n \"\",\n \"Pane mental model: a repository is the saved base repo; a Pane is a user-visible feature/PR workspace (Pane session) that normally maps to one Pane-managed git worktree and branch; a panel/tab is a terminal inside one Pane and shares that Pane's worktree; an agent is the CLI process running in a panel.\",\n \"\",\n \"Default happy path when the user asks you to use Pane or RunPane: run `runpane doctor --json`; read `runpane agent-context --json`; resolve the saved base repository with `runpane repos list --json` or add it once with `runpane repos add --path <repo> --yes --json`; create one visible Pane (Pane session) for the requested feature/PR with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json` or the equivalent `--tool-command <command>` form; then validate with `runpane panels wait` or `runpane panels screen` before reporting progress.\",\n \"\",\n \"Use Pane when the user wants visible Panes or co-drivable parallel feature/PR workspaces. Do not use Pane as your default private delegation mechanism; for private background decomposition, use your normal subagent/worktree workflow.\",\n \"\",\n \"Register the main/base repository once. Do not register pre-created git worktrees as separate Pane repositories unless the user explicitly asks.\",\n \"\",\n \"Use `runpane panes create` for separate visible Panes (Pane sessions) for feature/PR work. Use `runpane panels create` for reviewer/helper tabs inside an existing Pane that should share that Pane's worktree.\",\n \"\",\n \"Typical workflow: register the saved base repository once; create one Pane (Pane session) per feature/PR; use panels/tabs inside that Pane for helper or reviewer agents that should share the worktree; archive the Pane after the PR is done to remove it from active Panes and clean up its managed worktree when applicable.\",\n \"\",\n \"Skill routing reference: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for the behavior those words imply, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the [workflow map](https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png).\",\n \"Use those peer references together to choose the phase: discuss/investigate until the work is clear enough to delegate, then ticket/plan/implement/review/PR-test/teach-back as appropriate. The orchestrator and workflow map may point to different skills; reconcile them with the user's request instead of hardcoding a skill list or treating one reference as subordinate.\",\n \"For the Pane implementation source of truth for where the skill cache, cached workflow assets, and Pane Chat bootstrap live, reference [PR #291](https://github.com/dcouple/Pane/pull/291): `main/src/services/skillCacheManager.ts` owns `<PANE_DIR>/skills/`, `.sources/dcouple-skills`, and `pane-chat/runpane-orchestrator.md`; `main/src/services/paneChatManager.ts` owns the tiny bootstrap prompt that tells the selected Pane Chat agent to read that guide.\",\n \"Use GitHub reads against the [Parsa skills folder](https://github.com/dcouple/skills/tree/main/parsa) only to inspect or refresh referenced skill files; do not clone/install the repo unless the user asks.\",\n \"Do not hardcode a specific assistant brand in workflow guidance. Use the Pane agent or custom tool command the user selected, and use `runpane agents doctor --agent <agent> --repo <selector> --json` only when checking a built-in agent template.\",\n \"\",\n \"Start with `runpane doctor --json` before taking Pane actions. Use it to understand wrapper/runtime details, daemon reachability, and the next safe commands.\",\n \"\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22: `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"\",\n \"Use `runpane agent-context --json` for full Pane CLI context. Use `runpane agent-context --command \\\"panels wait\\\" --json` or another command name for detailed schema only when needed.\",\n \"\",\n \"Default to context-safe validation: after creating Panes or sending terminal input, run `runpane panels wait` or `runpane panels screen` before reporting success. Prefer `runpane panels submit` for normal text plus Enter; use `runpane panels input` only for exact bytes such as Ctrl-C or escape sequences.\",\n \"\",\n \"Common commands:\",\n \"- `runpane doctor --json`\",\n \"- `runpane agent-context --json`\",\n \"- `runpane repos list --json`\",\n \"- `runpane repos add --path <repo> --yes --json`\",\n \"- `runpane agents doctor --agent <agent> --repo active --json`\",\n \"- `runpane panes create --repo active --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels list --pane <pane-id> --json`\",\n \"- `runpane panels screen --panel <panel-id> --limit 80 --json`\",\n \"- `runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json`\",\n \"- `runpane panels submit --panel <panel-id> --text \\\"<answer>\\\" --yes --json`\",\n \"- `runpane panels input --panel <panel-id> --input-file <path|-> --yes --json`\",\n \"\",\n \"WSL note: if `runpane doctor --json` cannot find `/tmp/pane-daemon.../daemon.sock` or `runpane` resolves to a broken Windows shim, Pane may be running on Windows. Try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`, then create Panes through the same PowerShell form using the saved WSL repo name or id. Use `runpane agents doctor --agent <agent> --repo <selector> --json` to diagnose the repo environment Pane will actually use.\"\n ]\n }\n}") diff --git a/packages/runpane/src/generated/contract.ts b/packages/runpane/src/generated/contract.ts index 9e83f6ee..1236b483 100644 --- a/packages/runpane/src/generated/contract.ts +++ b/packages/runpane/src/generated/contract.ts @@ -3268,6 +3268,34 @@ export const RUNPANE_CONTRACT = { }, "lastActivity": { "type": "string" + }, + "terminalReady": { + "type": "boolean" + }, + "agentActivity": { + "enum": [ + "unknown", + "starting", + "active", + "idle", + "exited" + ] + }, + "inputRequired": { + "type": "boolean" + }, + "blocked": { + "type": "boolean", + "description": "Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details." + }, + "hasNewOutput": { + "type": "boolean" + }, + "outputGeneration": { + "type": "number" + }, + "lastMeaningfulEventAt": { + "type": "string" } }, "additionalProperties": false @@ -3275,6 +3303,31 @@ export const RUNPANE_CONTRACT = { "initialInput": { "$ref": "#/jsonSchemas/paneCreateResult/properties/items/items/oneOf/0/properties/initialInput" }, + "blocked": { + "type": "object", + "description": "Structured blocker details. This object is distinct from state.blocked, which is only a boolean.", + "required": [ + "kind", + "message" + ], + "properties": { + "kind": { + "enum": [ + "codex-update", + "agent-prompt", + "submission_unverified", + "unknown" + ] + }, + "message": { + "type": "string" + }, + "suggestedCommand": { + "type": "string" + } + }, + "additionalProperties": false + }, "nextCommand": { "type": "string" } @@ -3402,12 +3455,41 @@ export const RUNPANE_CONTRACT = { }, "lastActivity": { "type": "string" + }, + "terminalReady": { + "type": "boolean" + }, + "agentActivity": { + "enum": [ + "unknown", + "starting", + "active", + "idle", + "exited" + ] + }, + "inputRequired": { + "type": "boolean" + }, + "blocked": { + "type": "boolean", + "description": "Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details." + }, + "hasNewOutput": { + "type": "boolean" + }, + "outputGeneration": { + "type": "number" + }, + "lastMeaningfulEventAt": { + "type": "string" } }, "additionalProperties": false }, "blocked": { "type": "object", + "description": "Structured blocker details. This object is distinct from state.blocked, which is only a boolean.", "required": [ "kind", "message" diff --git a/shared/types/generatedRunpaneContract.ts b/shared/types/generatedRunpaneContract.ts index 9e83f6ee..1236b483 100644 --- a/shared/types/generatedRunpaneContract.ts +++ b/shared/types/generatedRunpaneContract.ts @@ -3268,6 +3268,34 @@ export const RUNPANE_CONTRACT = { }, "lastActivity": { "type": "string" + }, + "terminalReady": { + "type": "boolean" + }, + "agentActivity": { + "enum": [ + "unknown", + "starting", + "active", + "idle", + "exited" + ] + }, + "inputRequired": { + "type": "boolean" + }, + "blocked": { + "type": "boolean", + "description": "Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details." + }, + "hasNewOutput": { + "type": "boolean" + }, + "outputGeneration": { + "type": "number" + }, + "lastMeaningfulEventAt": { + "type": "string" } }, "additionalProperties": false @@ -3275,6 +3303,31 @@ export const RUNPANE_CONTRACT = { "initialInput": { "$ref": "#/jsonSchemas/paneCreateResult/properties/items/items/oneOf/0/properties/initialInput" }, + "blocked": { + "type": "object", + "description": "Structured blocker details. This object is distinct from state.blocked, which is only a boolean.", + "required": [ + "kind", + "message" + ], + "properties": { + "kind": { + "enum": [ + "codex-update", + "agent-prompt", + "submission_unverified", + "unknown" + ] + }, + "message": { + "type": "string" + }, + "suggestedCommand": { + "type": "string" + } + }, + "additionalProperties": false + }, "nextCommand": { "type": "string" } @@ -3402,12 +3455,41 @@ export const RUNPANE_CONTRACT = { }, "lastActivity": { "type": "string" + }, + "terminalReady": { + "type": "boolean" + }, + "agentActivity": { + "enum": [ + "unknown", + "starting", + "active", + "idle", + "exited" + ] + }, + "inputRequired": { + "type": "boolean" + }, + "blocked": { + "type": "boolean", + "description": "Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details." + }, + "hasNewOutput": { + "type": "boolean" + }, + "outputGeneration": { + "type": "number" + }, + "lastMeaningfulEventAt": { + "type": "string" } }, "additionalProperties": false }, "blocked": { "type": "object", + "description": "Structured blocker details. This object is distinct from state.blocked, which is only a boolean.", "required": [ "kind", "message" diff --git a/shared/types/panels.ts b/shared/types/panels.ts index 8936ba17..a5289808 100644 --- a/shared/types/panels.ts +++ b/shared/types/panels.ts @@ -63,6 +63,9 @@ export interface TerminalPanelState { // CLI tool init state isCliPanel?: boolean; // True if this terminal runs a CLI tool (claude/codex) isCliReady?: boolean; // True after the CLI tool has started responding + exitedAt?: string; // Last process exit time, retained after live cleanup + exitCode?: number; // Last process exit code when available + exitSignal?: number; // Last process exit signal when available } export interface DiffPanelState { diff --git a/shared/types/runpaneOrchestration.ts b/shared/types/runpaneOrchestration.ts index 21d13e35..bd0f063b 100644 --- a/shared/types/runpaneOrchestration.ts +++ b/shared/types/runpaneOrchestration.ts @@ -109,6 +109,7 @@ export interface RunpaneErrorPayload { } export type RunpanePanelActivityStatus = 'active' | 'idle'; +export type RunpaneAgentActivity = 'unknown' | 'starting' | 'active' | 'idle' | 'exited'; export type RunpanePanelScreenSource = 'alternateScreen' | 'scrollback' | 'persistedOutput' | 'empty'; export type RunpanePanelWaitCondition = 'initialized' | 'ready' | 'idle' | 'text'; export type RunpanePanelBlockerKind = @@ -125,6 +126,13 @@ export interface RunpanePanelStateSummary { isCliPanel?: boolean; agentType?: RunpaneAgentId; lastActivity?: string; + terminalReady?: boolean; + agentActivity?: RunpaneAgentActivity; + inputRequired?: boolean; + blocked?: boolean; + hasNewOutput?: boolean; + outputGeneration?: number; + lastMeaningfulEventAt?: string; } export interface RunpanePanelBlockedState { @@ -377,6 +385,7 @@ export interface RunpanePanelScreenResult { hasMore: boolean; text: string; state: RunpanePanelStateSummary; + blocked?: RunpanePanelBlockedState; nextCommand?: string; } From 07a38723511eddbdbe23de1305ea19ab92b3dcee Mon Sep 17 00:00:00 2001 From: parsakhaz <khazapar7@gmail.com> Date: Wed, 22 Jul 2026 16:29:10 -0700 Subject: [PATCH 2/4] feat: event log, cursors, and panels watch/await for RunPane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the event-driven RunPane orchestration epic (#356). Turns phase 1's semantic state into an observable event stream, so an orchestrator can block until something meaningful happens instead of polling. - Bounded in-memory event ring, daemon-host scoped via PaneRuntime, with a global monotonic `<epoch>:<n>` cursor that doubles as the event id. A re-bootstrap mints a fresh epoch, so a pre-restart cursor yields a structured `cursor_expired` naming the earliest available cursor and a snapshot reconciliation command — never a silent empty stream (D3). - Semantic transitions emitted for panel_created, terminal_ready, prompt_staged, prompt_submitted, agent_active, agent_idle, input_required, blocked, unblocked, panel_exited and panel_archived. Blocker edges are observed continuously via a trailing-throttled scan reached by both visible and hidden terminals. - Three new commands: `panels events` (pull replay, keeping polling first-class per D8), `panels watch --jsonl --since` (long-lived JSONL subscription), and `panels await --event` (exit-on-first-match). One retained socket buffers live frames across every replay, including heartbeats, so replay and live never race. - Heartbeat reconciliation on a strictly periodic timer replays the ring from the processed cursor, so a dropped event resolves the wait instead of stalling to timeout (D9). Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport. - `panels wait` is untouched and stays frozen (D2); everything is additive, with npm, Python and generated contracts in parity (D7). Refs #356 --- contracts/runpane/contract.json | 198 ++++++- contracts/runpane/schema.json | 26 + docs/RUNPANE_CLI_CONTRACT.md | 7 + main/src/core/runtime.ts | 6 + main/src/daemon/bootstrap.ts | 7 +- main/src/ipc/runpane.test.ts | 25 + main/src/ipc/runpane.ts | 135 ++--- main/src/ipc/session.ts | 2 + main/src/services/panelManager.ts | 5 +- main/src/services/runpaneBlockerDetection.ts | 44 ++ main/src/services/runpaneEventLog.test.ts | 71 +++ main/src/services/runpaneEventLog.ts | 91 +++ main/src/services/runpanePanelState.ts | 48 ++ .../src/services/terminalPanelManager.test.ts | 30 + main/src/services/terminalPanelManager.ts | 91 ++- packages/runpane-py/src/runpane/cli.py | 41 ++ .../runpane-py/src/runpane/daemon_client.py | 78 ++- .../src/runpane/generated_contract.py | 2 +- .../runpane-py/src/runpane/local_control.py | 173 +++++- packages/runpane/package.json | 4 +- packages/runpane/src/cli.ts | 6 + packages/runpane/src/commands.ts | 36 ++ packages/runpane/src/daemonClient.ts | 109 ++++ packages/runpane/src/generated/contract.ts | 530 ++++++++++++++++++ packages/runpane/src/localControl.ts | 290 +++++++++- .../runpane/src/localControl.watch.test.ts | 233 ++++++++ packages/runpane/vitest.config.ts | 8 + pnpm-lock.yaml | 3 + scripts/fixtures/runpane-contract.json | 40 ++ scripts/generate-runpane-contract.js | 2 +- scripts/test-runpane-contract.js | 3 + shared/types/generatedRunpaneContract.ts | 530 ++++++++++++++++++ shared/types/runpaneOrchestration.ts | 78 +++ 33 files changed, 2854 insertions(+), 98 deletions(-) create mode 100644 main/src/services/runpaneBlockerDetection.ts create mode 100644 main/src/services/runpaneEventLog.test.ts create mode 100644 main/src/services/runpaneEventLog.ts create mode 100644 main/src/services/runpanePanelState.ts create mode 100644 packages/runpane/src/localControl.watch.test.ts create mode 100644 packages/runpane/vitest.config.ts diff --git a/contracts/runpane/contract.json b/contracts/runpane/contract.json index 52391021..ace36b60 100644 --- a/contracts/runpane/contract.json +++ b/contracts/runpane/contract.json @@ -46,8 +46,18 @@ "agents": [ "codex", "claude" + ], + "eventSelectors": [ + "panel-created", "terminal-ready", "prompt-staged", "prompt-submitted", "agent-active", "agent-idle", + "input-required", "blocked", "unblocked", "panel-exited", "panel-archived" ] }, + "eventSelectorMap": { + "panel-created": "panel_created", "terminal-ready": "terminal_ready", "prompt-staged": "prompt_staged", + "prompt-submitted": "prompt_submitted", "agent-active": "agent_active", "agent-idle": "agent_idle", + "input-required": "input_required", "blocked": "blocked", "unblocked": "unblocked", + "panel-exited": "panel_exited", "panel-archived": "panel_archived" + }, "agentTemplates": { "codex": { "title": "Codex", @@ -306,6 +316,28 @@ "jsonSchemas": [ "panelWaitResult" ] + }, + { + "name": "panels events", + "summary": "Replay retained semantic panel events after a cursor.", + "usage": ["runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]"] , + "exitCodes": {"0":"success", "1":"transport or other error", "3":"cursor expired"}, + "jsonSchemas": ["panelEventsResult"] + }, + { + "name": "panels watch", + "summary": "Stream semantic panel events as compact JSONL.", + "usage": ["runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]"], + "outputMode": "jsonl", + "exitCodes": {"0":"clean stop", "1":"transport or other error", "3":"cursor expired"}, + "jsonSchemas": ["semanticEvent", "cursorExpiredError"] + }, + { + "name": "panels await", + "summary": "Wait for the first matching semantic panel event.", + "usage": ["runpane panels await --panel <panel-id> --event <selector> [--since <cursor>] [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]"], + "exitCodes": {"0":"matched", "1":"transport or other error", "2":"timed out", "3":"cursor expired"}, + "jsonSchemas": ["panelAwaitResult", "cursorExpiredError"] } ], "flags": { @@ -506,6 +538,21 @@ "value": "<milliseconds>", "description": "Polling interval for panels wait." }, + { + "name": "--event", + "value": "<selector>", + "description": "Semantic event selector in kebab-case." + }, + { + "name": "--since", + "value": "<cursor>", + "description": "Replay events strictly after this cursor." + }, + { + "name": "--heartbeat-ms", + "value": "<milliseconds>", + "description": "Periodic event-log and state reconciliation interval." + }, { "name": "--text", "value": "<text>", @@ -551,6 +598,10 @@ { "name": "--force", "description": "Archive even if the pane's branch has uncommitted, untracked, or unpushed changes." + }, + { + "name": "--jsonl", + "description": "Print one compact JSON object per line (panels watch always uses JSONL)." } ] }, @@ -579,6 +630,9 @@ " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", " runpane help [command]", "", "Quick start:", @@ -819,6 +873,9 @@ " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", "", "Run \"runpane help panels create\" or another command-specific topic for options." ], @@ -929,6 +986,18 @@ " --pane-dir <path> Connect to a specific Pane data directory", " --json Print machine-readable output" ], + "panels events": [ + "Usage:", " runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", + "", "Replays retained semantic events strictly after a cursor.", "", "Exit codes: 0 success, 3 cursor expired, 1 transport/error." + ], + "panels watch": [ + "Usage:", " runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", + "", "Streams one compact semantic event JSON object per stdout line.", "", "Exit codes: 0 clean stop, 3 cursor expired, 1 transport/error." + ], + "panels await": [ + "Usage:", " runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]", + "", "Waits for a matching semantic event and periodically reconciles state.", "", "Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport/error." + ], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -988,6 +1057,9 @@ " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", " runpane help [command]", "", "Quick start:", @@ -1217,6 +1289,9 @@ " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", "", "Run \"runpane help panels create\" or another command-specific topic for options." ], @@ -1327,6 +1402,9 @@ " --pane-dir <path> Connect to a specific Pane data directory", " --json Print machine-readable output" ], + "panels events": ["Usage:", " python -m runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", "", "Replays retained semantic events strictly after a cursor."], + "panels watch": ["Usage:", " python -m runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", "", "Streams one compact semantic event JSON object per stdout line."], + "panels await": ["Usage:", " python -m runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]", "", "Waits for a matching semantic event and periodically reconciles state."], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -1763,7 +1841,10 @@ "auto", "--yes", "--json" - ] + ], + ["panels", "events", "--panel", "panel-1", "--event", "agent-idle", "--since", "epoch:1", "--json"], + ["panels", "watch", "--panel", "panel-1", "--event", "blocked", "--since", "epoch:2", "--heartbeat-ms", "1000", "--jsonl"], + ["panels", "await", "--panel", "panel-1", "--event", "agent-idle", "--timeout-ms", "5000", "--heartbeat-ms", "500", "--json"] ], "topLevelHelpIncludes": [ "runpane setup", @@ -1786,6 +1867,9 @@ "runpane panels submit", "runpane panels submit-composer", "runpane panels wait", + "runpane panels events", + "runpane panels watch", + "runpane panels await", "runpane doctor --json", "runpane agent-context --json", "Agent discovery:", @@ -3382,6 +3466,67 @@ }, "additionalProperties": false }, + "semanticEvent": { + "type": "object", + "required": ["id", "cursor", "type", "at", "panelId", "state"], + "properties": { + "id": { "type": "string" }, + "cursor": { "type": "string" }, + "type": { "enum": ["panel_created", "terminal_ready", "prompt_staged", "prompt_submitted", "agent_active", "agent_idle", "input_required", "blocked", "unblocked", "panel_exited", "panel_archived"] }, + "at": { "type": "string" }, + "paneId": { "type": "string" }, + "panelId": { "type": "string" }, + "state": { "$ref": "#/jsonSchemas/panelWaitResult/properties/state" }, + "resolvedBy": { "enum": ["event", "reconciliation"] } + }, + "additionalProperties": false + }, + "cursorExpiredError": { + "type": "object", + "required": ["code", "earliestCursor", "reconcileCommand"], + "properties": { + "code": { "enum": ["cursor_expired"] }, + "earliestCursor": { "type": "string" }, + "reconcileCommand": { "type": "string" } + }, + "additionalProperties": false + }, + "panelEventsResult": { + "oneOf": [ + { + "type": "object", + "required": ["ok", "events", "cursor"], + "properties": { + "ok": { "enum": [true] }, + "events": { "type": "array", "items": { "$ref": "#/jsonSchemas/semanticEvent" } }, + "cursor": { "type": "string" } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["ok", "error"], + "properties": { + "ok": { "enum": [false] }, + "error": { "$ref": "#/jsonSchemas/cursorExpiredError" } + }, + "additionalProperties": false + } + ] + }, + "panelAwaitResult": { + "type": "object", + "required": ["ok", "timedOut", "state"], + "properties": { + "ok": { "type": "boolean" }, + "timedOut": { "type": "boolean" }, + "matchedEvent": { "enum": ["panel_created", "terminal_ready", "prompt_staged", "prompt_submitted", "agent_active", "agent_idle", "input_required", "blocked", "unblocked", "panel_exited", "panel_archived"] }, + "resolvedBy": { "enum": ["event", "reconciliation"] }, + "event": { "$ref": "#/jsonSchemas/semanticEvent" }, + "state": { "$ref": "#/jsonSchemas/panelWaitResult/properties/state" } + }, + "additionalProperties": false + }, "panelWaitResult": { "type": "object", "required": [ @@ -4126,6 +4271,18 @@ "--json", "--pane-dir <path>" ] + }, + { + "name": "panels events", "summary": "Replay semantic panel events after a cursor.", + "arguments": ["--panel <panel-id>", "--event <selector>", "--since <cursor>", "--json"] + }, + { + "name": "panels watch", "summary": "Stream semantic panel events as JSONL.", + "arguments": ["--panel <panel-id>", "--event <selector>", "--since <cursor>", "--heartbeat-ms <ms>", "--jsonl"] + }, + { + "name": "panels await", "summary": "Wait for a matching semantic panel event.", + "arguments": ["--panel <panel-id>", "--event <selector>", "--since <cursor>", "--timeout-ms <ms>", "--heartbeat-ms <ms>", "--json"] } ] }, @@ -5061,6 +5218,45 @@ "The default timeout and screen are intentionally small for agent context safety." ] }, + "panels events": { + "name": "panels events", "summary": "Replay retained semantic panel events after a cursor.", + "details": "Request/response pull surface for the bounded semantic event ring.", "requiresPaneDaemon": true, "mutates": false, + "arguments": [ + {"name":"--panel","value":"<panel-id>","required":false,"description":"Optional panel filter."}, + {"name":"--event","value":"<selector>","required":false,"description":"Optional semantic event selector."}, + {"name":"--since","value":"<cursor>","required":false,"description":"Exclusive replay cursor."}, + {"name":"--json","required":false,"description":"Print machine-readable output."} + ], + "examples": ["runpane panels events --since <cursor> --json"], "jsonSchemas": ["panelEventsResult"], + "notes": ["Exit code 3 means cursor_expired; reconcile from the command in the error."] + }, + "panels watch": { + "name": "panels watch", "summary": "Stream semantic panel events as JSONL.", + "details": "Uses replay then live delivery on one retained daemon connection with periodic reconciliation.", "requiresPaneDaemon": true, "mutates": false, + "arguments": [ + {"name":"--panel","value":"<panel-id>","required":false,"description":"Optional panel filter."}, + {"name":"--event","value":"<selector>","required":false,"description":"Optional semantic event selector."}, + {"name":"--since","value":"<cursor>","required":false,"description":"Exclusive replay cursor."}, + {"name":"--heartbeat-ms","value":"<ms>","required":false,"description":"Reconciliation interval."}, + {"name":"--jsonl","required":false,"description":"Describes the always-JSONL output."} + ], + "examples": ["runpane panels watch --panel <panel-id> --jsonl"], "jsonSchemas": ["semanticEvent", "cursorExpiredError"], + "notes": ["Stdout contains compact JSON records only. Exit codes: 0 clean stop, 1 transport error, 3 cursor expired."] + }, + "panels await": { + "name": "panels await", "summary": "Wait for the first matching semantic event.", + "details": "Exit-on-first-match sugar over watch with periodic state reconciliation.", "requiresPaneDaemon": true, "mutates": false, + "arguments": [ + {"name":"--panel","value":"<panel-id>","required":true,"description":"Panel to await."}, + {"name":"--event","value":"<selector>","required":true,"description":"Semantic event selector."}, + {"name":"--since","value":"<cursor>","required":false,"description":"Exclusive replay cursor."}, + {"name":"--timeout-ms","value":"<ms>","required":false,"description":"Wait timeout."}, + {"name":"--heartbeat-ms","value":"<ms>","required":false,"description":"Reconciliation interval."}, + {"name":"--json","required":false,"description":"Print machine-readable output."} + ], + "examples": ["runpane panels await --panel <panel-id> --event agent-idle --json"], "jsonSchemas": ["panelAwaitResult", "cursorExpiredError"], + "notes": ["Exit codes: 0 matched, 1 transport error, 2 timed out, 3 cursor expired."] + }, "panels create": { "name": "panels create", "summary": "Create a reviewer/helper terminal tab inside an existing Pane.", diff --git a/contracts/runpane/schema.json b/contracts/runpane/schema.json index ce95f20f..648653b2 100644 --- a/contracts/runpane/schema.json +++ b/contracts/runpane/schema.json @@ -141,10 +141,17 @@ }, "agents": { "$ref": "#/$defs/stringList" + }, + "eventSelectors": { + "$ref": "#/$defs/stringList" } }, "additionalProperties": false }, + "eventSelectorMap": { + "type": "object", + "additionalProperties": { "type": "string" } + }, "agentTemplates": { "type": "object", "additionalProperties": { @@ -283,6 +290,13 @@ }, "jsonSchemas": { "$ref": "#/$defs/stringList" + }, + "outputMode": { + "enum": ["jsonl"] + }, + "exitCodes": { + "type": "object", + "additionalProperties": { "type": "string" } } }, "additionalProperties": false @@ -361,6 +375,9 @@ "panels submit", "panels submit-composer", "panels wait", + "panels events", + "panels watch", + "panels await", "agent-context" ], "properties": { @@ -436,6 +453,15 @@ "panels wait": { "$ref": "#/$defs/helpLines" }, + "panels events": { + "$ref": "#/$defs/helpLines" + }, + "panels watch": { + "$ref": "#/$defs/helpLines" + }, + "panels await": { + "$ref": "#/$defs/helpLines" + }, "agent-context": { "$ref": "#/$defs/helpLines" } diff --git a/docs/RUNPANE_CLI_CONTRACT.md b/docs/RUNPANE_CLI_CONTRACT.md index 2b06c98c..247cbd31 100644 --- a/docs/RUNPANE_CLI_CONTRACT.md +++ b/docs/RUNPANE_CLI_CONTRACT.md @@ -201,6 +201,9 @@ Brief tools: - `panels submit`: Send text plus terminal Enter to a terminal panel. - `panels submit-composer`: Submit an agent composer with the correct key sequence, including Ctrl+Enter for Codex. - `panels wait`: Wait for terminal initialized, ready, idle, or text state with compact output. +- `panels events`: Replay semantic panel events after a cursor. +- `panels watch`: Stream semantic panel events as JSONL. +- `panels await`: Wait for a matching semantic panel event. Managed AGENTS.md block body: @@ -296,6 +299,9 @@ These flags are consumed by local daemon-control commands: --for <initialized|ready|idle|text> --contains <text> --interval-ms <milliseconds> +--event <selector> +--since <cursor> +--heartbeat-ms <milliseconds> --text <text> --input-file <path|-> --source <user|agent> @@ -306,6 +312,7 @@ These flags are consumed by local daemon-control commands: --focus --pinned --force +--jsonl ``` `runpane doctor --json`, `runpane repos list`, `runpane panes list`, `runpane panes create`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues. diff --git a/main/src/core/runtime.ts b/main/src/core/runtime.ts index 7ea04116..43289401 100644 --- a/main/src/core/runtime.ts +++ b/main/src/core/runtime.ts @@ -1,6 +1,7 @@ import type { ConfigManager } from '../services/configManager'; import type { PtyHostSpawnOpts } from '../ptyHost/types'; import { noopPaneEventSink, type PaneEventSink } from './eventSink'; +import type { RunpaneEventLog } from '../services/runpaneEventLog'; export interface PaneWebviewContext { panelId: string; @@ -53,6 +54,7 @@ export interface PaneRuntime { */ daemonEventSink?: PaneEventSink; getConfigManager(): ConfigManager; + getRunpaneEventLog(): RunpaneEventLog; getPtyHostRuntime(): PtyHostRuntime | null; getWebviewContextMap(): Map<number, PaneWebviewContext>; } @@ -83,6 +85,10 @@ export function getRuntimeConfigManager(): ConfigManager { return getPaneRuntime().getConfigManager(); } +export function getRuntimeRunpaneEventLog(): RunpaneEventLog { + return getPaneRuntime().getRunpaneEventLog(); +} + export function getPtyHostRuntime(): PtyHostRuntime | null { return getPaneRuntime().getPtyHostRuntime(); } diff --git a/main/src/daemon/bootstrap.ts b/main/src/daemon/bootstrap.ts index 32c87bc6..5ca59a30 100644 --- a/main/src/daemon/bootstrap.ts +++ b/main/src/daemon/bootstrap.ts @@ -35,6 +35,7 @@ import { setupEventListeners } from '../events'; import { getAppDirectory } from '../utils/appDirectory'; import { resourceMonitorService } from '../services/resourceMonitorService'; import type { PaneCommandRegistry } from './commandRegistry'; +import { RunpaneEventLog } from '../services/runpaneEventLog'; interface PaneDaemonHostOptions { app: App; @@ -65,11 +66,13 @@ function installPaneRuntime( getPtyHostRuntime: () => PtyHostRuntime | null, getWebviewContextMap: () => Map<number, PaneWebviewContext>, daemonEventSink?: PaneEventSink, + runpaneEventLog = new RunpaneEventLog(), ): void { setPaneRuntime({ eventSink, daemonEventSink, getConfigManager: () => configManager, + getRunpaneEventLog: () => runpaneEventLog, getPtyHostRuntime, getWebviewContextMap, }); @@ -93,10 +96,11 @@ export async function createPaneDaemonHost(options: PaneDaemonHostOptions): Prom const rendererEventSink = options.rendererEventSink ?? noopPaneEventSink; const headlessWebviewContextMap = new Map<number, PaneWebviewContext>(); const getWebviewContextMap = options.getWebviewContextMap ?? (() => headlessWebviewContextMap); + const runpaneEventLog = new RunpaneEventLog(); const configManager = new ConfigManager(); await configManager.initialize(); - installPaneRuntime(rendererEventSink, configManager, options.getPtyHostRuntime, getWebviewContextMap); + installPaneRuntime(rendererEventSink, configManager, options.getPtyHostRuntime, getWebviewContextMap, undefined, runpaneEventLog); const logger = new Logger(configManager); console.log('[Main] Logger initialized with file logging to ~/.pane/logs'); @@ -256,6 +260,7 @@ export async function createPaneDaemonHost(options: PaneDaemonHostOptions): Prom options.getPtyHostRuntime, getWebviewContextMap, createFanoutEventSink(daemonSinks), + runpaneEventLog, ); setupEventListeners(services); diff --git a/main/src/ipc/runpane.test.ts b/main/src/ipc/runpane.test.ts index 256bf7b6..e1ebb151 100644 --- a/main/src/ipc/runpane.test.ts +++ b/main/src/ipc/runpane.test.ts @@ -10,6 +10,10 @@ import type { AppServices } from './types'; import type { CreatePanelRequest, ToolPanel } from '../../../shared/types/panels'; import type { RunpaneToolSpec } from '../../../shared/types/runpaneOrchestration'; +const semanticEventAppend = vi.hoisted(() => vi.fn()); +const semanticEventReplay = vi.hoisted(() => vi.fn()); +const semanticCurrentCursor = vi.hoisted(() => vi.fn(() => 'epoch:0')); + vi.mock('../services/panelManager', () => ({ panelManager: { createPanel: vi.fn(), @@ -33,6 +37,10 @@ vi.mock('../services/terminalPanelManager', () => ({ }, })); +vi.mock('../core/runtime', () => ({ + getRuntimeRunpaneEventLog: () => ({ append: semanticEventAppend, replaySince: semanticEventReplay, currentCursor: semanticCurrentCursor }), +})); + import { RUNPANE_CONTRACT } from '../../../shared/types/generatedRunpaneContract'; import { panelManager } from '../services/panelManager'; import { terminalPanelManager } from '../services/terminalPanelManager'; @@ -229,6 +237,9 @@ function registerSessionsDeleteStub( describe('runpane IPC handlers', () => { beforeEach(() => { + semanticEventAppend.mockReset(); + semanticEventReplay.mockReset(); + semanticCurrentCursor.mockReturnValue('epoch:0'); session.isFavorite = undefined; session.favoritePinnedAt = undefined; vi.mocked(panelManager.createPanel).mockReset(); @@ -298,6 +309,20 @@ describe('runpane IPC handlers', () => { expect((result as { daemon: { channels: string[] } }).daemon.channels).toContain('runpane:doctor'); }); + it('AC2/AC3 replays each compact semantic transition strictly after the requested cursor with no screen text', async () => { + const registry = createRegistry(); + const event = { + id: 'epoch:2', cursor: 'epoch:2', type: 'agent_idle', at: '2026-01-01T00:00:00.000Z', + paneId: 'session-1', panelId: 'panel-1', state: { initialized: true, agentActivity: 'idle' }, + }; + semanticEventReplay.mockReturnValue({ ok: true, events: [event], cursor: 'epoch:2' }); + const result = await registry.invoke('runpane:panels:events', [{ since: 'epoch:1', panelId: 'panel-1' }]); + expect(semanticEventReplay).toHaveBeenCalledWith('epoch:1'); + expect(result).toEqual({ ok: true, events: [event], cursor: 'epoch:2' }); + expect(JSON.stringify(result)).not.toContain('screen'); + expect(semanticEventAppend).not.toHaveBeenCalled(); + }); + it('lists saved Pane repositories with session counts', async () => { const registry = createRegistry(); diff --git a/main/src/ipc/runpane.ts b/main/src/ipc/runpane.ts index 04745711..f53c0fbc 100644 --- a/main/src/ipc/runpane.ts +++ b/main/src/ipc/runpane.ts @@ -11,6 +11,9 @@ import { terminalPanelManager, type TerminalPanelSnapshot } from '../services/te import { ensureProjectAgentContext } from '../services/agentContextManager'; import { fastCheckWorkingDirectory, fastGetAheadBehind } from '../services/gitPlumbingCommands'; import { assessComposerEvidence, isSlashCommandInput } from './runpaneComposerEvidence'; +import { boundSanitizedLines, detectPanelBlocker } from '../services/runpaneBlockerDetection'; +import { getTerminalCustomState, panelStateSummary } from '../services/runpanePanelState'; +import { getRuntimeRunpaneEventLog } from '../core/runtime'; import type { ArchiveProgressManager, SerializedArchiveTask } from '../services/archiveProgressManager'; import type { Project } from '../database/models'; import type { Session, SessionOutput } from '../types/session'; @@ -68,7 +71,10 @@ import type { RunpaneResolvedTool, RunpaneToolSpec, RunpaneWorktreeCleanupState, + RunpanePanelsEventsRequest, + RunpanePanelsEventsResponse, } from '../../../shared/types/runpaneOrchestration'; +import { RUNPANE_EVENT_SELECTOR_TO_TYPE } from '../../../shared/types/runpaneOrchestration'; const RUNPANE_CHANNELS = [ 'runpane:doctor', @@ -86,6 +92,7 @@ const RUNPANE_CHANNELS = [ 'runpane:panels:submit', 'runpane:panels:submit-composer', 'runpane:panels:wait', + 'runpane:panels:events', 'runpane:agents:doctor', ] as const; @@ -141,6 +148,25 @@ export function registerRunpaneHandlers( }, result => ({ resultCount: result.repos.count })); }); + commandRegistry.register('runpane:panels:events', (request: unknown): RunpanePanelsEventsResponse => { + const normalized = parsePanelsEventsRequest(request); + const log = getRuntimeRunpaneEventLog(); + const replay = log.replaySince(normalized.since ?? log.currentCursor()); + if (!replay.ok) { + return normalized.panelId + ? { ...replay, error: { ...replay.error, reconcileCommand: panelScreenCommand(normalized.panelId) } } + : replay; + } + const eventType = normalized.event ? RUNPANE_EVENT_SELECTOR_TO_TYPE[normalized.event] : undefined; + return { + ...replay, + events: replay.events.filter((event) => + (!normalized.panelId || event.panelId === normalized.panelId) && + (!eventType || event.type === eventType) + ), + }; + }); + commandRegistry.register('runpane:repos:list', async (): Promise<RunpaneRepoListResult> => { return withRunpaneAction(services, 'repos:list', {}, () => { const repos = databaseService.getAllProjects().map((project) => @@ -738,6 +764,9 @@ async function submitCreateInitialInput( const sentAt = optionalString(customState.initialInputSentAt); const deliveryError = optionalString(customState.initialInputError); const delivered = Boolean(sentAt) && !deliveryError; + if (delivered && currentPanel) { + getRuntimeRunpaneEventLog().append('prompt_submitted', currentPanel, { paneId: panel.sessionId }); + } return { delivered, submitted: delivered, @@ -772,6 +801,7 @@ async function submitCreateInitialInput( } terminalPanelManager.writeToTerminal(panel.id, tool.initialInput); + getRuntimeRunpaneEventLog().append('prompt_staged', panel, { paneId: panel.sessionId }); await sleep(300); return submitCreateComposerInput(panel, tool); } @@ -830,6 +860,7 @@ async function submitCreateComposerInput( }); if (lastVerdict === 'cleared' && afterScreen.state.activityStatus === 'active') { + getRuntimeRunpaneEventLog().append('prompt_submitted', panel, { paneId: panel.sessionId }); return { delivered: true, submitted: true, @@ -1031,11 +1062,7 @@ async function buildPanelScreenResult(panel: ToolPanel, limit: number): Promise< const { source, rawText } = selectPanelScreenText(liveSnapshot, customState); const blockerWindow = boundSanitizedLines(rawText, DEFAULT_PANEL_SCREEN_LIMIT); const blocked = detectPanelBlocker(blockerWindow.text, baseState.agentType, panel.id); - const state: RunpanePanelStateSummary = { - ...baseState, - blocked: blocked !== undefined, - inputRequired: blocked?.kind === 'agent-prompt' || blocked?.kind === 'codex-update', - }; + const state = panelStateSummary(panel, liveSnapshot, customState, blocked); const bounded = boundSanitizedLines(rawText, limit); return { @@ -1086,40 +1113,6 @@ function selectPanelScreenText( return { source: 'empty', rawText: '' }; } -function panelStateSummary( - panel: ToolPanel, - snapshot: TerminalPanelSnapshot | null, - customState: TerminalPanelState = getTerminalCustomState(panel), -): RunpanePanelStateSummary { - const customAgentType = typeof customState.agentType === 'string' && AGENT_IDS.has(customState.agentType) - ? customState.agentType as RunpaneAgentId - : undefined; - const hasLiveTerminal = Boolean(snapshot || terminalPanelManager.isTerminalInitialized(panel.id)); - const agentActivity = snapshot?.agentActivity - ?? (customState.exitedAt ? 'exited' : 'unknown'); - - return { - initialized: hasLiveTerminal, - isAlternateScreen: snapshot?.isAlternateScreen ?? customState.isAlternateScreen, - activityStatus: snapshot?.activityStatus, - isCliReady: snapshot?.isCliReady ?? (hasLiveTerminal ? customState.isCliReady : undefined), - isCliPanel: snapshot?.isCliPanel ?? customState.isCliPanel, - agentType: snapshot?.agentType ?? customAgentType, - lastActivity: snapshot?.lastActivityTime ?? customState.lastActivityTime ?? toIsoString(panel.metadata.lastActiveAt), - terminalReady: snapshot?.terminalReady, - agentActivity, - hasNewOutput: snapshot - ? snapshot.outputGeneration > snapshot.outputGenerationAtQuiescence - : false, - outputGeneration: snapshot?.outputGeneration, - lastMeaningfulEventAt: snapshot?.lastMeaningfulEventAt ?? customState.exitedAt, - }; -} - -function getTerminalCustomState(panel: ToolPanel): TerminalPanelState { - return (isRecord(panel.state.customState) ? panel.state.customState : {}) as TerminalPanelState; -} - function normalizeScrollbackBuffer(value: TerminalPanelState['scrollbackBuffer']): string { if (typeof value === 'string') { return value; @@ -1130,22 +1123,6 @@ function normalizeScrollbackBuffer(value: TerminalPanelState['scrollbackBuffer'] return ''; } -function boundSanitizedLines(rawText: string, limit: number): { text: string; hasMore: boolean; returnedLineCount: number } { - const stripped = sanitizeTerminalOutput(rawText); - if (!stripped) { - return { text: '', hasMore: false, returnedLineCount: 0 }; - } - - const allLines = stripped.split('\n'); - const hasMore = allLines.length > limit; - const lines = hasMore ? allLines.slice(-limit) : allLines; - return { - text: lines.join('\n'), - hasMore, - returnedLineCount: lines.length, - }; -} - async function waitForPanel(panel: ToolPanel, request: RunpanePanelWaitRequest): Promise<RunpanePanelWaitResult> { const startedAt = Date.now(); const timeoutMs = request.timeoutMs ?? DEFAULT_PANEL_WAIT_TIMEOUT_MS; @@ -1223,36 +1200,6 @@ function panelWaitResult( }; } -function detectPanelBlocker( - text: string, - agentType: RunpaneAgentId | undefined, - panelId: string, -): RunpanePanelBlockedState | undefined { - if (!text) return undefined; - - if ( - (agentType === 'codex' || /codex/i.test(text)) && - /update available/i.test(text) && - (/skip/i.test(text) || /npm install -g @openai\/codex/i.test(text)) - ) { - return { - kind: 'codex-update', - message: 'Codex is showing an update prompt instead of accepting the task prompt.', - suggestedCommand: `runpane panels submit --panel ${panelId} --text "2" --yes --json`, - }; - } - - if (/press enter to continue/i.test(text)) { - return { - kind: 'agent-prompt', - message: 'The terminal is waiting at an interactive prompt.', - suggestedCommand: panelScreenCommand(panelId), - }; - } - - return undefined; -} - function ensureSubmitEnter(input: string): string { if (input.endsWith('\r\n')) { return `${input.slice(0, -2)}\r`; @@ -1298,6 +1245,9 @@ async function submitComposerForPanel( const submit = resolveComposerSubmit(strategy, state.agentType); terminalPanelManager.writeToTerminal(panel.id, submit.input); const verification = await verifyComposerSubmitted(panel, beforeScreen); + if (verification.verifiedSubmitted) { + getRuntimeRunpaneEventLog().append('prompt_submitted', panel, { paneId: panel.sessionId }); + } return { ok: verification.ok, @@ -1735,6 +1685,21 @@ function parsePanelWaitRequest(value: unknown): RunpanePanelWaitRequest { }; } +function parsePanelsEventsRequest(value: unknown): RunpanePanelsEventsRequest { + if (!isRecord(value)) throw new Error('Panel events request must be an object'); + const panelId = optionalString(value.panelId)?.trim(); + const since = optionalString(value.since)?.trim(); + const event = optionalString(value.event)?.trim(); + if (event && !Object.prototype.hasOwnProperty.call(RUNPANE_EVENT_SELECTOR_TO_TYPE, event)) { + throw new Error(`Unknown panel event selector: ${event}`); + } + return { + panelId: panelId || undefined, + since: since || undefined, + event: event as RunpanePanelsEventsRequest['event'], + }; +} + function parseAgentDoctorRequest(value: unknown): RunpaneAgentDoctorRequest { if (!isRecord(value)) { throw new Error('Agent doctor request must be an object'); diff --git a/main/src/ipc/session.ts b/main/src/ipc/session.ts index 032d9bbd..ce584dd5 100644 --- a/main/src/ipc/session.ts +++ b/main/src/ipc/session.ts @@ -27,6 +27,7 @@ import { } from '../utils/sessionValidation'; import type { SerializedArchiveTask } from '../services/archiveProgressManager'; import { detectProjectConfig } from '../services/projectConfigDetector'; +import { getRuntimeRunpaneEventLog } from '../core/runtime'; const DAEMON_SESSION_CHANNELS = [ 'sessions:get-all', @@ -405,6 +406,7 @@ export function registerSessionHandlers( for (const panel of panels) { try { if (panel.type === 'terminal') { + getRuntimeRunpaneEventLog().append('panel_archived', panel, { paneId: sessionId }); terminalPanelManager.destroyTerminal(panel.id); } } catch (panelError) { diff --git a/main/src/services/panelManager.ts b/main/src/services/panelManager.ts index ab01a0d2..42fee585 100644 --- a/main/src/services/panelManager.ts +++ b/main/src/services/panelManager.ts @@ -1,6 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; import { ToolPanel, CreatePanelRequest, PanelEventType, ToolPanelState, ToolPanelMetadata, ToolPanelType, LogsPanelState } from '../../../shared/types/panels'; -import { getPaneEventSink, getPaneWebviewContextMap } from '../core/runtime'; +import { getPaneEventSink, getPaneWebviewContextMap, getRuntimeRunpaneEventLog } from '../core/runtime'; import { databaseService } from './database'; import { panelEventBus } from './panelEventBus'; import { withLock } from '../utils/mutex'; @@ -145,6 +145,9 @@ export class PanelManager { // Emit IPC event to notify frontend this.sendRendererEvent('panel:created', panel); + if (panel.type === 'terminal') { + getRuntimeRunpaneEventLog().append('panel_created', panel, { paneId: panel.sessionId }); + } // Track terminal panel creation analytics (only for new panels, not restoration) if (request.type === 'terminal' && this.analyticsManager) { diff --git a/main/src/services/runpaneBlockerDetection.ts b/main/src/services/runpaneBlockerDetection.ts new file mode 100644 index 00000000..4d2165c4 --- /dev/null +++ b/main/src/services/runpaneBlockerDetection.ts @@ -0,0 +1,44 @@ +import type { + RunpaneAgentId, + RunpanePanelBlockedState, +} from '../../../shared/types/runpaneOrchestration'; +import { sanitizeTerminalOutput } from '../utils/terminalOutputSanitizer'; + +export function boundSanitizedLines( + rawText: string, + limit: number, +): { text: string; hasMore: boolean; returnedLineCount: number } { + const stripped = sanitizeTerminalOutput(rawText); + if (!stripped) return { text: '', hasMore: false, returnedLineCount: 0 }; + const allLines = stripped.split('\n'); + const hasMore = allLines.length > limit; + const lines = hasMore ? allLines.slice(-limit) : allLines; + return { text: lines.join('\n'), hasMore, returnedLineCount: lines.length }; +} + +export function detectPanelBlocker( + text: string, + agentType: RunpaneAgentId | undefined, + panelId: string, +): RunpanePanelBlockedState | undefined { + if (!text) return undefined; + if ( + (agentType === 'codex' || /codex/i.test(text)) && + /update available/i.test(text) && + (/skip/i.test(text) || /npm install -g @openai\/codex/i.test(text)) + ) { + return { + kind: 'codex-update', + message: 'Codex is showing an update prompt instead of accepting the task prompt.', + suggestedCommand: `runpane panels submit --panel ${panelId} --text "2" --yes --json`, + }; + } + if (/press enter to continue/i.test(text)) { + return { + kind: 'agent-prompt', + message: 'The terminal is waiting at an interactive prompt.', + suggestedCommand: `runpane panels screen --panel ${panelId} --json`, + }; + } + return undefined; +} diff --git a/main/src/services/runpaneEventLog.test.ts b/main/src/services/runpaneEventLog.test.ts new file mode 100644 index 00000000..4af9b13f --- /dev/null +++ b/main/src/services/runpaneEventLog.test.ts @@ -0,0 +1,71 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { ToolPanel } from '../../../shared/types/panels'; +import { noopPaneEventSink } from '../core/eventSink'; +import { resetPaneRuntimeForTests, setPaneRuntime } from '../core/runtime'; +import { RunpaneEventLog } from './runpaneEventLog'; + +vi.mock('./terminalPanelManager', () => ({ + terminalPanelManager: { + getTerminalSnapshot: vi.fn(() => null), + getLastKnownBlocker: vi.fn(() => undefined), + isTerminalInitialized: vi.fn(() => false), + }, +})); + +const panel: ToolPanel = { + id: 'panel-1', sessionId: 'pane-1', type: 'terminal', title: 'Terminal', + state: { isActive: true, customState: {} }, + metadata: { createdAt: new Date().toISOString(), lastActiveAt: new Date().toISOString(), position: 0 }, +}; + +function install(log: RunpaneEventLog): void { + setPaneRuntime({ + eventSink: noopPaneEventSink, daemonEventSink: noopPaneEventSink, + getConfigManager: () => ({}) as never, + getRunpaneEventLog: () => log, + getPtyHostRuntime: () => null, + getWebviewContextMap: () => new Map(), + }); +} + +describe('RunpaneEventLog', () => { + beforeEach(() => resetPaneRuntimeForTests()); + afterEach(() => resetPaneRuntimeForTests()); + + it('AC4 replays strictly after a cursor in order and preserves duplicate-identifying ids', () => { + const log = new RunpaneEventLog(5, 'epoch-a'); install(log); + const first = log.append('panel_created', panel); + const second = log.append('agent_active', panel); + const third = log.append('agent_idle', panel); + const replay = log.replaySince(first.cursor); + expect(replay.ok && replay.events).toEqual([second, third]); + const repeated = log.replaySince(first.cursor); + expect(repeated.ok && repeated.events.map(event => event.id)).toEqual([second.id, third.id]); + }); + + it('AC5 returns cursor_expired with the earliest cursor when retention ages a cursor out', () => { + const log = new RunpaneEventLog(2, 'epoch-a'); install(log); + const aged = log.currentCursor(); + log.append('panel_created', panel); log.append('agent_active', panel); log.append('agent_idle', panel); + expect(log.replaySince(aged)).toEqual({ ok: false, error: { + code: 'cursor_expired', earliestCursor: 'epoch-a:2', reconcileCommand: 'runpane panels screen --panel <panel-id> --json', + }}); + }); + + it('AC6 rejects foreign pre-restart, future, and malformed cursors after runtime re-bootstrap', () => { + const first = new RunpaneEventLog(5, 'epoch-before'); install(first); + const stale = first.append('panel_created', panel).cursor; + resetPaneRuntimeForTests(); + const second = new RunpaneEventLog(5, 'epoch-after'); install(second); + expect(second.earliestCursor()).toBe('epoch-after:0'); + for (const cursor of [stale, 'epoch-after:1', 'malformed']) { + const result = second.replaySince(cursor); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe('cursor_expired'); + expect(result.error.earliestCursor).toBe('epoch-after:0'); + expect(result.error.reconcileCommand).toContain('panels screen'); + } + } + }); +}); diff --git a/main/src/services/runpaneEventLog.ts b/main/src/services/runpaneEventLog.ts new file mode 100644 index 00000000..4da072d5 --- /dev/null +++ b/main/src/services/runpaneEventLog.ts @@ -0,0 +1,91 @@ +import { randomUUID } from 'crypto'; +import type { ToolPanel } from '../../../shared/types/panels'; +import type { + RunpaneCursorExpiredError, + RunpanePanelsEventsResponse, + RunpaneSemanticEvent, + RunpaneSemanticEventType, +} from '../../../shared/types/runpaneOrchestration'; +import { getPaneDaemonEventSink } from '../core/runtime'; +import { panelStateSummary } from './runpanePanelState'; +import { terminalPanelManager } from './terminalPanelManager'; + +export const RUNPANE_EVENT_LOG_CAPACITY = 5000; + +export class RunpaneEventLog { + private readonly epoch: string; + private readonly capacity: number; + private counter = 0; + private events: RunpaneSemanticEvent[] = []; + + constructor(capacity = RUNPANE_EVENT_LOG_CAPACITY, epoch = randomUUID()) { + this.capacity = capacity; + this.epoch = epoch; + } + + currentCursor(): string { + return `${this.epoch}:${this.counter}`; + } + + earliestCursor(): string { + return this.events[0]?.cursor ?? `${this.epoch}:0`; + } + + append(type: RunpaneSemanticEventType, panel: ToolPanel, options: { paneId?: string } = {}): RunpaneSemanticEvent { + this.counter += 1; + const cursor = this.currentCursor(); + const event: RunpaneSemanticEvent = { + id: cursor, + cursor, + type, + at: new Date().toISOString(), + paneId: options.paneId ?? panel.sessionId, + panelId: panel.id, + state: panelStateSummary( + panel, + terminalPanelManager.getTerminalSnapshot(panel.id), + undefined, + terminalPanelManager.getLastKnownBlocker(panel.id), + ), + }; + this.events.push(event); + if (this.events.length > this.capacity) this.events.splice(0, this.events.length - this.capacity); + getPaneDaemonEventSink().send('panel:semanticEvent', event); + return event; + } + + replaySince(cursor: string): RunpanePanelsEventsResponse { + const parsed = this.parseCursor(cursor); + const oldest = this.events.length > 0 ? this.cursorNumber(this.events[0].cursor) : 0; + if (!parsed || parsed.epoch !== this.epoch || parsed.n > this.counter || (this.events.length > 0 && parsed.n < oldest - 1)) { + return { ok: false, error: this.cursorExpired() }; + } + return { + ok: true, + events: this.events.filter((event) => this.cursorNumber(event.cursor) > parsed.n), + cursor: this.currentCursor(), + }; + } + + private parseCursor(cursor: string): { epoch: string; n: number } | null { + const separator = cursor.lastIndexOf(':'); + if (separator <= 0) return null; + const epoch = cursor.slice(0, separator); + const raw = cursor.slice(separator + 1); + if (!/^\d+$/.test(raw)) return null; + const n = Number(raw); + return Number.isSafeInteger(n) ? { epoch, n } : null; + } + + private cursorNumber(cursor: string): number { + return Number(cursor.slice(cursor.lastIndexOf(':') + 1)); + } + + private cursorExpired(): RunpaneCursorExpiredError { + return { + code: 'cursor_expired', + earliestCursor: this.earliestCursor(), + reconcileCommand: 'runpane panels screen --panel <panel-id> --json', + }; + } +} diff --git a/main/src/services/runpanePanelState.ts b/main/src/services/runpanePanelState.ts new file mode 100644 index 00000000..717c1434 --- /dev/null +++ b/main/src/services/runpanePanelState.ts @@ -0,0 +1,48 @@ +import type { TerminalPanelState, ToolPanel } from '../../../shared/types/panels'; +import type { + RunpaneAgentId, + RunpanePanelBlockedState, + RunpanePanelStateSummary, +} from '../../../shared/types/runpaneOrchestration'; +import { terminalPanelManager, type TerminalPanelSnapshot } from './terminalPanelManager'; + +const AGENT_IDS = new Set<string>(['claude', 'codex']); + +function toIsoString(value: string | number | Date | undefined): string | undefined { + if (value === undefined) return undefined; + const date = value instanceof Date ? value : new Date(value); + return Number.isNaN(date.getTime()) ? undefined : date.toISOString(); +} + +export function getTerminalCustomState(panel: ToolPanel): TerminalPanelState { + const value = panel.state.customState; + return (value && typeof value === 'object' && !Array.isArray(value) ? value : {}) as TerminalPanelState; +} + +export function panelStateSummary( + panel: ToolPanel, + snapshot: TerminalPanelSnapshot | null, + customState: TerminalPanelState = getTerminalCustomState(panel), + blocker?: RunpanePanelBlockedState, +): RunpanePanelStateSummary { + const customAgentType = typeof customState.agentType === 'string' && AGENT_IDS.has(customState.agentType) + ? customState.agentType as RunpaneAgentId + : undefined; + const hasLiveTerminal = Boolean(snapshot || terminalPanelManager.isTerminalInitialized(panel.id)); + return { + initialized: hasLiveTerminal, + isAlternateScreen: snapshot?.isAlternateScreen ?? customState.isAlternateScreen, + activityStatus: snapshot?.activityStatus, + isCliReady: snapshot?.isCliReady ?? (hasLiveTerminal ? customState.isCliReady : undefined), + isCliPanel: snapshot?.isCliPanel ?? customState.isCliPanel, + agentType: snapshot?.agentType ?? customAgentType, + lastActivity: snapshot?.lastActivityTime ?? customState.lastActivityTime ?? toIsoString(panel.metadata.lastActiveAt), + terminalReady: snapshot?.terminalReady, + agentActivity: snapshot?.agentActivity ?? (customState.exitedAt ? 'exited' : 'unknown'), + inputRequired: blocker?.kind === 'agent-prompt' || blocker?.kind === 'codex-update', + blocked: blocker !== undefined, + hasNewOutput: snapshot ? snapshot.outputGeneration > snapshot.outputGenerationAtQuiescence : false, + outputGeneration: snapshot?.outputGeneration, + lastMeaningfulEventAt: snapshot?.lastMeaningfulEventAt ?? customState.exitedAt, + }; +} diff --git a/main/src/services/terminalPanelManager.test.ts b/main/src/services/terminalPanelManager.test.ts index 54156c2f..9926d08a 100644 --- a/main/src/services/terminalPanelManager.test.ts +++ b/main/src/services/terminalPanelManager.test.ts @@ -6,6 +6,7 @@ import { TerminalStateEmulator } from './terminalStateEmulator'; import type { ToolPanel } from '../../../shared/types/panels'; const ptySpawn = vi.hoisted(() => vi.fn()); +const semanticAppend = vi.hoisted(() => vi.fn()); vi.mock('@lydell/node-pty', () => ({ spawn: ptySpawn })); vi.mock('./panelManager', () => ({ @@ -82,6 +83,9 @@ type TerminalUnderTest = { outputGenerationAtQuiescence: number; exitEventHandled: boolean; suppressSemanticExitPersistence: boolean; + lastKnownBlocker?: { kind: 'agent-prompt' | 'codex-update' | 'submission_unverified' | 'unknown'; message: string }; + blockerScanTimer: ReturnType<typeof setTimeout> | null; + lastBlockerScanAt: number; inSyncBlock: boolean; codexResumeOutputBuffer: string; codexAgentSessionId?: string; @@ -192,6 +196,8 @@ function createTerminal(overrides: Partial<TerminalUnderTest> = {}): TerminalUnd outputGenerationAtQuiescence: 0, exitEventHandled: false, suppressSemanticExitPersistence: false, + blockerScanTimer: null, + lastBlockerScanAt: 0, inSyncBlock: false, codexResumeOutputBuffer: '', ...overrides, @@ -253,6 +259,7 @@ function installRuntime(agentIdleDebounceMs?: unknown): { send: ReturnType<typeo setPaneRuntime({ eventSink, daemonEventSink: { send: vi.fn() }, + getRunpaneEventLog: () => ({ append: semanticAppend }) as never, getConfigManager: () => createConfigManagerStub(agentIdleDebounceMs), getPtyHostRuntime: () => null, getWebviewContextMap: () => new Map(), @@ -271,6 +278,7 @@ describe('TerminalPanelManager semantic agent state', () => { vi.mocked(panelManager.getPanel).mockReset(); vi.mocked(panelManager.updatePanel).mockReset(); ptySpawn.mockReset(); + semanticAppend.mockReset(); vi.clearAllTimers(); vi.useRealTimers(); }); @@ -378,6 +386,28 @@ describe('TerminalPanelManager semantic agent state', () => { expect(eventSink.send).toHaveBeenCalledWith('panel:activityStatus', expect.objectContaining({ status: 'idle', })); + expect(semanticAppend).toHaveBeenCalledTimes(1); + expect(semanticAppend).toHaveBeenCalledWith('panel_exited', panel, { paneId: 'session-1' }); + terminal.pty.emitExit({ exitCode: 7, signal: 15 }); + expect(semanticAppend).toHaveBeenCalledTimes(1); + disposeFlowControlRecord(terminal.flowControl); + }); + + it('AC2/AC3 detects a hidden-panel blocker on the trailing scan and emits edge-only events', async () => { + vi.useFakeTimers(); installRuntime(); + const panel = createPanel({ agentType: 'codex' }); + vi.mocked(panelManager.getPanel).mockReturnValue(panel); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const terminal = createTerminal({ outputBuffer: '', isVisible: false, screenEmulator: new TerminalStateEmulator(80, 24) }); + manager.terminals.set(terminal.panelId, terminal); manager.setupTerminalHandlers(terminal); + terminal.pty.emitData('Press Enter to continue'); + await vi.advanceTimersByTimeAsync(250); + expect(semanticAppend.mock.calls.map(call => call[0])).toContain('blocked'); + expect(semanticAppend.mock.calls.map(call => call[0])).toContain('input_required'); + const count = semanticAppend.mock.calls.filter(call => call[0] === 'blocked' || call[0] === 'input_required').length; + terminal.pty.emitData('\rPress Enter to continue'); + await vi.advanceTimersByTimeAsync(500); + expect(semanticAppend.mock.calls.filter(call => call[0] === 'blocked' || call[0] === 'input_required')).toHaveLength(count); disposeFlowControlRecord(terminal.flowControl); }); diff --git a/main/src/services/terminalPanelManager.ts b/main/src/services/terminalPanelManager.ts index ce0e4d30..77d2fe0a 100644 --- a/main/src/services/terminalPanelManager.ts +++ b/main/src/services/terminalPanelManager.ts @@ -1,6 +1,6 @@ import * as pty from '@lydell/node-pty'; import { ToolPanel, TerminalPanelState, PanelEventType } from '../../../shared/types/panels'; -import { getPaneDaemonEventSink, getPaneEventSink, getPtyHostRuntime, getRuntimeConfigManager, type PtyHandleLike, type PtyHostRuntime } from '../core/runtime'; +import { getPaneDaemonEventSink, getPaneEventSink, getPtyHostRuntime, getRuntimeConfigManager, getRuntimeRunpaneEventLog, type PtyHandleLike, type PtyHostRuntime } from '../core/runtime'; import { panelManager } from './panelManager'; import * as os from 'os'; import * as path from 'path'; @@ -18,7 +18,8 @@ import { onPtyBytes as flowControlOnPtyBytes, } from '../ptyHost/flowControl'; import { TerminalStateEmulator } from './terminalStateEmulator'; -import type { RunpaneAgentActivity } from '../../../shared/types/runpaneOrchestration'; +import type { RunpaneAgentActivity, RunpanePanelBlockedState, RunpaneSemanticEventType } from '../../../shared/types/runpaneOrchestration'; +import { boundSanitizedLines, detectPanelBlocker } from './runpaneBlockerDetection'; const OUTPUT_BATCH_INTERVAL = 32; // ms (~30fps) — wider window reduces TUI flicker const OUTPUT_BATCH_INTERVAL_HIDDEN = 250; // ms — background / hidden cadence to cut IPC wake-up cost @@ -27,6 +28,8 @@ const OUTPUT_BATCH_SIZE_HIDDEN = 80_000; // 80KB — cap hidden flush size to av const MAX_CONCURRENT_SPAWNS = 3; const IDLE_THRESHOLD_MS = 30_000; // 30s — mark panel idle after no PTY output const DEFAULT_AGENT_IDLE_DEBOUNCE_MS = 60_000; +const BLOCKER_SCAN_INTERVAL_MS = 500; +const BLOCKER_SCAN_LINE_LIMIT = 80; const MAX_SCROLLBACK_BUFFER_SIZE = 500_000; // 500KB of normal shell history const MAX_ALTERNATE_SCREEN_BUFFER_SIZE = 100_000; // 100KB of recent TUI redraw state const MIN_PTY_COLS = 20; @@ -214,6 +217,9 @@ interface TerminalProcess { outputGenerationAtQuiescence: number; exitEventHandled: boolean; suppressSemanticExitPersistence: boolean; + lastKnownBlocker?: RunpanePanelBlockedState; + blockerScanTimer: ReturnType<typeof setTimeout> | null; + lastBlockerScanAt: number; // DEC Mode 2026 synchronized-output block tracking — persists across chunks inSyncBlock: boolean; codexAgentSessionId?: string; @@ -227,6 +233,15 @@ export class TerminalPanelManager { private readonly MAX_SCROLLBACK_LINES = 10000; private analyticsManager: AnalyticsManager | null = null; + getLastKnownBlocker(panelId: string): RunpanePanelBlockedState | undefined { + return this.terminals.get(panelId)?.lastKnownBlocker; + } + + private appendSemanticEvent(terminal: TerminalProcess, type: RunpaneSemanticEventType): void { + const panel = panelManager.getPanel(terminal.panelId); + if (panel) getRuntimeRunpaneEventLog().append(type, panel, { paneId: terminal.sessionId }); + } + // Spawn concurrency limiter — prevents CPU spikes when many terminals init at once private activeSpawns = 0; private spawnQueue: Array<{ resolve: () => void; priority: number }> = []; @@ -554,6 +569,7 @@ export class TerminalPanelManager { const data = terminal.outputBuffer; terminal.outputBuffer = ''; + this.scheduleBlockerScan(terminal); if (!terminal.isVisible) { // Hidden terminals run headless: keep PTY output in main scrollback, but @@ -972,6 +988,8 @@ export class TerminalPanelManager { outputGenerationAtQuiescence: 0, exitEventHandled: false, suppressSemanticExitPersistence: false, + blockerScanTimer: null, + lastBlockerScanAt: 0, inSyncBlock: false, codexResumeOutputBuffer: '' }; @@ -1056,6 +1074,7 @@ export class TerminalPanelManager { // Emit to renderer this.sendRendererEvent('terminal:cliReady', { panelId }); + if (currentTerminal === terminalProcess) this.appendSemanticEvent(currentTerminal, 'terminal_ready'); this.sendInitialInputOnce(panelId); }; @@ -1170,6 +1189,7 @@ export class TerminalPanelManager { if (terminal.agentActivity !== 'active') { terminal.agentActivity = 'active'; this.stampMeaningfulEvent(terminal, outputAt); + this.appendSemanticEvent(terminal, 'agent_active'); } this.clearAgentIdleTimer(terminal); const agentIdleDebounceMs = normalizeAgentIdleDebounceMs( @@ -1181,6 +1201,8 @@ export class TerminalPanelManager { terminal.outputGenerationAtQuiescence = terminal.outputGeneration; terminal.agentIdleTimer = null; this.stampMeaningfulEvent(terminal); + this.scanBlockerNow(terminal); + this.appendSemanticEvent(terminal, 'agent_idle'); }, agentIdleDebounceMs); } @@ -1284,13 +1306,13 @@ export class TerminalPanelManager { terminal.pty.onExit((exitCode: { exitCode: number; signal?: number }) => { const currentTerminal = this.terminals.get(terminal.panelId); if (currentTerminal && currentTerminal !== terminal) return; - if (terminal.exitEventHandled) return; - terminal.exitEventHandled = true; if (!terminal.suppressSemanticExitPersistence) { this.markTerminalExited(terminal, { exitCode: exitCode.exitCode, exitSignal: exitCode.signal, }); + } else { + this.clearBlockerScanTimer(terminal); } // Clear idle timer and mark as idle on exit @@ -1668,6 +1690,56 @@ export class TerminalPanelManager { } } + private clearBlockerScanTimer(terminal: TerminalProcess): void { + if (terminal.blockerScanTimer) { + clearTimeout(terminal.blockerScanTimer); + terminal.blockerScanTimer = null; + } + } + + private scheduleBlockerScan(terminal: TerminalProcess): void { + const elapsed = Date.now() - terminal.lastBlockerScanAt; + if (elapsed >= BLOCKER_SCAN_INTERVAL_MS) { + this.scanBlockerNow(terminal); + return; + } + if (!terminal.blockerScanTimer) { + terminal.blockerScanTimer = setTimeout(() => { + terminal.blockerScanTimer = null; + if (this.terminals.get(terminal.panelId) === terminal) this.scanBlockerNow(terminal); + }, BLOCKER_SCAN_INTERVAL_MS - elapsed); + } + } + + private scanBlockerNow(terminal: TerminalProcess): void { + terminal.lastBlockerScanAt = Date.now(); + const snapshot = this.getTerminalSnapshot(terminal.panelId); + const rawText = snapshot?.screenText + ?? (snapshot?.isAlternateScreen ? snapshot.alternateScreenBuffer : snapshot?.scrollbackBuffer) + ?? ''; + const panel = panelManager.getPanel(terminal.panelId); + const agentType = snapshot?.agentType + ?? ((panel?.state.customState as TerminalPanelState | undefined)?.agentType); + const next = detectPanelBlocker( + boundSanitizedLines(rawText, BLOCKER_SCAN_LINE_LIMIT).text, + agentType, + terminal.panelId, + ); + const previous = terminal.lastKnownBlocker; + const previousInteractive = previous?.kind === 'agent-prompt' || previous?.kind === 'codex-update'; + const nextInteractive = next?.kind === 'agent-prompt' || next?.kind === 'codex-update'; + terminal.lastKnownBlocker = next; + + if (!previous && next) { + this.appendSemanticEvent(terminal, 'blocked'); + if (nextInteractive) this.appendSemanticEvent(terminal, 'input_required'); + } else if (previous && !next) { + this.appendSemanticEvent(terminal, 'unblocked'); + } else if (previous && next && !previousInteractive && nextInteractive) { + this.appendSemanticEvent(terminal, 'input_required'); + } + } + private persistExitFacts( panelId: string, facts: { exitedAt: string; exitCode?: number; exitSignal?: number }, @@ -1694,12 +1766,21 @@ export class TerminalPanelManager { terminal: TerminalProcess, facts: { exitCode?: number; exitSignal?: number } = {}, ): void { + if (terminal.exitEventHandled) { + if (!terminal.suppressSemanticExitPersistence && (facts.exitCode !== undefined || facts.exitSignal !== undefined)) { + this.persistExitFacts(terminal.panelId, { exitedAt: new Date().toISOString(), ...facts }); + } + return; + } + terminal.exitEventHandled = true; this.clearAgentIdleTimer(terminal); + this.clearBlockerScanTimer(terminal); terminal.agentActivity = 'exited'; terminal.outputGenerationAtQuiescence = terminal.outputGeneration; const exitedAt = new Date(); this.stampMeaningfulEvent(terminal, exitedAt); this.persistExitFacts(terminal.panelId, { exitedAt: exitedAt.toISOString(), ...facts }); + if (!terminal.suppressSemanticExitPersistence) this.appendSemanticEvent(terminal, 'panel_exited'); } destroyTerminal(panelId: string): void { @@ -1825,6 +1906,7 @@ export class TerminalPanelManager { terminal.suppressSemanticExitPersistence = true; terminal.screenEmulator?.dispose(); this.clearAgentIdleTimer(terminal); + this.clearBlockerScanTimer(terminal); this.terminals.delete(panelId); this.visibleViewersByPanel.delete(panelId); continue; @@ -1862,6 +1944,7 @@ export class TerminalPanelManager { terminal.idleTimer = null; } this.clearAgentIdleTimer(terminal); + this.clearBlockerScanTimer(terminal); terminal.suppressSemanticExitPersistence = true; terminal.screenEmulator?.dispose(); this.terminals.delete(panelId); diff --git a/packages/runpane-py/src/runpane/cli.py b/packages/runpane-py/src/runpane/cli.py index 845f3048..487a8b61 100644 --- a/packages/runpane-py/src/runpane/cli.py +++ b/packages/runpane-py/src/runpane/cli.py @@ -27,6 +27,9 @@ run_panels_submit, run_panels_submit_composer, run_panels_wait, + run_panels_events, + run_panels_watch, + run_panels_await, run_panes_archive, run_panes_create, run_panes_list, @@ -118,6 +121,10 @@ class ParsedArgs: pinned: bool = False composer_strategy: Optional[str] = None force: bool = False + event_selector: Optional[str] = None + since: Optional[str] = None + jsonl: bool = False + heartbeat_ms: Optional[float] = None help_topic: Optional[str] = None remote_setup_args: List[str] = field(default_factory=list) @@ -195,6 +202,12 @@ def dispatch_parsed_command(parsed: ParsedArgs, telemetry_context: WrapperTeleme return run_panels_submit_composer(parsed) if parsed.command == "panels wait": return run_panels_wait(parsed) + if parsed.command == "panels events": + return run_panels_events(parsed) + if parsed.command == "panels watch": + return run_panels_watch(parsed) + if parsed.command == "panels await": + return run_panels_await(parsed) if parsed.command == "agents doctor": return run_agents_doctor(parsed) if parsed.command in {"install", "update"}: @@ -382,6 +395,10 @@ def parse_args(argv: List[str]) -> ParsedArgs: parsed.target = "client" parse_flags(args, parsed) + if parsed.jsonl and parsed.command != "panels watch": + raise ValueError("--jsonl is only valid with panels watch.") + if parsed.command == "panels await" and (not parsed.panel_id or not parsed.event_selector): + raise ValueError("panels await requires --panel and --event.") return parsed @@ -477,6 +494,9 @@ def parse_local_boolean_flag(parsed: ParsedArgs, flag: str) -> None: if flag == "--force": parsed.force = True return + if flag == "--jsonl": + parsed.jsonl = True + return raise ValueError(f"Unknown option for {parsed.command}: {flag}") @@ -594,6 +614,24 @@ def parse_local_value_flag(parsed: ParsedArgs, flag: str, value: str) -> None: raise ValueError("--strategy must be one of: auto, codex-ctrl-enter, enter.") parsed.composer_strategy = value return + if flag == "--event": + selectors = set(RUNPANE_CONTRACT["enums"]["eventSelectors"]) + if value not in selectors: + raise ValueError(f"Invalid --event {value}. Expected one of: {', '.join(sorted(selectors))}") + parsed.event_selector = value + return + if flag == "--since": + parsed.since = value + return + if flag == "--heartbeat-ms": + try: + heartbeat_ms = float(value) + except ValueError as error: + raise ValueError("--heartbeat-ms must be a positive number.") from error + if heartbeat_ms <= 0: + raise ValueError("--heartbeat-ms must be a positive number.") + parsed.heartbeat_ms = heartbeat_ms + return raise ValueError(f"Unknown option for {parsed.command}: {flag}") @@ -615,6 +653,9 @@ def is_runpane_local_command(command: str) -> bool: "panels submit", "panels submit-composer", "panels wait", + "panels events", + "panels watch", + "panels await", "agents doctor", } diff --git a/packages/runpane-py/src/runpane/daemon_client.py b/packages/runpane-py/src/runpane/daemon_client.py index eb642fda..54026d3a 100644 --- a/packages/runpane-py/src/runpane/daemon_client.py +++ b/packages/runpane-py/src/runpane/daemon_client.py @@ -7,7 +7,9 @@ import posixpath import socket import sys -from typing import Any, Dict, List, Optional +import queue +import threading +from typing import Any, BinaryIO, Dict, List, Optional FRAME_DELIMITER = b"\n" UNIX_SOCKET_BASE_DIRECTORY = "/tmp" @@ -21,6 +23,80 @@ def __init__(self, message: str, code: Optional[str] = None) -> None: self.code = code +class RetainedDaemonConnection: + def __init__(self, pane_dir: Optional[str] = None) -> None: + endpoint = get_pane_daemon_endpoint(resolve_pane_directory(pane_dir)) + self._endpoint = endpoint + self._socket: Optional[socket.socket] = None + self._pipe: Optional[BinaryIO] = None + self._decoder = PaneDaemonFrameDecoder() + self._pending: Dict[int, queue.Queue[Any]] = {} + self.events: queue.Queue[Any] = queue.Queue() + self.errors: queue.Queue[Exception] = queue.Queue() + self._next_id = 1 + self._closed = False + if endpoint["transport"] == "pipe": + self._pipe = open(endpoint["path"], "r+b", buffering=0) + else: + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(endpoint["path"]) + self._thread = threading.Thread(target=self._read_loop, daemon=True) + self._thread.start() + + def request(self, channel: str, args: Optional[List[Any]] = None, timeout_ms: float = DEFAULT_TIMEOUT_MS) -> Any: + request_id = self._next_id + self._next_id += 1 + response_queue: queue.Queue[Any] = queue.Queue(maxsize=1) + self._pending[request_id] = response_queue + self._write(encode_frame({"type": "request", "id": request_id, "channel": channel, "args": args or []})) + try: + response = response_queue.get(timeout=timeout_ms / 1000) + except queue.Empty as error: + self._pending.pop(request_id, None) + raise PaneDaemonClientError("Timed out waiting for Pane daemon response", "ERR_RUNPANE_DAEMON_TIMEOUT") from error + if isinstance(response, Exception): + raise response + return response + + def close(self) -> None: + self._closed = True + if self._socket: + self._socket.close() + if self._pipe: + self._pipe.close() + + def _write(self, data: bytes) -> None: + if self._socket: + self._socket.sendall(data) + elif self._pipe: + self._pipe.write(data) + + def _read_loop(self) -> None: + try: + while not self._closed: + chunk = self._socket.recv(65536) if self._socket else self._pipe.read(65536) if self._pipe else b"" + if not chunk: + raise PaneDaemonClientError("Pane daemon closed the retained connection", "ERR_RUNPANE_DAEMON_CLOSED") + for frame in self._decoder.push(chunk): + if frame.get("type") == "event" and frame.get("channel") == "panel:semanticEvent": + args = frame.get("args") + self.events.put(args[0] if isinstance(args, list) and args else None) + elif frame.get("type") == "response": + target = self._pending.pop(frame.get("id"), None) + if target: + if frame.get("ok") is True: + target.put(frame.get("result")) + else: + payload = frame.get("error") or {} + target.put(PaneDaemonClientError(payload.get("message", "Pane daemon request failed"), payload.get("code"))) + except Exception as error: + if not self._closed: + self.errors.put(error) + for target in self._pending.values(): + target.put(error) + self._pending.clear() + + def resolve_pane_directory(pane_dir: Optional[str] = None) -> str: return pane_dir or os.environ.get("PANE_DIR") or os.environ.get("FOOZOL_DIR") or os.path.join(os.path.expanduser("~"), ".pane") diff --git a/packages/runpane-py/src/runpane/generated_contract.py b/packages/runpane-py/src/runpane/generated_contract.py index bdf57806..d527a665 100644 --- a/packages/runpane-py/src/runpane/generated_contract.py +++ b/packages/runpane-py/src/runpane/generated_contract.py @@ -1,4 +1,4 @@ # Generated by scripts/generate-runpane-contract.js. Do not edit by hand. import json -RUNPANE_CONTRACT = json.loads("{\n \"$schema\": \"./schema.json\",\n \"schemaVersion\": 1,\n \"name\": \"runpane\",\n \"description\": \"Thin installer and local control CLI for Pane\",\n \"packageInstallPolicy\": [\n \"The packages must not download, install, or configure Pane during package installation.\",\n \"Work starts only when a user runs `runpane ...`.\"\n ],\n \"compatibility\": {\n \"node\": \">=18.17.0\",\n \"python\": \">=3.8\"\n },\n \"terminology\": {\n \"repo\": \"Saved Pane repository/project record\",\n \"pane\": \"User-visible Pane session\",\n \"tool\": \"Terminal-backed tab\",\n \"agent\": \"Built-in agent command template\"\n },\n \"defaults\": {\n \"target\": \"client\",\n \"paneVersion\": \"latest\",\n \"channel\": \"stable\",\n \"format\": \"auto\",\n \"dryRun\": false,\n \"yes\": false,\n \"verbose\": false\n },\n \"enums\": {\n \"installTargets\": [\n \"client\",\n \"daemon\"\n ],\n \"artifactFormats\": [\n \"auto\",\n \"appimage\",\n \"deb\",\n \"dmg\",\n \"zip\",\n \"exe\"\n ],\n \"channels\": [\n \"stable\",\n \"nightly\"\n ],\n \"agents\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"agentTemplates\": {\n \"codex\": {\n \"title\": \"Codex\",\n \"command\": \"codex --yolo\",\n \"description\": \"Open a Codex terminal tab and allow the initial input to drive the agent.\"\n },\n \"claude\": {\n \"title\": \"Claude Code\",\n \"command\": \"claude --dangerously-skip-permissions\",\n \"description\": \"Open a Claude Code terminal tab and allow the initial input to drive the agent.\"\n }\n },\n \"commands\": [\n {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"usage\": [\n \"runpane help [command]\"\n ]\n },\n {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"usage\": [\n \"runpane setup\"\n ],\n \"interactiveEntrypoint\": true\n },\n {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"usage\": [\n \"runpane install [client|daemon] [options]\"\n ],\n \"defaultTarget\": \"client\",\n \"targets\": [\n \"client\",\n \"daemon\"\n ],\n \"unknownDaemonFlagsForwarded\": true\n },\n {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"usage\": [\n \"runpane update [options]\"\n ],\n \"target\": \"client\"\n },\n {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"usage\": [\n \"runpane version\",\n \"runpane --version\"\n ]\n },\n {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"usage\": [\n \"runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"usage\": [\n \"runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"usage\": [\n \"runpane agent-context [--json]\",\n \"runpane agent-context --command <command> [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"usage\": [\n \"runpane repos list [--json] [--pane-dir <path>]\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"usage\": [\n \"runpane repos add --path <path> [--name <name>] [--json] [--yes]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions in a saved repository.\",\n \"usage\": [\n \"runpane panes list [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"usage\": [\n \"runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [options]\",\n \"runpane panes create --from-json <path|-> [--yes] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"usage\": [\n \"runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"usage\": [\n \"runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready] --yes [--json]\",\n \"runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--focus|--no-focus] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"usage\": [\n \"runpane panels list --pane <pane-id> [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"usage\": [\n \"runpane panels output --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"usage\": [\n \"runpane panels screen --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"usage\": [\n \"runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"usage\": [\n \"runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"usage\": [\n \"runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"usage\": [\n \"runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ]\n }\n ],\n \"flags\": {\n \"wrapper\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"description\": \"Pane release to install or inspect.\"\n },\n {\n \"name\": \"--download-dir\",\n \"value\": \"<path>\",\n \"description\": \"Directory for downloaded Pane release artifacts.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"description\": \"Use or inspect an existing Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"description\": \"Pane release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"description\": \"Print the plan without downloading or installing.\"\n },\n {\n \"name\": \"--yes\",\n \"aliases\": [\n \"-y\"\n ],\n \"description\": \"Skip interactive prompts where possible.\"\n },\n {\n \"name\": \"--verbose\",\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"remoteValue\": [\n {\n \"name\": \"--label\",\n \"value\": \"<name>\"\n },\n {\n \"name\": \"--prefer-tunnel\",\n \"value\": \"<tailscale|ssh|manual|auto>\"\n },\n {\n \"name\": \"--channel\",\n \"value\": \"<stable|nightly>\"\n },\n {\n \"name\": \"--base-url\",\n \"value\": \"<url>\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\"\n },\n {\n \"name\": \"--listen-port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--repo-ref\",\n \"value\": \"<ref>\"\n }\n ],\n \"remoteBoolean\": [\n {\n \"name\": \"--auto-listen-port\"\n },\n {\n \"name\": \"--interactive-tailscale-setup\"\n },\n {\n \"name\": \"--no-install-service\"\n },\n {\n \"name\": \"--no-tailscale-serve\"\n },\n {\n \"name\": \"--print-only\"\n }\n ],\n \"localValue\": [\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"description\": \"Connect to a Pane daemon using this Pane data directory.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"description\": \"Pane/session id to inspect.\"\n },\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"description\": \"Tool panel id to inspect or control.\"\n },\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"description\": \"Name for the registered repository or created pane/session.\"\n },\n {\n \"name\": \"--worktree-name\",\n \"value\": \"<name>\",\n \"description\": \"Worktree name to request. Defaults to --name.\"\n },\n {\n \"name\": \"--base-branch\",\n \"value\": \"<branch>\",\n \"description\": \"Base branch for the created worktree.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"description\": \"Terminal tab title. Defaults to the selected agent title or Terminal.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"aliases\": [\n \"--prompt\"\n ],\n \"description\": \"Text to send to the terminal after the command is ready. --prompt is an alias.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"description\": \"Read a full panes.create request JSON payload from a file or stdin.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Maximum time to wait for each pane creation job.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Readiness wait timeout for panes create --wait-ready.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"description\": \"Accepted for forward compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"description\": \"Maximum recent output lines or records to read.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"description\": \"Panel wait condition.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"description\": \"Text to wait for with panels wait --for text.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Polling interval for panels wait.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"description\": \"Text bytes to send to a terminal panel.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read panel input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"description\": \"Identify whether a local-control mutation is user-initiated or agent/orchestrator-initiated.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"description\": \"Composer submit key sequence strategy for panels submit-composer.\"\n }\n ],\n \"localBoolean\": [\n {\n \"name\": \"--json\",\n \"description\": \"Print machine-readable JSON output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"description\": \"Wait for created terminal panels to be ready before returning.\"\n },\n {\n \"name\": \"--no-focus\",\n \"description\": \"Create the pane or panel in the background without changing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"description\": \"Explicitly focus the created pane or panel.\"\n },\n {\n \"name\": \"--pinned\",\n \"description\": \"Create the pane already pinned (the UI's favorite/pin star).\"\n },\n {\n \"name\": \"--force\",\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n }\n ]\n },\n \"help\": {\n \"npm\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z> Pane release to install\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path> Use an existing Pane executable\",\n \" --dry-run Print the plan without downloading\",\n \" --yes Skip interactive prompts where possible\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [options]\",\n \"\",\n \"Updates Pane using the same artifact selection as \\\"runpane install client\\\".\",\n \"\",\n \"Options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json Print a machine-readable environment report\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --pane-path <path> Inspect a specific Pane executable\",\n \" --format <format> Release artifact format to inspect\",\n \" --verbose Print extra diagnostics\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json Print machine-readable output\",\n \" --pane-dir <path> Connect to a specific Pane data directory\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path> Existing git repository path\",\n \" --name <name> Saved repository name; defaults to the directory name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without adding the repo\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to archive\",\n \" --source <user|agent> Mark mutation source\",\n \" --force Archive even if the pane has uncommitted, untracked, or unpushed changes\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to pin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without pinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to unpin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without unpinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads bounded recent terminal output from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Tool panel id\",\n \" --limit <count> Maximum recent output lines or records to read\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text bytes to send\",\n \" --input-file <path|-> Read input from a file or stdin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command> Print the full definition for one runpane command\",\n \" --json Print machine-readable output\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n },\n \"pip\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" pipx run runpane install client\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \" uvx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [--version <latest|vX.Y.Z>] [--dry-run] [--yes]\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\",\n \" --pane-path <path>\",\n \" --format <format>\",\n \" --verbose\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path>\",\n \" --name <name>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --source <user|agent>\",\n \" --force\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads recent terminal output records from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --limit <count>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --text <text>\",\n \" --input-file <path|->\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command>\",\n \" --json\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" python -m runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n }\n },\n \"docs\": {\n \"maintainerRules\": [\n \"Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\",\n \"Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\",\n \"The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\",\n \"Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\",\n \"Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\",\n \"`pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\",\n \"Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\"\n ],\n \"recommendedQuickStarts\": [\n \"npx --yes runpane@latest\",\n \"npm i -g runpane && runpane setup\",\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"npmCommands\": [\n \"npx --yes runpane@latest\",\n \"npx --yes runpane@latest setup\",\n \"npx --yes runpane@latest install client\",\n \"npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \"pnpm dlx runpane@latest\",\n \"pnpm dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"npm i -g runpane && runpane\",\n \"npm i -g runpane && runpane setup\",\n \"npm i -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"pnpm add -g runpane && runpane\",\n \"pnpm add -g runpane && runpane setup\",\n \"pnpm add -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"yarn dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"bunx runpane@latest install daemon --label \\\"My Server\\\"\"\n ],\n \"pythonCommands\": [\n \"pipx run runpane\",\n \"pipx run runpane setup\",\n \"python -m pip install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"python -m runpane setup\",\n \"runpane install daemon --label \\\"My Server\\\"\",\n \"pipx install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"uvx runpane@latest\",\n \"uvx runpane@latest setup\",\n \"uvx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"python -m runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"packageManagerNotes\": [\n \"Use `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\",\n \"Do not document `pnpm install runpane` as the public CLI install path.\"\n ],\n \"commandUsages\": [\n \"runpane\",\n \"runpane setup\",\n \"runpane install\",\n \"runpane install client\",\n \"runpane install daemon\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane doctor --json\",\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\",\n \"runpane repos list --json\",\n \"runpane repos add --path /path/to/repo --name Pane --yes --json\",\n \"runpane panes list --repo active --json\",\n \"runpane panes create --repo active --name issue-252 --agent <agent> --prompt \\\"Kick off the discussion skill for issue 252\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panels list --pane <pane-id> --json\",\n \"runpane panels output --panel <panel-id> --limit 200 --json\",\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes --json\",\n \"runpane help\",\n \"runpane <command> --help\"\n ],\n \"commandDescriptions\": [\n \"`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. In non-interactive shells or CI, both forms must print help, common commands, and agent discovery hints, then exit successfully instead of waiting for input.\",\n \"`runpane install` is an alias for `runpane install client`.\",\n \"`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\",\n \"`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\",\n \"The wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\",\n \"`runpane update` uses the same release resolution and installer path as `install client`.\",\n \"`runpane version` prints only wrapper package metadata and does not contact, launch, or focus the Pane app or daemon.\",\n \"`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, daemon reachability, and remote-daemon hints. Add `--json` for a machine-readable report that agents should run before mutating Pane state. Installed app version detection must be best-effort and must not launch, focus, or configure Pane; macOS wrappers read app bundle metadata instead of executing `Pane --version`.\",\n \"`runpane agent-context` prints a brief, token-efficient command schema for coding agents without connecting to the Pane daemon.\",\n \"`runpane agent-context --command \\\"panes create\\\"` prints the detailed definition for one command. Add `--json` for machine-readable output.\",\n \"`runpane repos list` connects to the running local Pane daemon and prints saved repository records.\",\n \"`runpane repos add` registers an existing git repository with the running local Pane daemon. It does not create directories or initialize git repositories by default.\",\n \"`runpane panes list` lists Pane sessions, optionally scoped to one saved repository.\",\n \"`runpane panes create` connects to the running local Pane daemon, resolves the requested saved base repository, creates user-visible Pane sessions backed by Pane-managed worktrees/branches, opens terminal-backed tool tabs, and optionally sends initial input to the started tool. Built-in agent panes and `--source agent` default to background/no-focus unless `--focus` is passed.\",\n \"For `panes create --wait-ready`, `initialInput.verifiedSubmitted: true` is reported only after argument attachment or composer-clear plus activity evidence. Routing input does not by itself verify submission.\",\n \"`runpane panes archive` archives a Pane exactly like the UI Archive action, including removal of its Pane-managed git worktree, and refuses (unless `--force`) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes. It waits for worktree removal to finish before returning and reports the outcome in `worktreeCleanup`.\",\n \"`runpane panels list` lists tool panels inside one Pane session.\",\n \"`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.\",\n \"`runpane panels input` sends exact input bytes to one terminal panel. Prefer `--input-file` for newlines, Ctrl-C, quotes, or shell-sensitive text.\",\n \"`runpane panes create --prompt` is an alias for `--initial-input`; request JSON and daemon payloads should use the canonical `initialInput` field.\",\n \"If composer submission cannot be verified without risking a duplicate, the create item is unsuccessful with `initialInput.staged`, `initialInput.attempts`, `initialInput.blocked.kind: submission_unverified`, and an actionable `nextCommand`. The CLI-facing `--prompt` alias maps to this canonical `initialInput` result.\",\n \"When running from WSL while Pane is installed on Windows, the Linux wrapper may look for a missing `/tmp/pane-daemon.../daemon.sock` or resolve to a Windows shim such as Volta. In that case invoke the Windows wrapper through PowerShell from a Windows cwd, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\"\n ],\n \"wrapperFlagNote\": \"The top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\",\n \"localControlFlagNote\": \"`runpane doctor --json`, `runpane repos list`, `runpane panes list`, `runpane panes create`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues.\",\n \"daemonFlagNote\": \"Unknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\",\n \"downloadAttribution\": [\n \"The npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\",\n \"The PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\",\n \"Wrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\",\n \"If the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\",\n \"Wrappers also emit best-effort lifecycle telemetry to `https://runpane.com/api/runpane/telemetry` for command start/success/failure, download request/success/failure, and GitHub fallback usage.\",\n \"Wrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so distinct wrapper users can be counted with `count(DISTINCT properties.install_id)`.\",\n \"Wrapper telemetry must be disabled in CI and when `RUNPANE_TELEMETRY_DISABLED` is set, and must not include raw paths, labels, prompts, raw error text, or environment values.\"\n ],\n \"publishingCredentials\": [\n \"Local implementation, build, and dry-run validation do not need npm or PyPI API tokens.\",\n \"Release publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\",\n \"Fallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\"\n ]\n },\n \"testFixtures\": {\n \"parserSamples\": [\n [\n \"setup\"\n ],\n [\n \"install\"\n ],\n [\n \"install\",\n \"client\",\n \"--version\",\n \"v2.2.8\",\n \"--format\",\n \"dmg\",\n \"--download-dir\",\n \"/tmp/pane-downloads\",\n \"--dry-run\",\n \"--yes\"\n ],\n [\n \"install\",\n \"daemon\",\n \"--label\",\n \"VM\",\n \"--prefer-tunnel\",\n \"ssh\",\n \"--channel\",\n \"nightly\",\n \"--base-url\",\n \"https://example.test\",\n \"--pane-dir\",\n \"/tmp/pane\",\n \"--listen-port\",\n \"4555\",\n \"--auto-listen-port\",\n \"--print-only\",\n \"--repo-ref\",\n \"main\",\n \"--unknown-future-flag\",\n \"future-value\",\n \"--dry-run\",\n \"--verbose\"\n ],\n [\n \"update\",\n \"--version\",\n \"latest\",\n \"--format\",\n \"appimage\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--dry-run\"\n ],\n [\n \"doctor\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--format\",\n \"zip\",\n \"--verbose\"\n ],\n [\n \"doctor\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"agent-context\"\n ],\n [\n \"agent-context\",\n \"--command\",\n \"panes create\",\n \"--json\"\n ],\n [\n \"repos\",\n \"list\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"repos\",\n \"add\",\n \"--path\",\n \"/tmp/repo\",\n \"--name\",\n \"Repo\",\n \"--dry-run\",\n \"--yes\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"panes\",\n \"list\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--repo\",\n \"active\",\n \"--name\",\n \"issue-252\",\n \"--agent\",\n \"codex\",\n \"--prompt\",\n \"Kick off discussion\",\n \"--source\",\n \"agent\",\n \"--pinned\",\n \"--dry-run\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"archive\",\n \"--pane\",\n \"session-1\",\n \"--force\",\n \"--source\",\n \"agent\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"pin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"unpin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"list\",\n \"--pane\",\n \"session-1\",\n \"--json\"\n ],\n [\n \"panels\",\n \"output\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"200\",\n \"--json\"\n ],\n [\n \"panels\",\n \"input\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"Continue\\\\n\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"--version\"\n ],\n [\n \"agents\",\n \"doctor\",\n \"--agent\",\n \"codex\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--source\",\n \"agent\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"45000\",\n \"--concurrency\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"screen\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"80\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"wait\",\n \"--panel\",\n \"panel-1\",\n \"--for\",\n \"text\",\n \"--contains\",\n \"ready\",\n \"--timeout-ms\",\n \"30000\",\n \"--interval-ms\",\n \"500\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--source\",\n \"agent\",\n \"--no-focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"15000\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--focus\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit-composer\",\n \"--panel\",\n \"panel-1\",\n \"--strategy\",\n \"auto\",\n \"--yes\",\n \"--json\"\n ]\n ],\n \"topLevelHelpIncludes\": [\n \"runpane setup\",\n \"runpane install\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane agent-context\",\n \"runpane repos list\",\n \"runpane repos add\",\n \"runpane panes list\",\n \"runpane panes create\",\n \"runpane panes archive\",\n \"runpane panels create\",\n \"runpane panels list\",\n \"runpane panels output\",\n \"runpane panels input\",\n \"runpane agents doctor\",\n \"runpane panels screen\",\n \"runpane panels submit\",\n \"runpane panels submit-composer\",\n \"runpane panels wait\",\n \"runpane doctor --json\",\n \"runpane agent-context --json\",\n \"Agent discovery:\",\n \"Common commands:\"\n ],\n \"npmHelpIncludes\": [\n \"pnpm dlx runpane@latest\",\n \"npx --yes runpane@latest\"\n ],\n \"pipHelpIncludes\": [\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"installHelpIncludes\": [\n \"--version <latest|vX.Y.Z>\",\n \"--format <auto|appimage|deb|dmg|zip|exe>\",\n \"--download-dir <path>\",\n \"--pane-path <path>\",\n \"--label <name>\",\n \"--prefer-tunnel <tailscale|ssh|manual|auto>\",\n \"--repo-ref <ref>\"\n ]\n },\n \"jsonSchemas\": {\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"doctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"source\",\n \"wrapper\",\n \"release\",\n \"installedPane\",\n \"daemon\",\n \"remoteSetup\",\n \"nextCommands\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"npm\",\n \"pip\"\n ]\n },\n \"wrapper\": {\n \"type\": \"object\",\n \"required\": [\n \"runtime\",\n \"version\",\n \"paneDir\",\n \"endpoint\"\n ],\n \"properties\": {\n \"runtime\": {\n \"enum\": [\n \"node\",\n \"python\"\n ]\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"paneDir\": {\n \"type\": \"string\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"platform\": {\n \"type\": \"object\",\n \"properties\": {\n \"os\": {\n \"type\": \"string\"\n },\n \"arch\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"release\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"installedPane\": {\n \"type\": \"object\",\n \"required\": [\n \"found\"\n ],\n \"properties\": {\n \"found\": {\n \"type\": \"boolean\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"daemon\": {\n \"type\": \"object\",\n \"required\": [\n \"reachable\",\n \"endpoint\"\n ],\n \"properties\": {\n \"reachable\": {\n \"type\": \"boolean\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"result\": {\n \"type\": \"object\"\n },\n \"error\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"remoteSetup\": {\n \"type\": \"object\",\n \"required\": [\n \"ready\",\n \"displayAvailable\",\n \"headlessEnvironmentApplied\",\n \"diagnostics\"\n ],\n \"properties\": {\n \"ready\": {\n \"type\": \"boolean\"\n },\n \"displayAvailable\": {\n \"type\": \"boolean\"\n },\n \"headlessEnvironmentApplied\": {\n \"type\": \"boolean\"\n },\n \"diagnostics\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"severity\",\n \"message\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"PANE_APPIMAGE_FUSE_MISSING\",\n \"PANE_ELECTRON_SANDBOX_ROOT\",\n \"PANE_ELECTRON_SANDBOX_UNAVAILABLE\",\n \"PANE_USER_SERVICE_UNAVAILABLE\"\n ]\n },\n \"severity\": {\n \"enum\": [\n \"warning\",\n \"error\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"recoveryCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommands\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repos\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repos\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"name\",\n \"path\",\n \"active\",\n \"sessionCount\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n },\n \"sessionCount\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"path\"\n ],\n \"properties\": {\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"created\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"created\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"preview\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"path\",\n \"alreadyExists\",\n \"wouldCreate\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"alreadyExists\": {\n \"type\": \"boolean\"\n },\n \"wouldCreate\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"repo\",\n \"panes\"\n ],\n \"properties\": {\n \"repo\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"const\": true\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panes\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"tool\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"worktreeName\": {\n \"type\": \"string\"\n },\n \"baseBranch\": {\n \"type\": \"string\"\n },\n \"sessionPrompt\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"agent\"\n ],\n \"properties\": {\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"command\"\n ],\n \"properties\": {\n \"command\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"timeoutMs\": {\n \"type\": \"number\"\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n },\n \"concurrency\": {\n \"type\": \"number\"\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repo\",\n \"items\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"name\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"sessionId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"type\": \"object\",\n \"required\": [\n \"delivered\",\n \"submitted\",\n \"inputBytes\"\n ],\n \"properties\": {\n \"delivered\": {\n \"type\": \"boolean\"\n },\n \"submitted\": {\n \"type\": \"boolean\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\",\n \"argument\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\",\n \"argument\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"staged\": {\n \"type\": \"boolean\"\n },\n \"attempts\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panes\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"panes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"paneId\",\n \"name\",\n \"status\",\n \"worktreePath\",\n \"repoId\",\n \"panelCount\",\n \"pinned\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"repoId\": {\n \"type\": \"number\"\n },\n \"repoName\": {\n \"type\": \"string\"\n },\n \"panelCount\": {\n \"type\": \"number\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"force\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panePinRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"panePinResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"const\": true\n },\n \"favoritePinnedAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveResult\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"archived\",\n \"forced\",\n \"worktreeCleanup\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"const\": true\n },\n \"forced\": {\n \"type\": \"boolean\"\n },\n \"worktreeCleanup\": {\n \"enum\": [\n \"completed\",\n \"failed\",\n \"timeout\",\n \"not-applicable\"\n ]\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"blocked\",\n \"nextCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"message\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"uncommitted-changes\",\n \"unpushed-commits\",\n \"uncommitted-and-unpushed\",\n \"status-unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panelListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panels\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panels\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"panelId\",\n \"paneId\",\n \"type\",\n \"title\",\n \"active\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"type\": \"string\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"position\": {\n \"type\": \"number\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActiveAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelOutputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"limit\",\n \"returnedCount\",\n \"hasMore\",\n \"outputs\",\n \"text\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"outputs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"type\",\n \"data\",\n \"timestamp\"\n ],\n \"properties\": {\n \"type\": {\n \"type\": \"string\"\n },\n \"data\": {},\n \"timestamp\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextBriefResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"summary\",\n \"rules\",\n \"tools\",\n \"detailCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"brief\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"rules\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"tools\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"arguments\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"detailCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextCommandResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"command\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"command\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"command\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"details\",\n \"arguments\",\n \"examples\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"details\": {\n \"type\": \"string\"\n },\n \"requiresPaneDaemon\": {\n \"type\": \"boolean\"\n },\n \"mutates\": {\n \"type\": \"boolean\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"examples\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"jsonSchemas\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"notes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"panelScreenResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"source\",\n \"limit\",\n \"returnedLineCount\",\n \"hasMore\",\n \"text\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedLineCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"$ref\": \"#/jsonSchemas/paneCreateResult/properties/items/items/oneOf/0/properties/initialInput\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"enter\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"enter\": {\n \"const\": \"cr\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelWaitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\",\n \"screen\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"screen\": {\n \"type\": \"object\",\n \"required\": [\n \"source\",\n \"text\",\n \"hasMore\"\n ],\n \"properties\": {\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentDoctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"agent\",\n \"command\",\n \"available\",\n \"checks\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"environment\": {\n \"enum\": [\n \"wsl\",\n \"windows\",\n \"linux\",\n \"macos\"\n ]\n },\n \"available\": {\n \"type\": \"boolean\"\n },\n \"executablePath\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"checks\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"ok\",\n \"message\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"tool\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"const\": \"terminal\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panelId\",\n \"title\",\n \"active\",\n \"focused\",\n \"tool\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"strategy\": {\n \"enum\": [\n \"auto\",\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"strategy\",\n \"sequenceName\",\n \"verifiedSubmitted\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"agentContext\": {\n \"brief\": {\n \"title\": \"Pane agent context\",\n \"summary\": \"Pane lets a developer manage saved base repositories, user-visible Panes (Pane sessions) for feature/PR work, and terminal-backed panel tabs. A Pane is a visible workspace that normally maps to one Pane-managed git worktree and branch; a panel is a terminal tab inside one Pane and shares that Pane's worktree; an agent is a CLI process running inside a panel.\",\n \"rules\": [\n \"Start with `runpane doctor --json` to understand wrapper, platform, daemon reachability, and the next safe commands before mutating Pane state.\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"Happy path for any user request to use Pane/RunPane: run `runpane doctor --json`, read `runpane agent-context --json`, resolve or add the saved base repo, create the requested visible Pane with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --pinned --no-focus --wait-ready --yes --json` when it should stay visible (or omit `--pinned`, or use `--tool-command <command>` instead of `--agent <agent>`), then validate with `panels wait` or `panels screen`.\",\n \"Treat Pane as the user's visible cockpit for watching/co-driving work. Do not create Panes or panels for private delegation unless the user asked for visible Pane orchestration or the result should appear in the Pane app.\",\n \"Register the saved base repository once with `repos add`; do not register a pre-created worktree as a separate repo unless the user explicitly asks.\",\n \"Use `panes create` for separate visible Panes (Pane sessions) for feature/PR work. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` for reviewer/helper/clean-context tabs that should stay inside an existing Pane and share that Pane's worktree.\",\n \"For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"Skill routing: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for those behaviors, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the workflow map at https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png. For where the local cache and Pane Chat bootstrap live in Pane, reference https://github.com/dcouple/Pane/pull/291. Use GitHub reads against https://github.com/dcouple/skills/tree/main/parsa only to inspect or refresh referenced skill files; do not clone or install it unless the user asks.\",\n \"When an agent creates Panes or panels, pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when declaring a Pane that should remain in the UI's favorite/pin set.\",\n \"Use `runpane agent-context --json` for the full agent-facing CLI context, or `runpane agent-context --command <command> --json` for one detailed command definition.\",\n \"For `agent-context --command`, use canonical spaced names like `panes create`; copied forms like `panes.create` or `runpane panes create` are accepted too.\",\n \"Use `runpane repos list --json` to find the saved repository when unsure after doctor shows the daemon is reachable.\",\n \"If WSL cannot reach Pane or `runpane` resolves to a broken Windows shim, the user may be running Windows Pane; try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`.\",\n \"If the repository exists on disk but is not saved in Pane, use `runpane repos add --path <repo> --yes --json` before creating panes.\",\n \"Use `runpane agents doctor --agent <codex|claude> --repo <selector> --json` when agent availability differs across host, Windows, WSL, or repo environments.\",\n \"Use `runpane panes create --wait-ready` to create Panes and validate initial terminal readiness in one call.\",\n \"Use `runpane panels screen` for compact current state, `panels wait` for readiness/text checks, `panels submit` for ordinary Enter-submitted input, and `panels submit-composer --strategy auto` for agent composers.\",\n \"Use `runpane panels input` only when exact bytes are required, such as Ctrl-C or handcrafted terminal input.\",\n \"After creating Panes or sending terminal input, validate with `panels wait` or bounded `panels screen` before reporting success.\"\n ],\n \"detailCommand\": \"runpane agent-context --command <command> [--json]\",\n \"tools\": [\n {\n \"name\": \"doctor\",\n \"summary\": \"Report wrapper, platform, installed Pane, and daemon reachability before an agent mutates Pane state.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\",\n \"--pane-path <path>\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"arguments\": [\n \"--command <command>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Check whether Codex or Claude is available in the repo environment Pane will use.\",\n \"arguments\": [\n \"--agent <codex|claude>\",\n \"--repo <selector>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"arguments\": [\n \"--path <path>\",\n \"--name <name>\",\n \"--yes\",\n \"--json\",\n \"--dry-run\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--name <name>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--prompt <text>\",\n \"--initial-input-file <path|->\",\n \"--from-json <path|->\",\n \"--source <user|agent>\",\n \"--pinned\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms <ms>\",\n \"--concurrency <count>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--source <user|agent>\",\n \"--force\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--source <user|agent>\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text plus terminal Enter to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer with the correct key sequence, including Ctrl+Enter for Codex.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--strategy <auto|codex-ctrl-enter|enter>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for terminal initialized, ready, idle, or text state with compact output.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--for <condition>\",\n \"--contains <text>\",\n \"--timeout-ms <ms>\",\n \"--interval-ms <ms>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n }\n ]\n },\n \"commands\": {\n \"help\": {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"details\": \"Use this when you need human-readable usage text for a command.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Optional command topic such as \\\"panes create\\\".\"\n }\n ],\n \"examples\": [\n \"runpane help\",\n \"runpane help panes create\"\n ],\n \"notes\": [\n \"Help text is generated from the runpane contract.\"\n ]\n },\n \"setup\": {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"details\": \"Use this for a human-driven Pane setup flow, not for unattended agent orchestration.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [],\n \"examples\": [\n \"runpane setup\"\n ],\n \"notes\": [\n \"In non-interactive shells, setup prints help and exits successfully.\"\n ]\n },\n \"install\": {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"details\": \"Installs or launches Pane release artifacts at command runtime. `install daemon` forwards remote setup flags to Pane.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"target\",\n \"value\": \"<client|daemon>\",\n \"required\": false,\n \"description\": \"Install the desktop client or configure a remote daemon host.\"\n },\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to install.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip interactive prompts where possible.\"\n }\n ],\n \"examples\": [\n \"runpane install client\",\n \"runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"notes\": [\n \"Package install itself is inert; work begins only when runpane is executed.\"\n ]\n },\n \"update\": {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"details\": \"Resolves and installs the selected Pane client release.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to update to.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Print the update plan without downloading.\"\n }\n ],\n \"examples\": [\n \"runpane update\",\n \"runpane update --version latest\"\n ],\n \"notes\": [\n \"Equivalent artifact selection to `runpane install client`.\"\n ]\n },\n \"version\": {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"details\": \"Use this to confirm which wrapper is available. App, daemon, and release diagnostics belong in doctor.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Ignored for version; retained only for parser compatibility.\"\n }\n ],\n \"examples\": [\n \"runpane version\",\n \"runpane --version\"\n ],\n \"notes\": []\n },\n \"doctor\": {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"details\": \"Use this first when an agent needs to understand whether Pane is installed, which wrapper/runtime is running, what daemon endpoint is expected, and whether the running Pane app is reachable. JSON mode should return a report even when the daemon is unreachable.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print a machine-readable environment report.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Inspect a specific Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<format>\",\n \"required\": false,\n \"description\": \"Release artifact format to inspect.\"\n },\n {\n \"name\": \"--verbose\",\n \"required\": false,\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"examples\": [\n \"runpane doctor --json\",\n \"runpane doctor\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ],\n \"notes\": [\n \"Agents should run `runpane doctor --json` before mutating Pane state.\",\n \"The JSON report includes daemon reachability as data; an unreachable daemon is not a reason to skip the rest of the report.\",\n \"Use `runpane agent-context --json` after doctor when full CLI context is needed.\"\n ]\n },\n \"agent-context\": {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"details\": \"Use this first when an agent needs to discover Pane primitives. It is local/offline and does not require a running Pane app.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Print the detailed definition for one command, for example \\\"panes create\\\".\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ],\n \"notes\": [\n \"Default output is brief so AGENTS.md can point here without bloating context.\",\n \"`--command` accepts canonical spaced names and common copied forms, including `panes.create` and `runpane panes create`.\"\n ]\n },\n \"repos list\": {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"details\": \"Use this to find the right Pane-managed repository before creating panes. If the repo is missing, use `repos add` first.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable repository records.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos list --json\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ],\n \"notes\": [\n \"Requires a running Pane app or daemon for the selected Pane data directory.\",\n \"If this fails from WSL with a missing `/tmp/pane-daemon.../daemon.sock`, `volta: command not found`, or a Windows shim error, the user may be running Windows Pane. Retry via `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\",\n \"When using PowerShell from WSL, set a Windows cwd such as `$env:TEMP` or `$env:USERPROFILE` before running runpane to avoid UNC cwd issues.\"\n ]\n },\n \"repos add\": {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"details\": \"Use this when the repository exists on disk but is not saved in Pane yet. It validates the path as a git repository.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"required\": true,\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": false,\n \"description\": \"Saved repository name; defaults to the directory name.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without adding the repo.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos add --path /path/to/repo --yes --json\"\n ],\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ],\n \"notes\": [\n \"It does not create directories or initialize git repositories by default.\",\n \"For a Windows Pane app managing WSL repositories, prefer a saved WSL repo from `repos list` when possible. Adding a brand-new WSL repo from Windows may require WSL-aware path handling.\"\n ]\n },\n \"panes list\": {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"details\": \"Use this after selecting a repository to find existing Pane sessions and their ids before inspecting panels.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panes list --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ],\n \"notes\": [\n \"Without --repo, this lists sessions across saved Pane repositories.\"\n ]\n },\n \"panes create\": {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"details\": \"Use this when the user wants separate visible Panes (Pane sessions) for feature or PR work. Select the saved base repository, choose a built-in agent or custom terminal command, and optionally send initial input after the tool starts. Pane creates and owns a git worktree/branch for each new Pane; do not pre-create a git worktree and register it as a separate repo unless the user explicitly asks. For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": true,\n \"description\": \"Pane/session name.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--prompt\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Alias for --initial-input; sends text after the command is ready.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read a full panes.create request JSON payload.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background/no-focus creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the pane in the background without stealing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created pane.\"\n },\n {\n \"name\": \"--pinned\",\n \"required\": false,\n \"description\": \"Create the pane already pinned (the Pane UI's favorite/pin star); does not imply focus.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait for each created terminal panel to be ready before returning.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness timeout per pane; defaults to 30000.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Accepted for compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n }\n ],\n \"examples\": [\n \"runpane panes create --repo active --name issue-257 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes create --repo active --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ],\n \"notes\": [\n \"At least one of --agent or --tool-command is required unless --from-json is used.\",\n \"`panes create` is for user-visible Pane orchestration, not the agent's default private delegation mechanism.\",\n \"Register the saved base repository once. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` instead when a reviewer/helper should share an existing Pane's worktree.\",\n \"Agent-created Panes should pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when the Pane should remain in the UI's favorite/pin set.\",\n \"The built-in agent templates come from the runpane contract; custom terminal commands can pass agent-specific flags when requested by the user.\",\n \"Use --initial-input-file for multi-line prompts or shell-sensitive initial input.\",\n \"With --wait-ready, verifiedSubmitted is earned from delivery evidence; routing initial input alone does not imply verified submission.\",\n \"If initialInput.blocked.kind is submission_unverified, do not submit again automatically. Inspect initialInput.staged and attempts, then run nextCommand to resolve the ambiguous composer state.\",\n \"When the JSON result includes nextCommand, run it to validate that the terminal produced output before reporting success.\",\n \"Multi-pane requests are created sequentially today. The --concurrency flag is accepted for compatibility, but agents should not rely on parallel creation.\",\n \"For POSIX or WSL command chaining, use a custom terminal command like `bash -lc 'cmd1 && cmd2 && cmd3'`.\",\n \"For Windows PowerShell command chaining, use a custom terminal command like `powershell -NoProfile -Command \\\"cmd1; if ($LASTEXITCODE) { exit $LASTEXITCODE }; cmd2\\\"`.\",\n \"From WSL with Windows Pane, invoke through PowerShell and select the saved WSL repo by name or id, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane panes create --repo \\\"WSL Pane\\\" --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json'`.\",\n \"Use --wait-ready when an agent needs to verify that an agent terminal started instead of only creating a pane.\",\n \"If readiness returns blocked, inspect blocked.suggestedCommand rather than guessing which prompt to answer.\"\n ]\n },\n \"panes archive\": {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"details\": \"Use this to close out a Pane once its PR has merged. Refuses to archive (unless --force) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes, so an agent cannot silently discard work. A merged and pushed branch has no unpushed commits, so it archives cleanly. Waits for worktree removal to finish before returning.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to archive.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; does not change archive safety behavior.\"\n },\n {\n \"name\": \"--force\",\n \"required\": false,\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panes archive --pane <pane-id> --force --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ],\n \"notes\": [\n \"If the result has ok:false with a blocked field, the pane was NOT archived; inspect blocked.code and rerun with --force if discarding the flagged work is intentional.\",\n \"A successful archive waits for the Pane-managed worktree to be removed before returning; check worktreeCleanup in the result for the final outcome.\",\n \"Archiving a main-repo Pane (no Pane-managed worktree) always succeeds immediately since nothing is deleted from disk.\"\n ]\n },\n \"panes pin\": {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to true without reading and toggling it. Repeating the command is idempotent and pinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to pin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes pin --pane <pane-id> --yes --json\",\n \"runpane panes pin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-pinned Pane pinned.\",\n \"Pinning does not focus the Pane.\"\n ]\n },\n \"panes unpin\": {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to false without reading and toggling it. Repeating the command is idempotent and unpinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to unpin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes unpin --pane <pane-id> --yes --json\",\n \"runpane panes unpin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-unpinned Pane unpinned.\",\n \"Unpinning does not focus the Pane.\"\n ]\n },\n \"panels list\": {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"details\": \"Use this to find the terminal panel id to inspect or control after creating or selecting a Pane session.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels list --pane <pane-id> --json\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ],\n \"notes\": [\n \"The ids returned here are stable inputs for `panels output` and `panels input`.\"\n ]\n },\n \"panels output\": {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"details\": \"Use this to inspect recent terminal output from a terminal-backed panel without loading the full history by default. Live terminal scrollback is returned as bounded recent lines; persisted output fallback is returned as bounded records. Terminal control noise is stripped before returning text.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Tool panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum recent output lines or records to read. Defaults to 200.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels output --panel <panel-id> --limit 200 --json\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ],\n \"notes\": [\n \"Default output is bounded to the latest 200 lines or records. Use a larger --limit only when hasMore is true and more history is needed.\",\n \"Use --json when an agent needs timestamps, record types, returnedCount, or hasMore.\",\n \"The output is intended to be agent-readable, but terminal prompts and shell echoes may still appear; use the newest relevant lines before concluding success.\",\n \"Prefer `panels screen` for a smaller current-state read before increasing output limits.\"\n ]\n },\n \"panels input\": {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"details\": \"Use this to answer prompts or continue an agent inside an existing Pane terminal panel. Input is byte-oriented; choose --input-file for exact control over newlines and control characters.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text bytes to send.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read input from a file or stdin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"printf '\\\\003' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"runpane panels input --panel <panel-id> --text \\\"simple text\\\" --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ],\n \"notes\": [\n \"Input is sent exactly as provided. Include a real newline byte when the terminal should receive Enter; across shells, `--input-file` is safer than `--text \\\"...\\\\n\\\"`.\",\n \"Use `--input-file -` or a temp file for multi-line input, quotes, Ctrl-C, or shell-sensitive text.\",\n \"If interrupting a running process, send Ctrl-C first, validate/read output, then send the next command in a separate `panels input` call so bytes are not dropped.\",\n \"After sending input, validate with `runpane panels output --panel <panel-id> --json` before reporting success.\",\n \"Runpane records action metadata and errors for observability, but should not log full input text by default.\",\n \"For ordinary text plus Enter, prefer `panels submit` so the terminal receives a CR Enter byte.\"\n ]\n },\n \"agents doctor\": {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"details\": \"Use this before creating Codex or Claude panes when PATH may differ between macOS/Linux/Windows/WSL or between the wrapper shell and Pane. The check runs through Pane project context, not the wrapper process.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": true,\n \"description\": \"Built-in agent command to diagnose.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Repository selector; defaults to the active Pane repo.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane agents doctor --agent <agent> --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ],\n \"notes\": [\n \"For WSL repos, install the agent inside the WSL distro Pane uses, not only on Windows.\",\n \"This diagnoses built-in agent templates only; custom commands should be validated by creating a pane and reading its screen.\"\n ]\n },\n \"panels screen\": {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"details\": \"Use this for token-safe current terminal state. It prefers active alternate-screen/TUI output, then live scrollback, then persisted output. Default output is bounded to 80 lines.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum lines to return; defaults to 80.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels screen --panel <panel-id> --limit 80 --json\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ],\n \"notes\": [\n \"Use this before `panels output` when an agent only needs the latest visible/current state.\",\n \"If hasMore is true and context is missing, rerun with a larger --limit or use `panels output`.\"\n ]\n },\n \"panels submit\": {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"details\": \"Use this for ordinary interactive submissions. The daemon normalizes a final LF or CRLF to CR, or appends CR if no newline is present. Exact byte workflows remain on `panels input`.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text to submit before Enter.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read text from a file or stdin before Enter.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit --panel <panel-id> --text \\\"2\\\" --yes --json\",\n \"printf \\\"echo hello\\\" | runpane panels submit --panel <panel-id> --input-file - --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ],\n \"notes\": [\n \"The response includes nextCommand for validation. Run it before reporting that the input worked.\",\n \"Use `panels input` for Ctrl-C, escape sequences, or any workflow requiring exact bytes.\"\n ]\n },\n \"panels wait\": {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"details\": \"Use this to validate asynchronous terminal behavior without pulling large scrollback. It returns brief readiness state, blocker hints, a compact screen, and a next command.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"required\": false,\n \"description\": \"Condition to wait for. Defaults to ready for CLI panels and idle otherwise.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text required for --for text; implies --for text when --for is omitted.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Wait timeout; defaults to 30000.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Polling interval; defaults to 500.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json\",\n \"runpane panels wait --panel <panel-id> --contains \\\"Ready\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ],\n \"notes\": [\n \"If blocked is present, do not assume success. Use blocked.suggestedCommand or inspect `panels screen`.\",\n \"The default timeout and screen are intentionally small for agent context safety.\"\n ]\n },\n \"panels create\": {\n \"name\": \"panels create\",\n \"summary\": \"Create a reviewer/helper terminal tab inside an existing Pane.\",\n \"details\": \"Use this to add reviewer, helper, or clean-context tabs to an existing Pane. The new panel shares the existing Pane's worktree and branch; it does not create a separate Pane session. Use `panes create` instead when the user wants a separate visible Pane (Pane session) for feature/PR work. Agent/orchestrator-created panels should pass --source agent or --no-focus so the user stays in the implementation tab. The daemon initializes the terminal immediately and can wait for CLI readiness.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Existing Pane session id to add the panel to.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent command template to launch.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to launch instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"required\": false,\n \"description\": \"Panel title override.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Initial input to send after the tool starts.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the panel in the background without changing active panel.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created panel.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait until the terminal tool is ready.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness wait timeout.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panels create --pane <pane-id> --tool-command <command> --title <title> --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ],\n \"notes\": [\n \"Use this for same-pane reviewer loops after PR creation/testing.\",\n \"Panels share the existing Pane's worktree and branch.\",\n \"`panels create` is for visible helper/reviewer tabs, not the agent's default private delegation mechanism.\",\n \"For agent-created panels, prefer --source agent or --no-focus to avoid stealing focus from the user or implementation tab.\"\n ]\n },\n \"panels submit-composer\": {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"details\": \"Use this after sending text into an agent composer when plain Enter is not the right submit action. Agents should use --strategy auto; Pane owns per-agent submit sequences internally and verifies visible composer state when possible.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"required\": false,\n \"description\": \"Composer submit key sequence strategy.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit-composer --panel <codex-panel-id> --strategy auto --yes --json\",\n \"runpane panels submit-composer --panel <panel-id> --strategy enter --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ],\n \"notes\": [\n \"Use `panels input` or `panels submit` to write prompt text first; this command only submits the current composer.\",\n \"Use --strategy auto for agent workflows; explicit strategies are diagnostic escape hatches.\",\n \"The JSON result includes sequenceName and verifiedSubmitted. If ok is false, follow blocked.suggestedCommand instead of assuming submission happened.\"\n ]\n }\n },\n \"managedBlock\": [\n \"## Pane\",\n \"\",\n \"The developer is using Pane for this repository. Pane can manage saved repositories and create user-visible Panes with terminal-backed tools for planning, discussion, implementation, and review work.\",\n \"\",\n \"This managed guidance was created by [runpane.com](https://runpane.com) for the [Pane repository](https://github.com/dcouple/Pane). Do not delete or overwrite this block; the developer uses it for their workflow and it should remain committed to the main repository unless they explicitly ask to remove it.\",\n \"\",\n \"Pane mental model: a repository is the saved base repo; a Pane is a user-visible feature/PR workspace (Pane session) that normally maps to one Pane-managed git worktree and branch; a panel/tab is a terminal inside one Pane and shares that Pane's worktree; an agent is the CLI process running in a panel.\",\n \"\",\n \"Default happy path when the user asks you to use Pane or RunPane: run `runpane doctor --json`; read `runpane agent-context --json`; resolve the saved base repository with `runpane repos list --json` or add it once with `runpane repos add --path <repo> --yes --json`; create one visible Pane (Pane session) for the requested feature/PR with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json` or the equivalent `--tool-command <command>` form; then validate with `runpane panels wait` or `runpane panels screen` before reporting progress.\",\n \"\",\n \"Use Pane when the user wants visible Panes or co-drivable parallel feature/PR workspaces. Do not use Pane as your default private delegation mechanism; for private background decomposition, use your normal subagent/worktree workflow.\",\n \"\",\n \"Register the main/base repository once. Do not register pre-created git worktrees as separate Pane repositories unless the user explicitly asks.\",\n \"\",\n \"Use `runpane panes create` for separate visible Panes (Pane sessions) for feature/PR work. Use `runpane panels create` for reviewer/helper tabs inside an existing Pane that should share that Pane's worktree.\",\n \"\",\n \"Typical workflow: register the saved base repository once; create one Pane (Pane session) per feature/PR; use panels/tabs inside that Pane for helper or reviewer agents that should share the worktree; archive the Pane after the PR is done to remove it from active Panes and clean up its managed worktree when applicable.\",\n \"\",\n \"Skill routing reference: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for the behavior those words imply, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the [workflow map](https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png).\",\n \"Use those peer references together to choose the phase: discuss/investigate until the work is clear enough to delegate, then ticket/plan/implement/review/PR-test/teach-back as appropriate. The orchestrator and workflow map may point to different skills; reconcile them with the user's request instead of hardcoding a skill list or treating one reference as subordinate.\",\n \"For the Pane implementation source of truth for where the skill cache, cached workflow assets, and Pane Chat bootstrap live, reference [PR #291](https://github.com/dcouple/Pane/pull/291): `main/src/services/skillCacheManager.ts` owns `<PANE_DIR>/skills/`, `.sources/dcouple-skills`, and `pane-chat/runpane-orchestrator.md`; `main/src/services/paneChatManager.ts` owns the tiny bootstrap prompt that tells the selected Pane Chat agent to read that guide.\",\n \"Use GitHub reads against the [Parsa skills folder](https://github.com/dcouple/skills/tree/main/parsa) only to inspect or refresh referenced skill files; do not clone/install the repo unless the user asks.\",\n \"Do not hardcode a specific assistant brand in workflow guidance. Use the Pane agent or custom tool command the user selected, and use `runpane agents doctor --agent <agent> --repo <selector> --json` only when checking a built-in agent template.\",\n \"\",\n \"Start with `runpane doctor --json` before taking Pane actions. Use it to understand wrapper/runtime details, daemon reachability, and the next safe commands.\",\n \"\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22: `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"\",\n \"Use `runpane agent-context --json` for full Pane CLI context. Use `runpane agent-context --command \\\"panels wait\\\" --json` or another command name for detailed schema only when needed.\",\n \"\",\n \"Default to context-safe validation: after creating Panes or sending terminal input, run `runpane panels wait` or `runpane panels screen` before reporting success. Prefer `runpane panels submit` for normal text plus Enter; use `runpane panels input` only for exact bytes such as Ctrl-C or escape sequences.\",\n \"\",\n \"Common commands:\",\n \"- `runpane doctor --json`\",\n \"- `runpane agent-context --json`\",\n \"- `runpane repos list --json`\",\n \"- `runpane repos add --path <repo> --yes --json`\",\n \"- `runpane agents doctor --agent <agent> --repo active --json`\",\n \"- `runpane panes create --repo active --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels list --pane <pane-id> --json`\",\n \"- `runpane panels screen --panel <panel-id> --limit 80 --json`\",\n \"- `runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json`\",\n \"- `runpane panels submit --panel <panel-id> --text \\\"<answer>\\\" --yes --json`\",\n \"- `runpane panels input --panel <panel-id> --input-file <path|-> --yes --json`\",\n \"\",\n \"WSL note: if `runpane doctor --json` cannot find `/tmp/pane-daemon.../daemon.sock` or `runpane` resolves to a broken Windows shim, Pane may be running on Windows. Try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`, then create Panes through the same PowerShell form using the saved WSL repo name or id. Use `runpane agents doctor --agent <agent> --repo <selector> --json` to diagnose the repo environment Pane will actually use.\"\n ]\n }\n}") +RUNPANE_CONTRACT = json.loads("{\n \"$schema\": \"./schema.json\",\n \"schemaVersion\": 1,\n \"name\": \"runpane\",\n \"description\": \"Thin installer and local control CLI for Pane\",\n \"packageInstallPolicy\": [\n \"The packages must not download, install, or configure Pane during package installation.\",\n \"Work starts only when a user runs `runpane ...`.\"\n ],\n \"compatibility\": {\n \"node\": \">=18.17.0\",\n \"python\": \">=3.8\"\n },\n \"terminology\": {\n \"repo\": \"Saved Pane repository/project record\",\n \"pane\": \"User-visible Pane session\",\n \"tool\": \"Terminal-backed tab\",\n \"agent\": \"Built-in agent command template\"\n },\n \"defaults\": {\n \"target\": \"client\",\n \"paneVersion\": \"latest\",\n \"channel\": \"stable\",\n \"format\": \"auto\",\n \"dryRun\": false,\n \"yes\": false,\n \"verbose\": false\n },\n \"enums\": {\n \"installTargets\": [\n \"client\",\n \"daemon\"\n ],\n \"artifactFormats\": [\n \"auto\",\n \"appimage\",\n \"deb\",\n \"dmg\",\n \"zip\",\n \"exe\"\n ],\n \"channels\": [\n \"stable\",\n \"nightly\"\n ],\n \"agents\": [\n \"codex\",\n \"claude\"\n ],\n \"eventSelectors\": [\n \"panel-created\",\n \"terminal-ready\",\n \"prompt-staged\",\n \"prompt-submitted\",\n \"agent-active\",\n \"agent-idle\",\n \"input-required\",\n \"blocked\",\n \"unblocked\",\n \"panel-exited\",\n \"panel-archived\"\n ]\n },\n \"eventSelectorMap\": {\n \"panel-created\": \"panel_created\",\n \"terminal-ready\": \"terminal_ready\",\n \"prompt-staged\": \"prompt_staged\",\n \"prompt-submitted\": \"prompt_submitted\",\n \"agent-active\": \"agent_active\",\n \"agent-idle\": \"agent_idle\",\n \"input-required\": \"input_required\",\n \"blocked\": \"blocked\",\n \"unblocked\": \"unblocked\",\n \"panel-exited\": \"panel_exited\",\n \"panel-archived\": \"panel_archived\"\n },\n \"agentTemplates\": {\n \"codex\": {\n \"title\": \"Codex\",\n \"command\": \"codex --yolo\",\n \"description\": \"Open a Codex terminal tab and allow the initial input to drive the agent.\"\n },\n \"claude\": {\n \"title\": \"Claude Code\",\n \"command\": \"claude --dangerously-skip-permissions\",\n \"description\": \"Open a Claude Code terminal tab and allow the initial input to drive the agent.\"\n }\n },\n \"commands\": [\n {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"usage\": [\n \"runpane help [command]\"\n ]\n },\n {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"usage\": [\n \"runpane setup\"\n ],\n \"interactiveEntrypoint\": true\n },\n {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"usage\": [\n \"runpane install [client|daemon] [options]\"\n ],\n \"defaultTarget\": \"client\",\n \"targets\": [\n \"client\",\n \"daemon\"\n ],\n \"unknownDaemonFlagsForwarded\": true\n },\n {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"usage\": [\n \"runpane update [options]\"\n ],\n \"target\": \"client\"\n },\n {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"usage\": [\n \"runpane version\",\n \"runpane --version\"\n ]\n },\n {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"usage\": [\n \"runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"usage\": [\n \"runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"usage\": [\n \"runpane agent-context [--json]\",\n \"runpane agent-context --command <command> [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"usage\": [\n \"runpane repos list [--json] [--pane-dir <path>]\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"usage\": [\n \"runpane repos add --path <path> [--name <name>] [--json] [--yes]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions in a saved repository.\",\n \"usage\": [\n \"runpane panes list [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"usage\": [\n \"runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [options]\",\n \"runpane panes create --from-json <path|-> [--yes] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"usage\": [\n \"runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"usage\": [\n \"runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready] --yes [--json]\",\n \"runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--focus|--no-focus] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"usage\": [\n \"runpane panels list --pane <pane-id> [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"usage\": [\n \"runpane panels output --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"usage\": [\n \"runpane panels screen --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"usage\": [\n \"runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"usage\": [\n \"runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"usage\": [\n \"runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"usage\": [\n \"runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ]\n },\n {\n \"name\": \"panels events\",\n \"summary\": \"Replay retained semantic panel events after a cursor.\",\n \"usage\": [\n \"runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]\"\n ],\n \"exitCodes\": {\n \"0\": \"success\",\n \"1\": \"transport or other error\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"panelEventsResult\"\n ]\n },\n {\n \"name\": \"panels watch\",\n \"summary\": \"Stream semantic panel events as compact JSONL.\",\n \"usage\": [\n \"runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]\"\n ],\n \"outputMode\": \"jsonl\",\n \"exitCodes\": {\n \"0\": \"clean stop\",\n \"1\": \"transport or other error\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"semanticEvent\",\n \"cursorExpiredError\"\n ]\n },\n {\n \"name\": \"panels await\",\n \"summary\": \"Wait for the first matching semantic panel event.\",\n \"usage\": [\n \"runpane panels await --panel <panel-id> --event <selector> [--since <cursor>] [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]\"\n ],\n \"exitCodes\": {\n \"0\": \"matched\",\n \"1\": \"transport or other error\",\n \"2\": \"timed out\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"panelAwaitResult\",\n \"cursorExpiredError\"\n ]\n }\n ],\n \"flags\": {\n \"wrapper\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"description\": \"Pane release to install or inspect.\"\n },\n {\n \"name\": \"--download-dir\",\n \"value\": \"<path>\",\n \"description\": \"Directory for downloaded Pane release artifacts.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"description\": \"Use or inspect an existing Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"description\": \"Pane release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"description\": \"Print the plan without downloading or installing.\"\n },\n {\n \"name\": \"--yes\",\n \"aliases\": [\n \"-y\"\n ],\n \"description\": \"Skip interactive prompts where possible.\"\n },\n {\n \"name\": \"--verbose\",\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"remoteValue\": [\n {\n \"name\": \"--label\",\n \"value\": \"<name>\"\n },\n {\n \"name\": \"--prefer-tunnel\",\n \"value\": \"<tailscale|ssh|manual|auto>\"\n },\n {\n \"name\": \"--channel\",\n \"value\": \"<stable|nightly>\"\n },\n {\n \"name\": \"--base-url\",\n \"value\": \"<url>\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\"\n },\n {\n \"name\": \"--listen-port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--repo-ref\",\n \"value\": \"<ref>\"\n }\n ],\n \"remoteBoolean\": [\n {\n \"name\": \"--auto-listen-port\"\n },\n {\n \"name\": \"--interactive-tailscale-setup\"\n },\n {\n \"name\": \"--no-install-service\"\n },\n {\n \"name\": \"--no-tailscale-serve\"\n },\n {\n \"name\": \"--print-only\"\n }\n ],\n \"localValue\": [\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"description\": \"Connect to a Pane daemon using this Pane data directory.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"description\": \"Pane/session id to inspect.\"\n },\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"description\": \"Tool panel id to inspect or control.\"\n },\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"description\": \"Name for the registered repository or created pane/session.\"\n },\n {\n \"name\": \"--worktree-name\",\n \"value\": \"<name>\",\n \"description\": \"Worktree name to request. Defaults to --name.\"\n },\n {\n \"name\": \"--base-branch\",\n \"value\": \"<branch>\",\n \"description\": \"Base branch for the created worktree.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"description\": \"Terminal tab title. Defaults to the selected agent title or Terminal.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"aliases\": [\n \"--prompt\"\n ],\n \"description\": \"Text to send to the terminal after the command is ready. --prompt is an alias.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"description\": \"Read a full panes.create request JSON payload from a file or stdin.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Maximum time to wait for each pane creation job.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Readiness wait timeout for panes create --wait-ready.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"description\": \"Accepted for forward compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"description\": \"Maximum recent output lines or records to read.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"description\": \"Panel wait condition.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"description\": \"Text to wait for with panels wait --for text.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Polling interval for panels wait.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"description\": \"Semantic event selector in kebab-case.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"description\": \"Replay events strictly after this cursor.\"\n },\n {\n \"name\": \"--heartbeat-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Periodic event-log and state reconciliation interval.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"description\": \"Text bytes to send to a terminal panel.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read panel input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"description\": \"Identify whether a local-control mutation is user-initiated or agent/orchestrator-initiated.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"description\": \"Composer submit key sequence strategy for panels submit-composer.\"\n }\n ],\n \"localBoolean\": [\n {\n \"name\": \"--json\",\n \"description\": \"Print machine-readable JSON output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"description\": \"Wait for created terminal panels to be ready before returning.\"\n },\n {\n \"name\": \"--no-focus\",\n \"description\": \"Create the pane or panel in the background without changing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"description\": \"Explicitly focus the created pane or panel.\"\n },\n {\n \"name\": \"--pinned\",\n \"description\": \"Create the pane already pinned (the UI's favorite/pin star).\"\n },\n {\n \"name\": \"--force\",\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n },\n {\n \"name\": \"--jsonl\",\n \"description\": \"Print one compact JSON object per line (panels watch always uses JSONL).\"\n }\n ]\n },\n \"help\": {\n \"npm\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z> Pane release to install\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path> Use an existing Pane executable\",\n \" --dry-run Print the plan without downloading\",\n \" --yes Skip interactive prompts where possible\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [options]\",\n \"\",\n \"Updates Pane using the same artifact selection as \\\"runpane install client\\\".\",\n \"\",\n \"Options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json Print a machine-readable environment report\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --pane-path <path> Inspect a specific Pane executable\",\n \" --format <format> Release artifact format to inspect\",\n \" --verbose Print extra diagnostics\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json Print machine-readable output\",\n \" --pane-dir <path> Connect to a specific Pane data directory\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path> Existing git repository path\",\n \" --name <name> Saved repository name; defaults to the directory name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without adding the repo\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to archive\",\n \" --source <user|agent> Mark mutation source\",\n \" --force Archive even if the pane has uncommitted, untracked, or unpushed changes\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to pin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without pinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to unpin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without unpinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads bounded recent terminal output from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Tool panel id\",\n \" --limit <count> Maximum recent output lines or records to read\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text bytes to send\",\n \" --input-file <path|-> Read input from a file or stdin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command> Print the full definition for one runpane command\",\n \" --json Print machine-readable output\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels events\": [\n \"Usage:\",\n \" runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]\",\n \"\",\n \"Replays retained semantic events strictly after a cursor.\",\n \"\",\n \"Exit codes: 0 success, 3 cursor expired, 1 transport/error.\"\n ],\n \"panels watch\": [\n \"Usage:\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]\",\n \"\",\n \"Streams one compact semantic event JSON object per stdout line.\",\n \"\",\n \"Exit codes: 0 clean stop, 3 cursor expired, 1 transport/error.\"\n ],\n \"panels await\": [\n \"Usage:\",\n \" runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]\",\n \"\",\n \"Waits for a matching semantic event and periodically reconciles state.\",\n \"\",\n \"Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport/error.\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n },\n \"pip\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" pipx run runpane install client\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \" uvx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [--version <latest|vX.Y.Z>] [--dry-run] [--yes]\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\",\n \" --pane-path <path>\",\n \" --format <format>\",\n \" --verbose\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path>\",\n \" --name <name>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --source <user|agent>\",\n \" --force\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads recent terminal output records from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --limit <count>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --text <text>\",\n \" --input-file <path|->\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command>\",\n \" --json\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels events\": [\n \"Usage:\",\n \" python -m runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]\",\n \"\",\n \"Replays retained semantic events strictly after a cursor.\"\n ],\n \"panels watch\": [\n \"Usage:\",\n \" python -m runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]\",\n \"\",\n \"Streams one compact semantic event JSON object per stdout line.\"\n ],\n \"panels await\": [\n \"Usage:\",\n \" python -m runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]\",\n \"\",\n \"Waits for a matching semantic event and periodically reconciles state.\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" python -m runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n }\n },\n \"docs\": {\n \"maintainerRules\": [\n \"Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\",\n \"Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\",\n \"The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\",\n \"Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\",\n \"Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\",\n \"`pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\",\n \"Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\"\n ],\n \"recommendedQuickStarts\": [\n \"npx --yes runpane@latest\",\n \"npm i -g runpane && runpane setup\",\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"npmCommands\": [\n \"npx --yes runpane@latest\",\n \"npx --yes runpane@latest setup\",\n \"npx --yes runpane@latest install client\",\n \"npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \"pnpm dlx runpane@latest\",\n \"pnpm dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"npm i -g runpane && runpane\",\n \"npm i -g runpane && runpane setup\",\n \"npm i -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"pnpm add -g runpane && runpane\",\n \"pnpm add -g runpane && runpane setup\",\n \"pnpm add -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"yarn dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"bunx runpane@latest install daemon --label \\\"My Server\\\"\"\n ],\n \"pythonCommands\": [\n \"pipx run runpane\",\n \"pipx run runpane setup\",\n \"python -m pip install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"python -m runpane setup\",\n \"runpane install daemon --label \\\"My Server\\\"\",\n \"pipx install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"uvx runpane@latest\",\n \"uvx runpane@latest setup\",\n \"uvx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"python -m runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"packageManagerNotes\": [\n \"Use `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\",\n \"Do not document `pnpm install runpane` as the public CLI install path.\"\n ],\n \"commandUsages\": [\n \"runpane\",\n \"runpane setup\",\n \"runpane install\",\n \"runpane install client\",\n \"runpane install daemon\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane doctor --json\",\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\",\n \"runpane repos list --json\",\n \"runpane repos add --path /path/to/repo --name Pane --yes --json\",\n \"runpane panes list --repo active --json\",\n \"runpane panes create --repo active --name issue-252 --agent <agent> --prompt \\\"Kick off the discussion skill for issue 252\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panels list --pane <pane-id> --json\",\n \"runpane panels output --panel <panel-id> --limit 200 --json\",\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes --json\",\n \"runpane help\",\n \"runpane <command> --help\"\n ],\n \"commandDescriptions\": [\n \"`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. In non-interactive shells or CI, both forms must print help, common commands, and agent discovery hints, then exit successfully instead of waiting for input.\",\n \"`runpane install` is an alias for `runpane install client`.\",\n \"`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\",\n \"`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\",\n \"The wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\",\n \"`runpane update` uses the same release resolution and installer path as `install client`.\",\n \"`runpane version` prints only wrapper package metadata and does not contact, launch, or focus the Pane app or daemon.\",\n \"`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, daemon reachability, and remote-daemon hints. Add `--json` for a machine-readable report that agents should run before mutating Pane state. Installed app version detection must be best-effort and must not launch, focus, or configure Pane; macOS wrappers read app bundle metadata instead of executing `Pane --version`.\",\n \"`runpane agent-context` prints a brief, token-efficient command schema for coding agents without connecting to the Pane daemon.\",\n \"`runpane agent-context --command \\\"panes create\\\"` prints the detailed definition for one command. Add `--json` for machine-readable output.\",\n \"`runpane repos list` connects to the running local Pane daemon and prints saved repository records.\",\n \"`runpane repos add` registers an existing git repository with the running local Pane daemon. It does not create directories or initialize git repositories by default.\",\n \"`runpane panes list` lists Pane sessions, optionally scoped to one saved repository.\",\n \"`runpane panes create` connects to the running local Pane daemon, resolves the requested saved base repository, creates user-visible Pane sessions backed by Pane-managed worktrees/branches, opens terminal-backed tool tabs, and optionally sends initial input to the started tool. Built-in agent panes and `--source agent` default to background/no-focus unless `--focus` is passed.\",\n \"For `panes create --wait-ready`, `initialInput.verifiedSubmitted: true` is reported only after argument attachment or composer-clear plus activity evidence. Routing input does not by itself verify submission.\",\n \"`runpane panes archive` archives a Pane exactly like the UI Archive action, including removal of its Pane-managed git worktree, and refuses (unless `--force`) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes. It waits for worktree removal to finish before returning and reports the outcome in `worktreeCleanup`.\",\n \"`runpane panels list` lists tool panels inside one Pane session.\",\n \"`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.\",\n \"`runpane panels input` sends exact input bytes to one terminal panel. Prefer `--input-file` for newlines, Ctrl-C, quotes, or shell-sensitive text.\",\n \"`runpane panes create --prompt` is an alias for `--initial-input`; request JSON and daemon payloads should use the canonical `initialInput` field.\",\n \"If composer submission cannot be verified without risking a duplicate, the create item is unsuccessful with `initialInput.staged`, `initialInput.attempts`, `initialInput.blocked.kind: submission_unverified`, and an actionable `nextCommand`. The CLI-facing `--prompt` alias maps to this canonical `initialInput` result.\",\n \"When running from WSL while Pane is installed on Windows, the Linux wrapper may look for a missing `/tmp/pane-daemon.../daemon.sock` or resolve to a Windows shim such as Volta. In that case invoke the Windows wrapper through PowerShell from a Windows cwd, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\"\n ],\n \"wrapperFlagNote\": \"The top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\",\n \"localControlFlagNote\": \"`runpane doctor --json`, `runpane repos list`, `runpane panes list`, `runpane panes create`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues.\",\n \"daemonFlagNote\": \"Unknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\",\n \"downloadAttribution\": [\n \"The npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\",\n \"The PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\",\n \"Wrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\",\n \"If the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\",\n \"Wrappers also emit best-effort lifecycle telemetry to `https://runpane.com/api/runpane/telemetry` for command start/success/failure, download request/success/failure, and GitHub fallback usage.\",\n \"Wrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so distinct wrapper users can be counted with `count(DISTINCT properties.install_id)`.\",\n \"Wrapper telemetry must be disabled in CI and when `RUNPANE_TELEMETRY_DISABLED` is set, and must not include raw paths, labels, prompts, raw error text, or environment values.\"\n ],\n \"publishingCredentials\": [\n \"Local implementation, build, and dry-run validation do not need npm or PyPI API tokens.\",\n \"Release publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\",\n \"Fallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\"\n ]\n },\n \"testFixtures\": {\n \"parserSamples\": [\n [\n \"setup\"\n ],\n [\n \"install\"\n ],\n [\n \"install\",\n \"client\",\n \"--version\",\n \"v2.2.8\",\n \"--format\",\n \"dmg\",\n \"--download-dir\",\n \"/tmp/pane-downloads\",\n \"--dry-run\",\n \"--yes\"\n ],\n [\n \"install\",\n \"daemon\",\n \"--label\",\n \"VM\",\n \"--prefer-tunnel\",\n \"ssh\",\n \"--channel\",\n \"nightly\",\n \"--base-url\",\n \"https://example.test\",\n \"--pane-dir\",\n \"/tmp/pane\",\n \"--listen-port\",\n \"4555\",\n \"--auto-listen-port\",\n \"--print-only\",\n \"--repo-ref\",\n \"main\",\n \"--unknown-future-flag\",\n \"future-value\",\n \"--dry-run\",\n \"--verbose\"\n ],\n [\n \"update\",\n \"--version\",\n \"latest\",\n \"--format\",\n \"appimage\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--dry-run\"\n ],\n [\n \"doctor\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--format\",\n \"zip\",\n \"--verbose\"\n ],\n [\n \"doctor\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"agent-context\"\n ],\n [\n \"agent-context\",\n \"--command\",\n \"panes create\",\n \"--json\"\n ],\n [\n \"repos\",\n \"list\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"repos\",\n \"add\",\n \"--path\",\n \"/tmp/repo\",\n \"--name\",\n \"Repo\",\n \"--dry-run\",\n \"--yes\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"panes\",\n \"list\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--repo\",\n \"active\",\n \"--name\",\n \"issue-252\",\n \"--agent\",\n \"codex\",\n \"--prompt\",\n \"Kick off discussion\",\n \"--source\",\n \"agent\",\n \"--pinned\",\n \"--dry-run\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"archive\",\n \"--pane\",\n \"session-1\",\n \"--force\",\n \"--source\",\n \"agent\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"pin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"unpin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"list\",\n \"--pane\",\n \"session-1\",\n \"--json\"\n ],\n [\n \"panels\",\n \"output\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"200\",\n \"--json\"\n ],\n [\n \"panels\",\n \"input\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"Continue\\\\n\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"--version\"\n ],\n [\n \"agents\",\n \"doctor\",\n \"--agent\",\n \"codex\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--source\",\n \"agent\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"45000\",\n \"--concurrency\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"screen\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"80\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"wait\",\n \"--panel\",\n \"panel-1\",\n \"--for\",\n \"text\",\n \"--contains\",\n \"ready\",\n \"--timeout-ms\",\n \"30000\",\n \"--interval-ms\",\n \"500\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--source\",\n \"agent\",\n \"--no-focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"15000\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--focus\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit-composer\",\n \"--panel\",\n \"panel-1\",\n \"--strategy\",\n \"auto\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"events\",\n \"--panel\",\n \"panel-1\",\n \"--event\",\n \"agent-idle\",\n \"--since\",\n \"epoch:1\",\n \"--json\"\n ],\n [\n \"panels\",\n \"watch\",\n \"--panel\",\n \"panel-1\",\n \"--event\",\n \"blocked\",\n \"--since\",\n \"epoch:2\",\n \"--heartbeat-ms\",\n \"1000\",\n \"--jsonl\"\n ],\n [\n \"panels\",\n \"await\",\n \"--panel\",\n \"panel-1\",\n \"--event\",\n \"agent-idle\",\n \"--timeout-ms\",\n \"5000\",\n \"--heartbeat-ms\",\n \"500\",\n \"--json\"\n ]\n ],\n \"topLevelHelpIncludes\": [\n \"runpane setup\",\n \"runpane install\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane agent-context\",\n \"runpane repos list\",\n \"runpane repos add\",\n \"runpane panes list\",\n \"runpane panes create\",\n \"runpane panes archive\",\n \"runpane panels create\",\n \"runpane panels list\",\n \"runpane panels output\",\n \"runpane panels input\",\n \"runpane agents doctor\",\n \"runpane panels screen\",\n \"runpane panels submit\",\n \"runpane panels submit-composer\",\n \"runpane panels wait\",\n \"runpane panels events\",\n \"runpane panels watch\",\n \"runpane panels await\",\n \"runpane doctor --json\",\n \"runpane agent-context --json\",\n \"Agent discovery:\",\n \"Common commands:\"\n ],\n \"npmHelpIncludes\": [\n \"pnpm dlx runpane@latest\",\n \"npx --yes runpane@latest\"\n ],\n \"pipHelpIncludes\": [\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"installHelpIncludes\": [\n \"--version <latest|vX.Y.Z>\",\n \"--format <auto|appimage|deb|dmg|zip|exe>\",\n \"--download-dir <path>\",\n \"--pane-path <path>\",\n \"--label <name>\",\n \"--prefer-tunnel <tailscale|ssh|manual|auto>\",\n \"--repo-ref <ref>\"\n ]\n },\n \"jsonSchemas\": {\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"doctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"source\",\n \"wrapper\",\n \"release\",\n \"installedPane\",\n \"daemon\",\n \"remoteSetup\",\n \"nextCommands\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"npm\",\n \"pip\"\n ]\n },\n \"wrapper\": {\n \"type\": \"object\",\n \"required\": [\n \"runtime\",\n \"version\",\n \"paneDir\",\n \"endpoint\"\n ],\n \"properties\": {\n \"runtime\": {\n \"enum\": [\n \"node\",\n \"python\"\n ]\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"paneDir\": {\n \"type\": \"string\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"platform\": {\n \"type\": \"object\",\n \"properties\": {\n \"os\": {\n \"type\": \"string\"\n },\n \"arch\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"release\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"installedPane\": {\n \"type\": \"object\",\n \"required\": [\n \"found\"\n ],\n \"properties\": {\n \"found\": {\n \"type\": \"boolean\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"daemon\": {\n \"type\": \"object\",\n \"required\": [\n \"reachable\",\n \"endpoint\"\n ],\n \"properties\": {\n \"reachable\": {\n \"type\": \"boolean\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"result\": {\n \"type\": \"object\"\n },\n \"error\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"remoteSetup\": {\n \"type\": \"object\",\n \"required\": [\n \"ready\",\n \"displayAvailable\",\n \"headlessEnvironmentApplied\",\n \"diagnostics\"\n ],\n \"properties\": {\n \"ready\": {\n \"type\": \"boolean\"\n },\n \"displayAvailable\": {\n \"type\": \"boolean\"\n },\n \"headlessEnvironmentApplied\": {\n \"type\": \"boolean\"\n },\n \"diagnostics\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"severity\",\n \"message\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"PANE_APPIMAGE_FUSE_MISSING\",\n \"PANE_ELECTRON_SANDBOX_ROOT\",\n \"PANE_ELECTRON_SANDBOX_UNAVAILABLE\",\n \"PANE_USER_SERVICE_UNAVAILABLE\"\n ]\n },\n \"severity\": {\n \"enum\": [\n \"warning\",\n \"error\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"recoveryCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommands\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repos\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repos\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"name\",\n \"path\",\n \"active\",\n \"sessionCount\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n },\n \"sessionCount\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"path\"\n ],\n \"properties\": {\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"created\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"created\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"preview\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"path\",\n \"alreadyExists\",\n \"wouldCreate\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"alreadyExists\": {\n \"type\": \"boolean\"\n },\n \"wouldCreate\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"repo\",\n \"panes\"\n ],\n \"properties\": {\n \"repo\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"const\": true\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panes\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"tool\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"worktreeName\": {\n \"type\": \"string\"\n },\n \"baseBranch\": {\n \"type\": \"string\"\n },\n \"sessionPrompt\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"agent\"\n ],\n \"properties\": {\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"command\"\n ],\n \"properties\": {\n \"command\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"timeoutMs\": {\n \"type\": \"number\"\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n },\n \"concurrency\": {\n \"type\": \"number\"\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repo\",\n \"items\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"name\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"sessionId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"type\": \"object\",\n \"required\": [\n \"delivered\",\n \"submitted\",\n \"inputBytes\"\n ],\n \"properties\": {\n \"delivered\": {\n \"type\": \"boolean\"\n },\n \"submitted\": {\n \"type\": \"boolean\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\",\n \"argument\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\",\n \"argument\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"staged\": {\n \"type\": \"boolean\"\n },\n \"attempts\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panes\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"panes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"paneId\",\n \"name\",\n \"status\",\n \"worktreePath\",\n \"repoId\",\n \"panelCount\",\n \"pinned\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"repoId\": {\n \"type\": \"number\"\n },\n \"repoName\": {\n \"type\": \"string\"\n },\n \"panelCount\": {\n \"type\": \"number\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"force\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panePinRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"panePinResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"const\": true\n },\n \"favoritePinnedAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveResult\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"archived\",\n \"forced\",\n \"worktreeCleanup\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"const\": true\n },\n \"forced\": {\n \"type\": \"boolean\"\n },\n \"worktreeCleanup\": {\n \"enum\": [\n \"completed\",\n \"failed\",\n \"timeout\",\n \"not-applicable\"\n ]\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"blocked\",\n \"nextCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"message\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"uncommitted-changes\",\n \"unpushed-commits\",\n \"uncommitted-and-unpushed\",\n \"status-unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panelListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panels\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panels\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"panelId\",\n \"paneId\",\n \"type\",\n \"title\",\n \"active\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"type\": \"string\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"position\": {\n \"type\": \"number\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActiveAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelOutputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"limit\",\n \"returnedCount\",\n \"hasMore\",\n \"outputs\",\n \"text\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"outputs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"type\",\n \"data\",\n \"timestamp\"\n ],\n \"properties\": {\n \"type\": {\n \"type\": \"string\"\n },\n \"data\": {},\n \"timestamp\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextBriefResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"summary\",\n \"rules\",\n \"tools\",\n \"detailCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"brief\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"rules\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"tools\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"arguments\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"detailCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextCommandResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"command\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"command\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"command\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"details\",\n \"arguments\",\n \"examples\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"details\": {\n \"type\": \"string\"\n },\n \"requiresPaneDaemon\": {\n \"type\": \"boolean\"\n },\n \"mutates\": {\n \"type\": \"boolean\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"examples\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"jsonSchemas\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"notes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"panelScreenResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"source\",\n \"limit\",\n \"returnedLineCount\",\n \"hasMore\",\n \"text\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedLineCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"$ref\": \"#/jsonSchemas/paneCreateResult/properties/items/items/oneOf/0/properties/initialInput\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"enter\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"enter\": {\n \"const\": \"cr\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"semanticEvent\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"cursor\",\n \"type\",\n \"at\",\n \"panelId\",\n \"state\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"cursor\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"enum\": [\n \"panel_created\",\n \"terminal_ready\",\n \"prompt_staged\",\n \"prompt_submitted\",\n \"agent_active\",\n \"agent_idle\",\n \"input_required\",\n \"blocked\",\n \"unblocked\",\n \"panel_exited\",\n \"panel_archived\"\n ]\n },\n \"at\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"state\": {\n \"$ref\": \"#/jsonSchemas/panelWaitResult/properties/state\"\n },\n \"resolvedBy\": {\n \"enum\": [\n \"event\",\n \"reconciliation\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"cursorExpiredError\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"earliestCursor\",\n \"reconcileCommand\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"cursor_expired\"\n ]\n },\n \"earliestCursor\": {\n \"type\": \"string\"\n },\n \"reconcileCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelEventsResult\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"events\",\n \"cursor\"\n ],\n \"properties\": {\n \"ok\": {\n \"enum\": [\n true\n ]\n },\n \"events\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/jsonSchemas/semanticEvent\"\n }\n },\n \"cursor\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"enum\": [\n false\n ]\n },\n \"error\": {\n \"$ref\": \"#/jsonSchemas/cursorExpiredError\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panelAwaitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"timedOut\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"matchedEvent\": {\n \"enum\": [\n \"panel_created\",\n \"terminal_ready\",\n \"prompt_staged\",\n \"prompt_submitted\",\n \"agent_active\",\n \"agent_idle\",\n \"input_required\",\n \"blocked\",\n \"unblocked\",\n \"panel_exited\",\n \"panel_archived\"\n ]\n },\n \"resolvedBy\": {\n \"enum\": [\n \"event\",\n \"reconciliation\"\n ]\n },\n \"event\": {\n \"$ref\": \"#/jsonSchemas/semanticEvent\"\n },\n \"state\": {\n \"$ref\": \"#/jsonSchemas/panelWaitResult/properties/state\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelWaitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\",\n \"screen\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"screen\": {\n \"type\": \"object\",\n \"required\": [\n \"source\",\n \"text\",\n \"hasMore\"\n ],\n \"properties\": {\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentDoctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"agent\",\n \"command\",\n \"available\",\n \"checks\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"environment\": {\n \"enum\": [\n \"wsl\",\n \"windows\",\n \"linux\",\n \"macos\"\n ]\n },\n \"available\": {\n \"type\": \"boolean\"\n },\n \"executablePath\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"checks\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"ok\",\n \"message\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"tool\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"const\": \"terminal\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panelId\",\n \"title\",\n \"active\",\n \"focused\",\n \"tool\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"strategy\": {\n \"enum\": [\n \"auto\",\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"strategy\",\n \"sequenceName\",\n \"verifiedSubmitted\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"agentContext\": {\n \"brief\": {\n \"title\": \"Pane agent context\",\n \"summary\": \"Pane lets a developer manage saved base repositories, user-visible Panes (Pane sessions) for feature/PR work, and terminal-backed panel tabs. A Pane is a visible workspace that normally maps to one Pane-managed git worktree and branch; a panel is a terminal tab inside one Pane and shares that Pane's worktree; an agent is a CLI process running inside a panel.\",\n \"rules\": [\n \"Start with `runpane doctor --json` to understand wrapper, platform, daemon reachability, and the next safe commands before mutating Pane state.\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"Happy path for any user request to use Pane/RunPane: run `runpane doctor --json`, read `runpane agent-context --json`, resolve or add the saved base repo, create the requested visible Pane with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --pinned --no-focus --wait-ready --yes --json` when it should stay visible (or omit `--pinned`, or use `--tool-command <command>` instead of `--agent <agent>`), then validate with `panels wait` or `panels screen`.\",\n \"Treat Pane as the user's visible cockpit for watching/co-driving work. Do not create Panes or panels for private delegation unless the user asked for visible Pane orchestration or the result should appear in the Pane app.\",\n \"Register the saved base repository once with `repos add`; do not register a pre-created worktree as a separate repo unless the user explicitly asks.\",\n \"Use `panes create` for separate visible Panes (Pane sessions) for feature/PR work. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` for reviewer/helper/clean-context tabs that should stay inside an existing Pane and share that Pane's worktree.\",\n \"For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"Skill routing: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for those behaviors, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the workflow map at https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png. For where the local cache and Pane Chat bootstrap live in Pane, reference https://github.com/dcouple/Pane/pull/291. Use GitHub reads against https://github.com/dcouple/skills/tree/main/parsa only to inspect or refresh referenced skill files; do not clone or install it unless the user asks.\",\n \"When an agent creates Panes or panels, pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when declaring a Pane that should remain in the UI's favorite/pin set.\",\n \"Use `runpane agent-context --json` for the full agent-facing CLI context, or `runpane agent-context --command <command> --json` for one detailed command definition.\",\n \"For `agent-context --command`, use canonical spaced names like `panes create`; copied forms like `panes.create` or `runpane panes create` are accepted too.\",\n \"Use `runpane repos list --json` to find the saved repository when unsure after doctor shows the daemon is reachable.\",\n \"If WSL cannot reach Pane or `runpane` resolves to a broken Windows shim, the user may be running Windows Pane; try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`.\",\n \"If the repository exists on disk but is not saved in Pane, use `runpane repos add --path <repo> --yes --json` before creating panes.\",\n \"Use `runpane agents doctor --agent <codex|claude> --repo <selector> --json` when agent availability differs across host, Windows, WSL, or repo environments.\",\n \"Use `runpane panes create --wait-ready` to create Panes and validate initial terminal readiness in one call.\",\n \"Use `runpane panels screen` for compact current state, `panels wait` for readiness/text checks, `panels submit` for ordinary Enter-submitted input, and `panels submit-composer --strategy auto` for agent composers.\",\n \"Use `runpane panels input` only when exact bytes are required, such as Ctrl-C or handcrafted terminal input.\",\n \"After creating Panes or sending terminal input, validate with `panels wait` or bounded `panels screen` before reporting success.\"\n ],\n \"detailCommand\": \"runpane agent-context --command <command> [--json]\",\n \"tools\": [\n {\n \"name\": \"doctor\",\n \"summary\": \"Report wrapper, platform, installed Pane, and daemon reachability before an agent mutates Pane state.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\",\n \"--pane-path <path>\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"arguments\": [\n \"--command <command>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Check whether Codex or Claude is available in the repo environment Pane will use.\",\n \"arguments\": [\n \"--agent <codex|claude>\",\n \"--repo <selector>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"arguments\": [\n \"--path <path>\",\n \"--name <name>\",\n \"--yes\",\n \"--json\",\n \"--dry-run\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--name <name>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--prompt <text>\",\n \"--initial-input-file <path|->\",\n \"--from-json <path|->\",\n \"--source <user|agent>\",\n \"--pinned\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms <ms>\",\n \"--concurrency <count>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--source <user|agent>\",\n \"--force\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--source <user|agent>\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text plus terminal Enter to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer with the correct key sequence, including Ctrl+Enter for Codex.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--strategy <auto|codex-ctrl-enter|enter>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for terminal initialized, ready, idle, or text state with compact output.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--for <condition>\",\n \"--contains <text>\",\n \"--timeout-ms <ms>\",\n \"--interval-ms <ms>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels events\",\n \"summary\": \"Replay semantic panel events after a cursor.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--event <selector>\",\n \"--since <cursor>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels watch\",\n \"summary\": \"Stream semantic panel events as JSONL.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--event <selector>\",\n \"--since <cursor>\",\n \"--heartbeat-ms <ms>\",\n \"--jsonl\"\n ]\n },\n {\n \"name\": \"panels await\",\n \"summary\": \"Wait for a matching semantic panel event.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--event <selector>\",\n \"--since <cursor>\",\n \"--timeout-ms <ms>\",\n \"--heartbeat-ms <ms>\",\n \"--json\"\n ]\n }\n ]\n },\n \"commands\": {\n \"help\": {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"details\": \"Use this when you need human-readable usage text for a command.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Optional command topic such as \\\"panes create\\\".\"\n }\n ],\n \"examples\": [\n \"runpane help\",\n \"runpane help panes create\"\n ],\n \"notes\": [\n \"Help text is generated from the runpane contract.\"\n ]\n },\n \"setup\": {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"details\": \"Use this for a human-driven Pane setup flow, not for unattended agent orchestration.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [],\n \"examples\": [\n \"runpane setup\"\n ],\n \"notes\": [\n \"In non-interactive shells, setup prints help and exits successfully.\"\n ]\n },\n \"install\": {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"details\": \"Installs or launches Pane release artifacts at command runtime. `install daemon` forwards remote setup flags to Pane.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"target\",\n \"value\": \"<client|daemon>\",\n \"required\": false,\n \"description\": \"Install the desktop client or configure a remote daemon host.\"\n },\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to install.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip interactive prompts where possible.\"\n }\n ],\n \"examples\": [\n \"runpane install client\",\n \"runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"notes\": [\n \"Package install itself is inert; work begins only when runpane is executed.\"\n ]\n },\n \"update\": {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"details\": \"Resolves and installs the selected Pane client release.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to update to.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Print the update plan without downloading.\"\n }\n ],\n \"examples\": [\n \"runpane update\",\n \"runpane update --version latest\"\n ],\n \"notes\": [\n \"Equivalent artifact selection to `runpane install client`.\"\n ]\n },\n \"version\": {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"details\": \"Use this to confirm which wrapper is available. App, daemon, and release diagnostics belong in doctor.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Ignored for version; retained only for parser compatibility.\"\n }\n ],\n \"examples\": [\n \"runpane version\",\n \"runpane --version\"\n ],\n \"notes\": []\n },\n \"doctor\": {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"details\": \"Use this first when an agent needs to understand whether Pane is installed, which wrapper/runtime is running, what daemon endpoint is expected, and whether the running Pane app is reachable. JSON mode should return a report even when the daemon is unreachable.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print a machine-readable environment report.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Inspect a specific Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<format>\",\n \"required\": false,\n \"description\": \"Release artifact format to inspect.\"\n },\n {\n \"name\": \"--verbose\",\n \"required\": false,\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"examples\": [\n \"runpane doctor --json\",\n \"runpane doctor\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ],\n \"notes\": [\n \"Agents should run `runpane doctor --json` before mutating Pane state.\",\n \"The JSON report includes daemon reachability as data; an unreachable daemon is not a reason to skip the rest of the report.\",\n \"Use `runpane agent-context --json` after doctor when full CLI context is needed.\"\n ]\n },\n \"agent-context\": {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"details\": \"Use this first when an agent needs to discover Pane primitives. It is local/offline and does not require a running Pane app.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Print the detailed definition for one command, for example \\\"panes create\\\".\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ],\n \"notes\": [\n \"Default output is brief so AGENTS.md can point here without bloating context.\",\n \"`--command` accepts canonical spaced names and common copied forms, including `panes.create` and `runpane panes create`.\"\n ]\n },\n \"repos list\": {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"details\": \"Use this to find the right Pane-managed repository before creating panes. If the repo is missing, use `repos add` first.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable repository records.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos list --json\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ],\n \"notes\": [\n \"Requires a running Pane app or daemon for the selected Pane data directory.\",\n \"If this fails from WSL with a missing `/tmp/pane-daemon.../daemon.sock`, `volta: command not found`, or a Windows shim error, the user may be running Windows Pane. Retry via `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\",\n \"When using PowerShell from WSL, set a Windows cwd such as `$env:TEMP` or `$env:USERPROFILE` before running runpane to avoid UNC cwd issues.\"\n ]\n },\n \"repos add\": {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"details\": \"Use this when the repository exists on disk but is not saved in Pane yet. It validates the path as a git repository.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"required\": true,\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": false,\n \"description\": \"Saved repository name; defaults to the directory name.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without adding the repo.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos add --path /path/to/repo --yes --json\"\n ],\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ],\n \"notes\": [\n \"It does not create directories or initialize git repositories by default.\",\n \"For a Windows Pane app managing WSL repositories, prefer a saved WSL repo from `repos list` when possible. Adding a brand-new WSL repo from Windows may require WSL-aware path handling.\"\n ]\n },\n \"panes list\": {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"details\": \"Use this after selecting a repository to find existing Pane sessions and their ids before inspecting panels.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panes list --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ],\n \"notes\": [\n \"Without --repo, this lists sessions across saved Pane repositories.\"\n ]\n },\n \"panes create\": {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"details\": \"Use this when the user wants separate visible Panes (Pane sessions) for feature or PR work. Select the saved base repository, choose a built-in agent or custom terminal command, and optionally send initial input after the tool starts. Pane creates and owns a git worktree/branch for each new Pane; do not pre-create a git worktree and register it as a separate repo unless the user explicitly asks. For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": true,\n \"description\": \"Pane/session name.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--prompt\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Alias for --initial-input; sends text after the command is ready.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read a full panes.create request JSON payload.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background/no-focus creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the pane in the background without stealing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created pane.\"\n },\n {\n \"name\": \"--pinned\",\n \"required\": false,\n \"description\": \"Create the pane already pinned (the Pane UI's favorite/pin star); does not imply focus.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait for each created terminal panel to be ready before returning.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness timeout per pane; defaults to 30000.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Accepted for compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n }\n ],\n \"examples\": [\n \"runpane panes create --repo active --name issue-257 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes create --repo active --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ],\n \"notes\": [\n \"At least one of --agent or --tool-command is required unless --from-json is used.\",\n \"`panes create` is for user-visible Pane orchestration, not the agent's default private delegation mechanism.\",\n \"Register the saved base repository once. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` instead when a reviewer/helper should share an existing Pane's worktree.\",\n \"Agent-created Panes should pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when the Pane should remain in the UI's favorite/pin set.\",\n \"The built-in agent templates come from the runpane contract; custom terminal commands can pass agent-specific flags when requested by the user.\",\n \"Use --initial-input-file for multi-line prompts or shell-sensitive initial input.\",\n \"With --wait-ready, verifiedSubmitted is earned from delivery evidence; routing initial input alone does not imply verified submission.\",\n \"If initialInput.blocked.kind is submission_unverified, do not submit again automatically. Inspect initialInput.staged and attempts, then run nextCommand to resolve the ambiguous composer state.\",\n \"When the JSON result includes nextCommand, run it to validate that the terminal produced output before reporting success.\",\n \"Multi-pane requests are created sequentially today. The --concurrency flag is accepted for compatibility, but agents should not rely on parallel creation.\",\n \"For POSIX or WSL command chaining, use a custom terminal command like `bash -lc 'cmd1 && cmd2 && cmd3'`.\",\n \"For Windows PowerShell command chaining, use a custom terminal command like `powershell -NoProfile -Command \\\"cmd1; if ($LASTEXITCODE) { exit $LASTEXITCODE }; cmd2\\\"`.\",\n \"From WSL with Windows Pane, invoke through PowerShell and select the saved WSL repo by name or id, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane panes create --repo \\\"WSL Pane\\\" --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json'`.\",\n \"Use --wait-ready when an agent needs to verify that an agent terminal started instead of only creating a pane.\",\n \"If readiness returns blocked, inspect blocked.suggestedCommand rather than guessing which prompt to answer.\"\n ]\n },\n \"panes archive\": {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"details\": \"Use this to close out a Pane once its PR has merged. Refuses to archive (unless --force) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes, so an agent cannot silently discard work. A merged and pushed branch has no unpushed commits, so it archives cleanly. Waits for worktree removal to finish before returning.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to archive.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; does not change archive safety behavior.\"\n },\n {\n \"name\": \"--force\",\n \"required\": false,\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panes archive --pane <pane-id> --force --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ],\n \"notes\": [\n \"If the result has ok:false with a blocked field, the pane was NOT archived; inspect blocked.code and rerun with --force if discarding the flagged work is intentional.\",\n \"A successful archive waits for the Pane-managed worktree to be removed before returning; check worktreeCleanup in the result for the final outcome.\",\n \"Archiving a main-repo Pane (no Pane-managed worktree) always succeeds immediately since nothing is deleted from disk.\"\n ]\n },\n \"panes pin\": {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to true without reading and toggling it. Repeating the command is idempotent and pinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to pin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes pin --pane <pane-id> --yes --json\",\n \"runpane panes pin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-pinned Pane pinned.\",\n \"Pinning does not focus the Pane.\"\n ]\n },\n \"panes unpin\": {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to false without reading and toggling it. Repeating the command is idempotent and unpinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to unpin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes unpin --pane <pane-id> --yes --json\",\n \"runpane panes unpin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-unpinned Pane unpinned.\",\n \"Unpinning does not focus the Pane.\"\n ]\n },\n \"panels list\": {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"details\": \"Use this to find the terminal panel id to inspect or control after creating or selecting a Pane session.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels list --pane <pane-id> --json\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ],\n \"notes\": [\n \"The ids returned here are stable inputs for `panels output` and `panels input`.\"\n ]\n },\n \"panels output\": {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"details\": \"Use this to inspect recent terminal output from a terminal-backed panel without loading the full history by default. Live terminal scrollback is returned as bounded recent lines; persisted output fallback is returned as bounded records. Terminal control noise is stripped before returning text.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Tool panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum recent output lines or records to read. Defaults to 200.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels output --panel <panel-id> --limit 200 --json\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ],\n \"notes\": [\n \"Default output is bounded to the latest 200 lines or records. Use a larger --limit only when hasMore is true and more history is needed.\",\n \"Use --json when an agent needs timestamps, record types, returnedCount, or hasMore.\",\n \"The output is intended to be agent-readable, but terminal prompts and shell echoes may still appear; use the newest relevant lines before concluding success.\",\n \"Prefer `panels screen` for a smaller current-state read before increasing output limits.\"\n ]\n },\n \"panels input\": {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"details\": \"Use this to answer prompts or continue an agent inside an existing Pane terminal panel. Input is byte-oriented; choose --input-file for exact control over newlines and control characters.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text bytes to send.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read input from a file or stdin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"printf '\\\\003' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"runpane panels input --panel <panel-id> --text \\\"simple text\\\" --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ],\n \"notes\": [\n \"Input is sent exactly as provided. Include a real newline byte when the terminal should receive Enter; across shells, `--input-file` is safer than `--text \\\"...\\\\n\\\"`.\",\n \"Use `--input-file -` or a temp file for multi-line input, quotes, Ctrl-C, or shell-sensitive text.\",\n \"If interrupting a running process, send Ctrl-C first, validate/read output, then send the next command in a separate `panels input` call so bytes are not dropped.\",\n \"After sending input, validate with `runpane panels output --panel <panel-id> --json` before reporting success.\",\n \"Runpane records action metadata and errors for observability, but should not log full input text by default.\",\n \"For ordinary text plus Enter, prefer `panels submit` so the terminal receives a CR Enter byte.\"\n ]\n },\n \"agents doctor\": {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"details\": \"Use this before creating Codex or Claude panes when PATH may differ between macOS/Linux/Windows/WSL or between the wrapper shell and Pane. The check runs through Pane project context, not the wrapper process.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": true,\n \"description\": \"Built-in agent command to diagnose.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Repository selector; defaults to the active Pane repo.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane agents doctor --agent <agent> --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ],\n \"notes\": [\n \"For WSL repos, install the agent inside the WSL distro Pane uses, not only on Windows.\",\n \"This diagnoses built-in agent templates only; custom commands should be validated by creating a pane and reading its screen.\"\n ]\n },\n \"panels screen\": {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"details\": \"Use this for token-safe current terminal state. It prefers active alternate-screen/TUI output, then live scrollback, then persisted output. Default output is bounded to 80 lines.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum lines to return; defaults to 80.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels screen --panel <panel-id> --limit 80 --json\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ],\n \"notes\": [\n \"Use this before `panels output` when an agent only needs the latest visible/current state.\",\n \"If hasMore is true and context is missing, rerun with a larger --limit or use `panels output`.\"\n ]\n },\n \"panels submit\": {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"details\": \"Use this for ordinary interactive submissions. The daemon normalizes a final LF or CRLF to CR, or appends CR if no newline is present. Exact byte workflows remain on `panels input`.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text to submit before Enter.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read text from a file or stdin before Enter.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit --panel <panel-id> --text \\\"2\\\" --yes --json\",\n \"printf \\\"echo hello\\\" | runpane panels submit --panel <panel-id> --input-file - --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ],\n \"notes\": [\n \"The response includes nextCommand for validation. Run it before reporting that the input worked.\",\n \"Use `panels input` for Ctrl-C, escape sequences, or any workflow requiring exact bytes.\"\n ]\n },\n \"panels wait\": {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"details\": \"Use this to validate asynchronous terminal behavior without pulling large scrollback. It returns brief readiness state, blocker hints, a compact screen, and a next command.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"required\": false,\n \"description\": \"Condition to wait for. Defaults to ready for CLI panels and idle otherwise.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text required for --for text; implies --for text when --for is omitted.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Wait timeout; defaults to 30000.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Polling interval; defaults to 500.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json\",\n \"runpane panels wait --panel <panel-id> --contains \\\"Ready\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ],\n \"notes\": [\n \"If blocked is present, do not assume success. Use blocked.suggestedCommand or inspect `panels screen`.\",\n \"The default timeout and screen are intentionally small for agent context safety.\"\n ]\n },\n \"panels events\": {\n \"name\": \"panels events\",\n \"summary\": \"Replay retained semantic panel events after a cursor.\",\n \"details\": \"Request/response pull surface for the bounded semantic event ring.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": false,\n \"description\": \"Optional panel filter.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional semantic event selector.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Exclusive replay cursor.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels events --since <cursor> --json\"\n ],\n \"jsonSchemas\": [\n \"panelEventsResult\"\n ],\n \"notes\": [\n \"Exit code 3 means cursor_expired; reconcile from the command in the error.\"\n ]\n },\n \"panels watch\": {\n \"name\": \"panels watch\",\n \"summary\": \"Stream semantic panel events as JSONL.\",\n \"details\": \"Uses replay then live delivery on one retained daemon connection with periodic reconciliation.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": false,\n \"description\": \"Optional panel filter.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional semantic event selector.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Exclusive replay cursor.\"\n },\n {\n \"name\": \"--heartbeat-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Reconciliation interval.\"\n },\n {\n \"name\": \"--jsonl\",\n \"required\": false,\n \"description\": \"Describes the always-JSONL output.\"\n }\n ],\n \"examples\": [\n \"runpane panels watch --panel <panel-id> --jsonl\"\n ],\n \"jsonSchemas\": [\n \"semanticEvent\",\n \"cursorExpiredError\"\n ],\n \"notes\": [\n \"Stdout contains compact JSON records only. Exit codes: 0 clean stop, 1 transport error, 3 cursor expired.\"\n ]\n },\n \"panels await\": {\n \"name\": \"panels await\",\n \"summary\": \"Wait for the first matching semantic event.\",\n \"details\": \"Exit-on-first-match sugar over watch with periodic state reconciliation.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Panel to await.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Semantic event selector.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Exclusive replay cursor.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Wait timeout.\"\n },\n {\n \"name\": \"--heartbeat-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Reconciliation interval.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels await --panel <panel-id> --event agent-idle --json\"\n ],\n \"jsonSchemas\": [\n \"panelAwaitResult\",\n \"cursorExpiredError\"\n ],\n \"notes\": [\n \"Exit codes: 0 matched, 1 transport error, 2 timed out, 3 cursor expired.\"\n ]\n },\n \"panels create\": {\n \"name\": \"panels create\",\n \"summary\": \"Create a reviewer/helper terminal tab inside an existing Pane.\",\n \"details\": \"Use this to add reviewer, helper, or clean-context tabs to an existing Pane. The new panel shares the existing Pane's worktree and branch; it does not create a separate Pane session. Use `panes create` instead when the user wants a separate visible Pane (Pane session) for feature/PR work. Agent/orchestrator-created panels should pass --source agent or --no-focus so the user stays in the implementation tab. The daemon initializes the terminal immediately and can wait for CLI readiness.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Existing Pane session id to add the panel to.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent command template to launch.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to launch instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"required\": false,\n \"description\": \"Panel title override.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Initial input to send after the tool starts.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the panel in the background without changing active panel.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created panel.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait until the terminal tool is ready.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness wait timeout.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panels create --pane <pane-id> --tool-command <command> --title <title> --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ],\n \"notes\": [\n \"Use this for same-pane reviewer loops after PR creation/testing.\",\n \"Panels share the existing Pane's worktree and branch.\",\n \"`panels create` is for visible helper/reviewer tabs, not the agent's default private delegation mechanism.\",\n \"For agent-created panels, prefer --source agent or --no-focus to avoid stealing focus from the user or implementation tab.\"\n ]\n },\n \"panels submit-composer\": {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"details\": \"Use this after sending text into an agent composer when plain Enter is not the right submit action. Agents should use --strategy auto; Pane owns per-agent submit sequences internally and verifies visible composer state when possible.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"required\": false,\n \"description\": \"Composer submit key sequence strategy.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit-composer --panel <codex-panel-id> --strategy auto --yes --json\",\n \"runpane panels submit-composer --panel <panel-id> --strategy enter --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ],\n \"notes\": [\n \"Use `panels input` or `panels submit` to write prompt text first; this command only submits the current composer.\",\n \"Use --strategy auto for agent workflows; explicit strategies are diagnostic escape hatches.\",\n \"The JSON result includes sequenceName and verifiedSubmitted. If ok is false, follow blocked.suggestedCommand instead of assuming submission happened.\"\n ]\n }\n },\n \"managedBlock\": [\n \"## Pane\",\n \"\",\n \"The developer is using Pane for this repository. Pane can manage saved repositories and create user-visible Panes with terminal-backed tools for planning, discussion, implementation, and review work.\",\n \"\",\n \"This managed guidance was created by [runpane.com](https://runpane.com) for the [Pane repository](https://github.com/dcouple/Pane). Do not delete or overwrite this block; the developer uses it for their workflow and it should remain committed to the main repository unless they explicitly ask to remove it.\",\n \"\",\n \"Pane mental model: a repository is the saved base repo; a Pane is a user-visible feature/PR workspace (Pane session) that normally maps to one Pane-managed git worktree and branch; a panel/tab is a terminal inside one Pane and shares that Pane's worktree; an agent is the CLI process running in a panel.\",\n \"\",\n \"Default happy path when the user asks you to use Pane or RunPane: run `runpane doctor --json`; read `runpane agent-context --json`; resolve the saved base repository with `runpane repos list --json` or add it once with `runpane repos add --path <repo> --yes --json`; create one visible Pane (Pane session) for the requested feature/PR with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json` or the equivalent `--tool-command <command>` form; then validate with `runpane panels wait` or `runpane panels screen` before reporting progress.\",\n \"\",\n \"Use Pane when the user wants visible Panes or co-drivable parallel feature/PR workspaces. Do not use Pane as your default private delegation mechanism; for private background decomposition, use your normal subagent/worktree workflow.\",\n \"\",\n \"Register the main/base repository once. Do not register pre-created git worktrees as separate Pane repositories unless the user explicitly asks.\",\n \"\",\n \"Use `runpane panes create` for separate visible Panes (Pane sessions) for feature/PR work. Use `runpane panels create` for reviewer/helper tabs inside an existing Pane that should share that Pane's worktree.\",\n \"\",\n \"Typical workflow: register the saved base repository once; create one Pane (Pane session) per feature/PR; use panels/tabs inside that Pane for helper or reviewer agents that should share the worktree; archive the Pane after the PR is done to remove it from active Panes and clean up its managed worktree when applicable.\",\n \"\",\n \"Skill routing reference: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for the behavior those words imply, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the [workflow map](https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png).\",\n \"Use those peer references together to choose the phase: discuss/investigate until the work is clear enough to delegate, then ticket/plan/implement/review/PR-test/teach-back as appropriate. The orchestrator and workflow map may point to different skills; reconcile them with the user's request instead of hardcoding a skill list or treating one reference as subordinate.\",\n \"For the Pane implementation source of truth for where the skill cache, cached workflow assets, and Pane Chat bootstrap live, reference [PR #291](https://github.com/dcouple/Pane/pull/291): `main/src/services/skillCacheManager.ts` owns `<PANE_DIR>/skills/`, `.sources/dcouple-skills`, and `pane-chat/runpane-orchestrator.md`; `main/src/services/paneChatManager.ts` owns the tiny bootstrap prompt that tells the selected Pane Chat agent to read that guide.\",\n \"Use GitHub reads against the [Parsa skills folder](https://github.com/dcouple/skills/tree/main/parsa) only to inspect or refresh referenced skill files; do not clone/install the repo unless the user asks.\",\n \"Do not hardcode a specific assistant brand in workflow guidance. Use the Pane agent or custom tool command the user selected, and use `runpane agents doctor --agent <agent> --repo <selector> --json` only when checking a built-in agent template.\",\n \"\",\n \"Start with `runpane doctor --json` before taking Pane actions. Use it to understand wrapper/runtime details, daemon reachability, and the next safe commands.\",\n \"\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22: `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"\",\n \"Use `runpane agent-context --json` for full Pane CLI context. Use `runpane agent-context --command \\\"panels wait\\\" --json` or another command name for detailed schema only when needed.\",\n \"\",\n \"Default to context-safe validation: after creating Panes or sending terminal input, run `runpane panels wait` or `runpane panels screen` before reporting success. Prefer `runpane panels submit` for normal text plus Enter; use `runpane panels input` only for exact bytes such as Ctrl-C or escape sequences.\",\n \"\",\n \"Common commands:\",\n \"- `runpane doctor --json`\",\n \"- `runpane agent-context --json`\",\n \"- `runpane repos list --json`\",\n \"- `runpane repos add --path <repo> --yes --json`\",\n \"- `runpane agents doctor --agent <agent> --repo active --json`\",\n \"- `runpane panes create --repo active --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels list --pane <pane-id> --json`\",\n \"- `runpane panels screen --panel <panel-id> --limit 80 --json`\",\n \"- `runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json`\",\n \"- `runpane panels submit --panel <panel-id> --text \\\"<answer>\\\" --yes --json`\",\n \"- `runpane panels input --panel <panel-id> --input-file <path|-> --yes --json`\",\n \"\",\n \"WSL note: if `runpane doctor --json` cannot find `/tmp/pane-daemon.../daemon.sock` or `runpane` resolves to a broken Windows shim, Pane may be running on Windows. Try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`, then create Panes through the same PowerShell form using the saved WSL repo name or id. Use `runpane agents doctor --agent <agent> --repo <selector> --json` to diagnose the repo environment Pane will actually use.\"\n ]\n }\n}") diff --git a/packages/runpane-py/src/runpane/local_control.py b/packages/runpane-py/src/runpane/local_control.py index 77e2fd41..addd3196 100644 --- a/packages/runpane-py/src/runpane/local_control.py +++ b/packages/runpane-py/src/runpane/local_control.py @@ -3,9 +3,12 @@ import json import os import sys +import queue +import signal +import time from typing import Any, Dict, Optional -from .daemon_client import invoke_daemon +from .daemon_client import invoke_daemon, RetainedDaemonConnection from .generated_contract import RUNPANE_CONTRACT @@ -270,6 +273,174 @@ def run_panels_wait(parsed: Any) -> int: return 0 if result.get("ok") else 1 +def run_panels_events(parsed: Any) -> int: + result = invoke_daemon("runpane:panels:events", [{"panelId": parsed.panel_id, "event": parsed.event_selector, "since": parsed.since}], pane_dir=parsed.pane_dir) + if not result.get("ok"): + print(json.dumps(result, separators=(",", ":")), file=sys.stderr) + return 3 + if parsed.json: + print_json(result) + else: + for event in result.get("events", []): + print(f"{event.get('cursor')}\t{event.get('type')}\t{event.get('panelId')}") + return 0 + + +class EventStream: + def __init__(self, parsed: Any) -> None: + self.connection = RetainedDaemonConnection(parsed.pane_dir) + self.initial_since = parsed.since + self.processed_cursor = "" + self.emitted_cursor = "" + + def replay(self, resolved_by: str, emit: Any) -> Optional[int]: + response = self.connection.request("runpane:panels:events", [{"since": self.processed_cursor or self.initial_since}]) + if not response.get("ok"): + print(json.dumps(response, separators=(",", ":")), file=sys.stderr) + return 3 + for event in sorted(response.get("events", []), key=cursor_number): + self.deliver(event, resolved_by, emit) + while True: + try: + event = self.connection.events.get_nowait() + except queue.Empty: + break + self.deliver(event, resolved_by, emit) + cursor = response.get("cursor") + if cursor and (not self.processed_cursor or cursor_number(cursor) > cursor_number(self.processed_cursor)): + self.processed_cursor = cursor + return None + + def deliver(self, event: Any, resolved_by: str, emit: Any) -> None: + validate_event(event) + if self.processed_cursor and cursor_number(event["cursor"]) <= cursor_number(self.processed_cursor): + return + self.processed_cursor = event["cursor"] + emit(event, resolved_by) + + def close(self) -> None: + self.connection.close() + + +def run_panels_watch(parsed: Any) -> int: + stream = EventStream(parsed) + stopped = False + def stop(_signum: int, _frame: Any) -> None: + nonlocal stopped + stopped = True + previous_int = signal.signal(signal.SIGINT, stop) + previous_term = signal.signal(signal.SIGTERM, stop) + def emit(event: Dict[str, Any], resolved_by: str) -> None: + if matches_event(parsed, event, False): + print(json.dumps({**event, "resolvedBy": resolved_by}, separators=(",", ":")), flush=True) + stream.emitted_cursor = event["cursor"] + try: + code = stream.replay("event", emit) + if code is not None: + return code + heartbeat = (parsed.heartbeat_ms or 30_000) / 1000 + next_heartbeat = time.monotonic() + heartbeat + while not stopped: + if not stream.connection.errors.empty(): + error = stream.connection.errors.get_nowait() + print(str(error), file=sys.stderr) + return 1 + try: + event = stream.connection.events.get(timeout=min(0.05, max(next_heartbeat - time.monotonic(), 0.001))) + stream.deliver(event, "event", emit) + except queue.Empty: + pass + if time.monotonic() >= next_heartbeat: + code = stream.replay("reconciliation", emit) + if code is not None: + return code + next_heartbeat += heartbeat + return 0 + finally: + signal.signal(signal.SIGINT, previous_int) + signal.signal(signal.SIGTERM, previous_term) + stream.close() + + +def run_panels_await(parsed: Any) -> int: + if not parsed.panel_id or not parsed.event_selector: + raise ValueError("runpane panels await requires --panel and --event.") + stream = EventStream(parsed) + match: Optional[Dict[str, Any]] = None + resolved = "event" + def emit(event: Dict[str, Any], resolved_by: str) -> None: + nonlocal match, resolved + if match is None and matches_event(parsed, event, True): + match, resolved = event, resolved_by + try: + code = stream.replay("event", emit) + if code is not None: + return code + deadline = time.monotonic() + (parsed.timeout_ms or 30_000) / 1000 + heartbeat = (parsed.heartbeat_ms or 30_000) / 1000 + next_heartbeat = time.monotonic() + heartbeat + while match is None and time.monotonic() < deadline: + if not stream.connection.errors.empty(): + print(str(stream.connection.errors.get_nowait()), file=sys.stderr) + return 1 + try: + event = stream.connection.events.get(timeout=min(0.05, max(deadline - time.monotonic(), 0.001))) + stream.deliver(event, "event", emit) + except queue.Empty: + pass + if match is None and time.monotonic() >= next_heartbeat: + code = stream.replay("reconciliation", emit) + if code is not None: + return code + if match is None and is_state_backed(parsed.event_selector): + screen = stream.connection.request("runpane:panels:screen", [{"panelId": parsed.panel_id}]) + event_type = match_state(parsed.event_selector, screen.get("state") or {}) + if event_type: + match = {"id": stream.processed_cursor, "cursor": stream.processed_cursor, "type": event_type, "at": "", "paneId": screen.get("paneId"), "panelId": parsed.panel_id, "state": screen.get("state") or {}} + resolved = "reconciliation" + next_heartbeat += heartbeat + if match is not None: + print(json.dumps({"ok": True, "timedOut": False, "matchedEvent": match["type"], "resolvedBy": resolved, "event": match, "state": match["state"]}, separators=(",", ":"))) + return 0 + screen = stream.connection.request("runpane:panels:screen", [{"panelId": parsed.panel_id}]) + print(json.dumps({"ok": False, "timedOut": True, "state": screen.get("state") or {}}, separators=(",", ":"))) + return 2 + finally: + stream.close() + + +EVENT_TYPES = {"panel-created":"panel_created", "terminal-ready":"terminal_ready", "prompt-staged":"prompt_staged", "prompt-submitted":"prompt_submitted", "agent-active":"agent_active", "agent-idle":"agent_idle", "input-required":"input_required", "blocked":"blocked", "unblocked":"unblocked", "panel-exited":"panel_exited", "panel-archived":"panel_archived"} +def cursor_number(value: Any) -> int: + if isinstance(value, dict): value = value.get("cursor") + if not isinstance(value, str) or ":" not in value: raise ValueError("Malformed semantic event cursor") + return int(value.rsplit(":", 1)[1]) +def validate_event(event: Any) -> None: + if (not isinstance(event, dict) or not all(isinstance(event.get(key), str) for key in ("id", "cursor", "type", "at", "panelId")) + or event.get("id") != event.get("cursor") or event.get("type") not in set(EVENT_TYPES.values()) + or (event.get("paneId") is not None and not isinstance(event.get("paneId"), str)) + or not isinstance(event.get("state"), dict) or not isinstance(event["state"].get("initialized"), bool)): + raise ValueError("Malformed semantic event envelope") +def matches_event(parsed: Any, event: Dict[str, Any], await_mode: bool) -> bool: + if parsed.panel_id and event.get("panelId") != parsed.panel_id: return False + if not parsed.event_selector: return True + if await_mode and parsed.event_selector == "agent-idle": return event.get("type") in {"agent_idle", "input_required", "blocked", "panel_exited"} + return EVENT_TYPES.get(parsed.event_selector) == event.get("type") +def is_state_backed(selector: str) -> bool: + return selector in {"terminal-ready", "agent-active", "agent-idle", "input-required", "blocked", "unblocked", "panel-exited"} +def match_state(selector: str, state: Dict[str, Any]) -> Optional[str]: + if selector == "terminal-ready" and state.get("terminalReady"): return "terminal_ready" + if selector == "agent-active" and state.get("agentActivity") == "active": return "agent_active" + if selector == "agent-idle": + if state.get("agentActivity") == "exited": return "panel_exited" + if state.get("blocked"): return "input_required" if state.get("inputRequired") else "blocked" + if state.get("agentActivity") == "idle": return "agent_idle" + if selector == "input-required" and state.get("inputRequired"): return "input_required" + if selector == "blocked" and state.get("blocked"): return "blocked" + if selector == "unblocked" and state.get("blocked") is False: return "unblocked" + if selector == "panel-exited" and state.get("agentActivity") == "exited": return "panel_exited" + return None + + def run_agents_doctor(parsed: Any) -> int: if not parsed.agent: raise ValueError("runpane agents doctor requires --agent codex|claude.") diff --git a/packages/runpane/package.json b/packages/runpane/package.json index 8ea6b258..4e6b0018 100644 --- a/packages/runpane/package.json +++ b/packages/runpane/package.json @@ -25,6 +25,7 @@ "build": "tsc && node scripts/mark-executable.js", "lint": "eslint src --ext .ts", "typecheck": "tsc --noEmit", + "test": "vitest run", "prepack": "pnpm run build" }, "engines": { @@ -40,6 +41,7 @@ "@typescript-eslint/parser": "^8.19.0", "eslint": "^9.17.0", "typescript": "^5.7.2", - "typescript-eslint": "^8.19.0" + "typescript-eslint": "^8.19.0", + "vitest": "^2.1.9" } } diff --git a/packages/runpane/src/cli.ts b/packages/runpane/src/cli.ts index 8d5719a9..a7cd2d41 100644 --- a/packages/runpane/src/cli.ts +++ b/packages/runpane/src/cli.ts @@ -23,6 +23,9 @@ import { runPanelsSubmit, runPanelsSubmitComposer, runPanelsWait, + runPanelsEvents, + runPanelsWatch, + runPanelsAwait, runPanesArchive, runPanesCreate, runPanesList, @@ -149,6 +152,9 @@ async function dispatchParsedCommand(parsed: ParsedArgs, telemetryContext: Wrapp if (parsed.command === 'panels wait') { return runPanelsWait(parsed); } + if (parsed.command === 'panels events') return runPanelsEvents(parsed); + if (parsed.command === 'panels watch') return runPanelsWatch(parsed); + if (parsed.command === 'panels await') return runPanelsAwait(parsed); if (parsed.command === 'agents doctor') { return runAgentsDoctor(parsed); diff --git a/packages/runpane/src/commands.ts b/packages/runpane/src/commands.ts index 9a2695eb..afc9f19e 100644 --- a/packages/runpane/src/commands.ts +++ b/packages/runpane/src/commands.ts @@ -53,6 +53,10 @@ export interface ParsedArgs { pinned?: boolean; composerStrategy?: string; force?: boolean; + eventSelector?: string; + since?: string; + jsonl?: boolean; + heartbeatMs?: number; remoteSetupArgs: string[]; } @@ -138,6 +142,7 @@ export function parseRunpaneArgs(argv: string[]): ParsedArgs { } parseFlags(args, parsed); + validateEventCommandFlags(parsed); return parsed; } @@ -271,6 +276,10 @@ function parseLocalBooleanFlag(flag: string, parsed: ParsedArgs): void { parsed.force = true; return; } + if (flag === '--jsonl') { + parsed.jsonl = true; + return; + } throw new Error(`Unknown option for ${parsed.command}: ${flag}`); } @@ -408,6 +417,23 @@ function parseLocalValueFlag(flag: string, value: string, parsed: ParsedArgs): v parsed.composerStrategy = value; return; } + if (flag === '--event') { + if (!(RUNPANE_CONTRACT.enums.eventSelectors as readonly string[]).includes(value)) { + throw new Error(`Invalid --event "${value}". Expected one of: ${RUNPANE_CONTRACT.enums.eventSelectors.join(', ')}`); + } + parsed.eventSelector = value; + return; + } + if (flag === '--since') { + parsed.since = value; + return; + } + if (flag === '--heartbeat-ms') { + const heartbeatMs = Number(value); + if (!Number.isFinite(heartbeatMs) || heartbeatMs <= 0) throw new Error('--heartbeat-ms must be a positive number.'); + parsed.heartbeatMs = heartbeatMs; + return; + } throw new Error(`Unknown option for ${parsed.command}: ${flag}`); } @@ -429,9 +455,19 @@ function isRunpaneLocalCommand(command: RunpaneCommand): boolean { || command === 'panels submit' || command === 'panels submit-composer' || command === 'panels wait' + || command === 'panels events' + || command === 'panels watch' + || command === 'panels await' || command === 'agents doctor'; } +function validateEventCommandFlags(parsed: ParsedArgs): void { + if (parsed.jsonl && parsed.command !== 'panels watch') throw new Error('--jsonl is only valid with panels watch.'); + if (parsed.command === 'panels await' && (!parsed.panelId || !parsed.eventSelector)) { + throw new Error('panels await requires --panel and --event.'); + } +} + function appendRemoteArg(parsed: ParsedArgs, flag: string, value?: string): void { if (parsed.command === 'install' && parsed.target === 'daemon') { parsed.remoteSetupArgs.push(flag); diff --git a/packages/runpane/src/daemonClient.ts b/packages/runpane/src/daemonClient.ts index 1a2e9bd2..38151f9f 100644 --- a/packages/runpane/src/daemonClient.ts +++ b/packages/runpane/src/daemonClient.ts @@ -50,6 +50,13 @@ interface InvokeOptions { timeoutMs?: number; } +export interface RetainedDaemonConnection { + request<T = unknown>(channel: string, args?: unknown[], timeoutMs?: number): Promise<T>; + onSemanticEvent(listener: (payload: unknown) => void): () => void; + onError(listener: (error: Error) => void): () => void; + close(): void; +} + const FRAME_DELIMITER = '\n'; const UNIX_SOCKET_BASE_DIRECTORY = '/tmp'; const DAEMON_SOCKET_FILENAME = 'daemon.sock'; @@ -163,6 +170,108 @@ export async function invokeDaemon<T = unknown>( }); } +export async function openRetainedDaemonConnection(options: InvokeOptions = {}): Promise<RetainedDaemonConnection> { + const endpoint = getPaneDaemonEndpoint(resolvePaneDirectory(options.paneDir)); + const socket = net.createConnection(endpoint.path); + const decoder = new PaneDaemonFrameDecoder(); + const semanticListeners = new Set<(payload: unknown) => void>(); + const errorListeners = new Set<(error: Error) => void>(); + const pending = new Map<number, { + resolve(value: unknown): void; + reject(error: Error): void; + timer: ReturnType<typeof setTimeout>; + }>(); + let nextRequestId = 1; + let closed = false; + + const fail = (error: Error) => { + if (closed) return; + for (const entry of pending.values()) { + clearTimeout(entry.timer); + entry.reject(error); + } + pending.clear(); + for (const listener of errorListeners) listener(error); + }; + + socket.on('data', (chunk) => { + try { + for (const frame of decoder.push(chunk)) { + if (frame.type === 'event' && frame.channel === 'panel:semanticEvent') { + for (const listener of semanticListeners) listener(frame.args[0]); + continue; + } + if (frame.type !== 'response') continue; + const entry = pending.get(frame.id); + if (!entry) continue; + pending.delete(frame.id); + clearTimeout(entry.timer); + if (frame.ok) entry.resolve(frame.result); + else entry.reject(new PaneDaemonClientError(frame.error.message, frame.error.code)); + } + } catch (error) { + fail(error instanceof Error ? error : new Error(String(error))); + } + }); + socket.once('error', (error: NodeJS.ErrnoException) => { + fail(new PaneDaemonClientError( + `Could not connect to Pane daemon at ${endpoint.path}: ${error.message}`, + error.code ?? 'ERR_RUNPANE_DAEMON_CONNECT_FAILED', + )); + }); + socket.once('close', () => { + if (!closed) fail(new PaneDaemonClientError( + `Pane daemon closed the retained connection at ${endpoint.path}`, + 'ERR_RUNPANE_DAEMON_CLOSED', + )); + }); + + await new Promise<void>((resolve, reject) => { + socket.once('connect', resolve); + socket.once('error', reject); + }); + + return { + request<T>(channel: string, args: unknown[] = [], timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS): Promise<T> { + if (closed) return Promise.reject(new PaneDaemonClientError('Pane daemon connection is closed', 'ERR_RUNPANE_DAEMON_CLOSED')); + const id = nextRequestId++; + return new Promise<T>((resolve, reject) => { + const timer = setTimeout(() => { + pending.delete(id); + reject(new PaneDaemonClientError(`Timed out waiting for Pane daemon response on ${endpoint.path}`, 'ERR_RUNPANE_DAEMON_TIMEOUT')); + }, timeoutMs); + pending.set(id, { + resolve: (value) => resolve(value as T), + reject, + timer, + }); + socket.write(encodePaneDaemonFrame({ type: 'request', id, channel, args })); + }); + }, + onSemanticEvent(listener) { + semanticListeners.add(listener); + return () => semanticListeners.delete(listener); + }, + onError(listener) { + errorListeners.add(listener); + return () => errorListeners.delete(listener); + }, + close() { + if (closed) return; + closed = true; + for (const entry of pending.values()) { + clearTimeout(entry.timer); + entry.reject(new PaneDaemonClientError('Pane daemon connection closed', 'ERR_RUNPANE_DAEMON_CLOSED')); + } + pending.clear(); + semanticListeners.clear(); + errorListeners.clear(); + socket.removeAllListeners(); + if (!socket.destroyed) socket.destroy(); + }, + }; +} + function resolveAppDirectory(appDirectory: string, platform: NodeJS.Platform): string { if (platform === 'win32') { return path.win32.resolve(appDirectory); diff --git a/packages/runpane/src/generated/contract.ts b/packages/runpane/src/generated/contract.ts index 1236b483..e1fcb0df 100644 --- a/packages/runpane/src/generated/contract.ts +++ b/packages/runpane/src/generated/contract.ts @@ -48,8 +48,34 @@ export const RUNPANE_CONTRACT = { "agents": [ "codex", "claude" + ], + "eventSelectors": [ + "panel-created", + "terminal-ready", + "prompt-staged", + "prompt-submitted", + "agent-active", + "agent-idle", + "input-required", + "blocked", + "unblocked", + "panel-exited", + "panel-archived" ] }, + "eventSelectorMap": { + "panel-created": "panel_created", + "terminal-ready": "terminal_ready", + "prompt-staged": "prompt_staged", + "prompt-submitted": "prompt_submitted", + "agent-active": "agent_active", + "agent-idle": "agent_idle", + "input-required": "input_required", + "blocked": "blocked", + "unblocked": "unblocked", + "panel-exited": "panel_exited", + "panel-archived": "panel_archived" + }, "agentTemplates": { "codex": { "title": "Codex", @@ -308,6 +334,55 @@ export const RUNPANE_CONTRACT = { "jsonSchemas": [ "panelWaitResult" ] + }, + { + "name": "panels events", + "summary": "Replay retained semantic panel events after a cursor.", + "usage": [ + "runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]" + ], + "exitCodes": { + "0": "success", + "1": "transport or other error", + "3": "cursor expired" + }, + "jsonSchemas": [ + "panelEventsResult" + ] + }, + { + "name": "panels watch", + "summary": "Stream semantic panel events as compact JSONL.", + "usage": [ + "runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]" + ], + "outputMode": "jsonl", + "exitCodes": { + "0": "clean stop", + "1": "transport or other error", + "3": "cursor expired" + }, + "jsonSchemas": [ + "semanticEvent", + "cursorExpiredError" + ] + }, + { + "name": "panels await", + "summary": "Wait for the first matching semantic panel event.", + "usage": [ + "runpane panels await --panel <panel-id> --event <selector> [--since <cursor>] [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]" + ], + "exitCodes": { + "0": "matched", + "1": "transport or other error", + "2": "timed out", + "3": "cursor expired" + }, + "jsonSchemas": [ + "panelAwaitResult", + "cursorExpiredError" + ] } ], "flags": { @@ -508,6 +583,21 @@ export const RUNPANE_CONTRACT = { "value": "<milliseconds>", "description": "Polling interval for panels wait." }, + { + "name": "--event", + "value": "<selector>", + "description": "Semantic event selector in kebab-case." + }, + { + "name": "--since", + "value": "<cursor>", + "description": "Replay events strictly after this cursor." + }, + { + "name": "--heartbeat-ms", + "value": "<milliseconds>", + "description": "Periodic event-log and state reconciliation interval." + }, { "name": "--text", "value": "<text>", @@ -553,6 +643,10 @@ export const RUNPANE_CONTRACT = { { "name": "--force", "description": "Archive even if the pane's branch has uncommitted, untracked, or unpushed changes." + }, + { + "name": "--jsonl", + "description": "Print one compact JSON object per line (panels watch always uses JSONL)." } ] }, @@ -581,6 +675,9 @@ export const RUNPANE_CONTRACT = { " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", " runpane help [command]", "", "Quick start:", @@ -821,6 +918,9 @@ export const RUNPANE_CONTRACT = { " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", "", "Run \"runpane help panels create\" or another command-specific topic for options." ], @@ -931,6 +1031,30 @@ export const RUNPANE_CONTRACT = { " --pane-dir <path> Connect to a specific Pane data directory", " --json Print machine-readable output" ], + "panels events": [ + "Usage:", + " runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", + "", + "Replays retained semantic events strictly after a cursor.", + "", + "Exit codes: 0 success, 3 cursor expired, 1 transport/error." + ], + "panels watch": [ + "Usage:", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", + "", + "Streams one compact semantic event JSON object per stdout line.", + "", + "Exit codes: 0 clean stop, 3 cursor expired, 1 transport/error." + ], + "panels await": [ + "Usage:", + " runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]", + "", + "Waits for a matching semantic event and periodically reconciles state.", + "", + "Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport/error." + ], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -990,6 +1114,9 @@ export const RUNPANE_CONTRACT = { " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", " runpane help [command]", "", "Quick start:", @@ -1219,6 +1346,9 @@ export const RUNPANE_CONTRACT = { " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", "", "Run \"runpane help panels create\" or another command-specific topic for options." ], @@ -1329,6 +1459,24 @@ export const RUNPANE_CONTRACT = { " --pane-dir <path> Connect to a specific Pane data directory", " --json Print machine-readable output" ], + "panels events": [ + "Usage:", + " python -m runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", + "", + "Replays retained semantic events strictly after a cursor." + ], + "panels watch": [ + "Usage:", + " python -m runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", + "", + "Streams one compact semantic event JSON object per stdout line." + ], + "panels await": [ + "Usage:", + " python -m runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]", + "", + "Waits for a matching semantic event and periodically reconciles state." + ], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -1765,6 +1913,43 @@ export const RUNPANE_CONTRACT = { "auto", "--yes", "--json" + ], + [ + "panels", + "events", + "--panel", + "panel-1", + "--event", + "agent-idle", + "--since", + "epoch:1", + "--json" + ], + [ + "panels", + "watch", + "--panel", + "panel-1", + "--event", + "blocked", + "--since", + "epoch:2", + "--heartbeat-ms", + "1000", + "--jsonl" + ], + [ + "panels", + "await", + "--panel", + "panel-1", + "--event", + "agent-idle", + "--timeout-ms", + "5000", + "--heartbeat-ms", + "500", + "--json" ] ], "topLevelHelpIncludes": [ @@ -1788,6 +1973,9 @@ export const RUNPANE_CONTRACT = { "runpane panels submit", "runpane panels submit-composer", "runpane panels wait", + "runpane panels events", + "runpane panels watch", + "runpane panels await", "runpane doctor --json", "runpane agent-context --json", "Agent discovery:", @@ -3384,6 +3572,172 @@ export const RUNPANE_CONTRACT = { }, "additionalProperties": false }, + "semanticEvent": { + "type": "object", + "required": [ + "id", + "cursor", + "type", + "at", + "panelId", + "state" + ], + "properties": { + "id": { + "type": "string" + }, + "cursor": { + "type": "string" + }, + "type": { + "enum": [ + "panel_created", + "terminal_ready", + "prompt_staged", + "prompt_submitted", + "agent_active", + "agent_idle", + "input_required", + "blocked", + "unblocked", + "panel_exited", + "panel_archived" + ] + }, + "at": { + "type": "string" + }, + "paneId": { + "type": "string" + }, + "panelId": { + "type": "string" + }, + "state": { + "$ref": "#/jsonSchemas/panelWaitResult/properties/state" + }, + "resolvedBy": { + "enum": [ + "event", + "reconciliation" + ] + } + }, + "additionalProperties": false + }, + "cursorExpiredError": { + "type": "object", + "required": [ + "code", + "earliestCursor", + "reconcileCommand" + ], + "properties": { + "code": { + "enum": [ + "cursor_expired" + ] + }, + "earliestCursor": { + "type": "string" + }, + "reconcileCommand": { + "type": "string" + } + }, + "additionalProperties": false + }, + "panelEventsResult": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "events", + "cursor" + ], + "properties": { + "ok": { + "enum": [ + true + ] + }, + "events": { + "type": "array", + "items": { + "$ref": "#/jsonSchemas/semanticEvent" + } + }, + "cursor": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ok", + "error" + ], + "properties": { + "ok": { + "enum": [ + false + ] + }, + "error": { + "$ref": "#/jsonSchemas/cursorExpiredError" + } + }, + "additionalProperties": false + } + ] + }, + "panelAwaitResult": { + "type": "object", + "required": [ + "ok", + "timedOut", + "state" + ], + "properties": { + "ok": { + "type": "boolean" + }, + "timedOut": { + "type": "boolean" + }, + "matchedEvent": { + "enum": [ + "panel_created", + "terminal_ready", + "prompt_staged", + "prompt_submitted", + "agent_active", + "agent_idle", + "input_required", + "blocked", + "unblocked", + "panel_exited", + "panel_archived" + ] + }, + "resolvedBy": { + "enum": [ + "event", + "reconciliation" + ] + }, + "event": { + "$ref": "#/jsonSchemas/semanticEvent" + }, + "state": { + "$ref": "#/jsonSchemas/panelWaitResult/properties/state" + } + }, + "additionalProperties": false + }, "panelWaitResult": { "type": "object", "required": [ @@ -4128,6 +4482,39 @@ export const RUNPANE_CONTRACT = { "--json", "--pane-dir <path>" ] + }, + { + "name": "panels events", + "summary": "Replay semantic panel events after a cursor.", + "arguments": [ + "--panel <panel-id>", + "--event <selector>", + "--since <cursor>", + "--json" + ] + }, + { + "name": "panels watch", + "summary": "Stream semantic panel events as JSONL.", + "arguments": [ + "--panel <panel-id>", + "--event <selector>", + "--since <cursor>", + "--heartbeat-ms <ms>", + "--jsonl" + ] + }, + { + "name": "panels await", + "summary": "Wait for a matching semantic panel event.", + "arguments": [ + "--panel <panel-id>", + "--event <selector>", + "--since <cursor>", + "--timeout-ms <ms>", + "--heartbeat-ms <ms>", + "--json" + ] } ] }, @@ -5063,6 +5450,149 @@ export const RUNPANE_CONTRACT = { "The default timeout and screen are intentionally small for agent context safety." ] }, + "panels events": { + "name": "panels events", + "summary": "Replay retained semantic panel events after a cursor.", + "details": "Request/response pull surface for the bounded semantic event ring.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--panel", + "value": "<panel-id>", + "required": false, + "description": "Optional panel filter." + }, + { + "name": "--event", + "value": "<selector>", + "required": false, + "description": "Optional semantic event selector." + }, + { + "name": "--since", + "value": "<cursor>", + "required": false, + "description": "Exclusive replay cursor." + }, + { + "name": "--json", + "required": false, + "description": "Print machine-readable output." + } + ], + "examples": [ + "runpane panels events --since <cursor> --json" + ], + "jsonSchemas": [ + "panelEventsResult" + ], + "notes": [ + "Exit code 3 means cursor_expired; reconcile from the command in the error." + ] + }, + "panels watch": { + "name": "panels watch", + "summary": "Stream semantic panel events as JSONL.", + "details": "Uses replay then live delivery on one retained daemon connection with periodic reconciliation.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--panel", + "value": "<panel-id>", + "required": false, + "description": "Optional panel filter." + }, + { + "name": "--event", + "value": "<selector>", + "required": false, + "description": "Optional semantic event selector." + }, + { + "name": "--since", + "value": "<cursor>", + "required": false, + "description": "Exclusive replay cursor." + }, + { + "name": "--heartbeat-ms", + "value": "<ms>", + "required": false, + "description": "Reconciliation interval." + }, + { + "name": "--jsonl", + "required": false, + "description": "Describes the always-JSONL output." + } + ], + "examples": [ + "runpane panels watch --panel <panel-id> --jsonl" + ], + "jsonSchemas": [ + "semanticEvent", + "cursorExpiredError" + ], + "notes": [ + "Stdout contains compact JSON records only. Exit codes: 0 clean stop, 1 transport error, 3 cursor expired." + ] + }, + "panels await": { + "name": "panels await", + "summary": "Wait for the first matching semantic event.", + "details": "Exit-on-first-match sugar over watch with periodic state reconciliation.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--panel", + "value": "<panel-id>", + "required": true, + "description": "Panel to await." + }, + { + "name": "--event", + "value": "<selector>", + "required": true, + "description": "Semantic event selector." + }, + { + "name": "--since", + "value": "<cursor>", + "required": false, + "description": "Exclusive replay cursor." + }, + { + "name": "--timeout-ms", + "value": "<ms>", + "required": false, + "description": "Wait timeout." + }, + { + "name": "--heartbeat-ms", + "value": "<ms>", + "required": false, + "description": "Reconciliation interval." + }, + { + "name": "--json", + "required": false, + "description": "Print machine-readable output." + } + ], + "examples": [ + "runpane panels await --panel <panel-id> --event agent-idle --json" + ], + "jsonSchemas": [ + "panelAwaitResult", + "cursorExpiredError" + ], + "notes": [ + "Exit codes: 0 matched, 1 transport error, 2 timed out, 3 cursor expired." + ] + }, "panels create": { "name": "panels create", "summary": "Create a reviewer/helper terminal tab inside an existing Pane.", diff --git a/packages/runpane/src/localControl.ts b/packages/runpane/src/localControl.ts index e461544f..987f30ff 100644 --- a/packages/runpane/src/localControl.ts +++ b/packages/runpane/src/localControl.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import { stdin as input, stdout as output } from 'node:process'; import { createInterface } from 'node:readline/promises'; -import { invokeDaemon } from './daemonClient'; +import { invokeDaemon, openRetainedDaemonConnection, type RetainedDaemonConnection } from './daemonClient'; import { RUNPANE_CONTRACT } from './generated/contract'; import type { ParsedArgs, RunpaneAgent } from './commands'; @@ -260,8 +260,49 @@ interface PanelStateSummary { isCliPanel?: boolean; agentType?: RunpaneAgent; lastActivity?: string; + terminalReady?: boolean; + agentActivity?: 'unknown' | 'starting' | 'active' | 'idle' | 'exited'; + inputRequired?: boolean; + blocked?: boolean; + hasNewOutput?: boolean; + outputGeneration?: number; + lastMeaningfulEventAt?: string; } +type SemanticEventType = 'panel_created' | 'terminal_ready' | 'prompt_staged' | 'prompt_submitted' + | 'agent_active' | 'agent_idle' | 'input_required' | 'blocked' | 'unblocked' + | 'panel_exited' | 'panel_archived'; + +interface SemanticEvent { + id: string; + cursor: string; + type: SemanticEventType; + at: string; + paneId?: string; + panelId: string; + state: PanelStateSummary; +} + +interface EventsResult { + ok: true; + events: SemanticEvent[]; + cursor: string; +} + +interface EventsErrorResult { + ok: false; + error: { code: 'cursor_expired'; earliestCursor: string; reconcileCommand: string }; +} + +type EventsResponse = EventsResult | EventsErrorResult; + +const EVENT_SELECTOR_TYPES: Record<string, SemanticEventType> = { + 'panel-created': 'panel_created', 'terminal-ready': 'terminal_ready', 'prompt-staged': 'prompt_staged', + 'prompt-submitted': 'prompt_submitted', 'agent-active': 'agent_active', 'agent-idle': 'agent_idle', + 'input-required': 'input_required', blocked: 'blocked', unblocked: 'unblocked', + 'panel-exited': 'panel_exited', 'panel-archived': 'panel_archived', +}; + interface PanelBlockedState { kind: 'codex-update' | 'agent-prompt' | 'submission_unverified' | 'unknown'; message: string; @@ -646,6 +687,253 @@ export async function runPanelsWait(parsed: ParsedArgs): Promise<number> { return result.ok ? 0 : 1; } +export async function runPanelsEvents(parsed: ParsedArgs): Promise<number> { + const result = await invokeDaemon<EventsResponse>('runpane:panels:events', [{ + panelId: parsed.panelId, + event: parsed.eventSelector, + since: parsed.since, + }], { paneDir: parsed.paneDir }); + if (!result.ok) { + console.error(JSON.stringify(result)); + return 3; + } + if (parsed.json) printJson(result); + else for (const event of result.events) console.log(`${event.cursor}\t${event.type}\t${event.panelId}`); + return 0; +} + +export async function runPanelsWatch(parsed: ParsedArgs): Promise<number> { + const stream = await SemanticEventStream.open(parsed); + let transportFailed = false; + let terminalErrorCode = 1; + let stopped = false; + let heartbeatInFlight = false; + const heartbeatRef: { current?: ReturnType<typeof setInterval> } = {}; + const finish = (code: number): number => { + stopped = true; + if (heartbeatRef.current) clearInterval(heartbeatRef.current); + process.off('SIGINT', stopCleanly); + process.off('SIGTERM', stopCleanly); + stream.close(); + return code; + }; + const stopCleanly = () => { stopped = true; }; + process.once('SIGINT', stopCleanly); + process.once('SIGTERM', stopCleanly); + stream.onTransportError(() => { transportFailed = true; }); + stream.onEvent((event, resolvedBy) => { + if (matchesParsedEvent(parsed, event, false)) { + process.stdout.write(`${JSON.stringify({ ...event, resolvedBy })}\n`); + stream.noteEmitted(event.cursor); + } + }); + try { + await stream.replay('event'); + } catch (error) { + return finish(handleStreamError(error)); + } + heartbeatRef.current = setInterval(() => { + if (heartbeatInFlight || stopped) return; + heartbeatInFlight = true; + void stream.replay('reconciliation').catch((error: unknown) => { + transportFailed = true; + if (error instanceof CursorExpiredError) { + terminalErrorCode = 3; + console.error(JSON.stringify(error.payload)); + } + }).finally(() => { heartbeatInFlight = false; }); + }, parsed.heartbeatMs ?? 30_000); + while (!stopped && !transportFailed) await delay(20); + return finish(transportFailed ? terminalErrorCode : 0); +} + +export async function runPanelsAwait(parsed: ParsedArgs): Promise<number> { + if (!parsed.panelId || !parsed.eventSelector) throw new Error('runpane panels await requires --panel and --event.'); + const stream = await SemanticEventStream.open(parsed); + let matched: { event: SemanticEvent; resolvedBy: 'event' | 'reconciliation' } | undefined; + let transportError: Error | undefined; + stream.onTransportError((error) => { transportError = error; }); + stream.onEvent((event, resolvedBy) => { + if (!matched && matchesParsedEvent(parsed, event, true)) matched = { event, resolvedBy }; + }); + try { + await stream.replay('event'); + } catch (error) { + stream.close(); + return handleStreamError(error); + } + const timeoutAt = Date.now() + (parsed.timeoutMs ?? 30_000); + const heartbeatMs = parsed.heartbeatMs ?? 30_000; + let nextHeartbeat = Date.now() + heartbeatMs; + while (!matched && !transportError && Date.now() < timeoutAt) { + const now = Date.now(); + if (now >= nextHeartbeat) { + try { + await stream.replay('reconciliation'); + if (!matched && isStateBackedSelector(parsed.eventSelector)) { + const screen = await stream.request<PanelScreenResult>('runpane:panels:screen', [{ panelId: parsed.panelId }]); + const reconciledType = matchState(parsed.eventSelector, screen.state); + if (reconciledType) { + matched = { + resolvedBy: 'reconciliation', + event: syntheticEvent(parsed.panelId, screen.paneId, reconciledType, screen.state, stream.processedCursor), + }; + } + } + } catch (error) { + stream.close(); + return handleStreamError(error); + } + nextHeartbeat += heartbeatMs; + } + if (!matched && !transportError) await delay(Math.min(20, Math.max(timeoutAt - Date.now(), 1))); + } + if (transportError) { + stream.close(); + console.error(transportError.message); + return 1; + } + if (matched) { + const result = { + ok: true, + timedOut: false, + matchedEvent: matched.event.type, + resolvedBy: matched.resolvedBy, + event: matched.event, + state: matched.event.state, + }; + stream.close(); + console.log(JSON.stringify(result)); + return 0; + } + try { + const screen = await stream.request<PanelScreenResult>('runpane:panels:screen', [{ panelId: parsed.panelId }]); + stream.close(); + console.log(JSON.stringify({ ok: false, timedOut: true, state: screen.state })); + return 2; + } catch (error) { + stream.close(); + console.error(error instanceof Error ? error.message : String(error)); + return 1; + } +} + +class CursorExpiredError extends Error { + constructor(readonly payload: EventsErrorResult) { + super('RunPane event cursor expired'); + } +} + +class SemanticEventStream { + private buffered: SemanticEvent[] = []; + private buffering = true; + private eventListener?: (event: SemanticEvent, resolvedBy: 'event' | 'reconciliation') => void; + private transportListener?: (error: Error) => void; + processedCursor = ''; + emittedCursor = ''; + + private constructor(private readonly connection: RetainedDaemonConnection, private readonly initialSince?: string) { + connection.onSemanticEvent((payload) => { + const event = parseSemanticEvent(payload); + if (this.buffering) this.buffered.push(event); + else this.deliver(event, 'event'); + }); + connection.onError((error) => this.transportListener?.(error)); + } + + static async open(parsed: ParsedArgs): Promise<SemanticEventStream> { + const connection = await openRetainedDaemonConnection({ paneDir: parsed.paneDir }); + return new SemanticEventStream(connection, parsed.since); + } + + onEvent(listener: (event: SemanticEvent, resolvedBy: 'event' | 'reconciliation') => void): void { this.eventListener = listener; } + onTransportError(listener: (error: Error) => void): void { this.transportListener = listener; } + noteEmitted(cursor: string): void { this.emittedCursor = cursor; } + request<T>(channel: string, args: unknown[]): Promise<T> { return this.connection.request<T>(channel, args); } + + async replay(resolvedBy: 'event' | 'reconciliation'): Promise<void> { + this.buffering = true; + const since = this.processedCursor || this.initialSince; + const response = await this.connection.request<EventsResponse>('runpane:panels:events', [{ since }]); + if (!response.ok) throw new CursorExpiredError(response); + for (const event of [...response.events].sort(compareEvents)) this.deliver(event, resolvedBy); + const buffered = this.buffered.splice(0).sort(compareEvents); + for (const event of buffered) this.deliver(event, resolvedBy === 'reconciliation' ? 'reconciliation' : 'event'); + if (!this.processedCursor || compareCursors(this.processedCursor, response.cursor) < 0) this.processedCursor = response.cursor; + this.buffering = false; + } + + close(): void { this.connection.close(); } + + private deliver(event: SemanticEvent, resolvedBy: 'event' | 'reconciliation'): void { + if (this.processedCursor && compareCursors(event.cursor, this.processedCursor) <= 0) return; + this.processedCursor = event.cursor; + this.eventListener?.(event, resolvedBy); + } +} + +function parseSemanticEvent(value: unknown): SemanticEvent { + if (!isObject(value)) throw new Error('Malformed semantic event: expected an object'); + const state = value.state; + if (typeof value.id !== 'string' || typeof value.cursor !== 'string' || typeof value.type !== 'string' + || !Object.values(EVENT_SELECTOR_TYPES).includes(value.type as SemanticEventType) + || value.id !== value.cursor || typeof value.at !== 'string' || typeof value.panelId !== 'string' + || (value.paneId !== undefined && typeof value.paneId !== 'string') + || !isObject(state) || typeof state.initialized !== 'boolean') { + throw new Error('Malformed semantic event envelope'); + } + return value as unknown as SemanticEvent; +} + +function isObject(value: unknown): value is Record<string, unknown> { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function compareEvents(left: SemanticEvent, right: SemanticEvent): number { return compareCursors(left.cursor, right.cursor); } +function compareCursors(left: string, right: string): number { + const l = splitCursor(left); const r = splitCursor(right); + if (l.epoch !== r.epoch) throw new Error('Cannot compare semantic event cursors from different epochs'); + return l.n - r.n; +} +function splitCursor(cursor: string): { epoch: string; n: number } { + const index = cursor.lastIndexOf(':'); + const n = Number(cursor.slice(index + 1)); + if (index <= 0 || !Number.isSafeInteger(n) || n < 0) throw new Error(`Malformed semantic event cursor: ${cursor}`); + return { epoch: cursor.slice(0, index), n }; +} +function matchesParsedEvent(parsed: ParsedArgs, event: SemanticEvent, awaitMode: boolean): boolean { + if (parsed.panelId && event.panelId !== parsed.panelId) return false; + if (!parsed.eventSelector) return true; + if (awaitMode && parsed.eventSelector === 'agent-idle') return ['agent_idle', 'input_required', 'blocked', 'panel_exited'].includes(event.type); + return EVENT_SELECTOR_TYPES[parsed.eventSelector] === event.type; +} +function isStateBackedSelector(selector: string): boolean { + return ['terminal-ready', 'agent-active', 'agent-idle', 'input-required', 'blocked', 'unblocked', 'panel-exited'].includes(selector); +} +function matchState(selector: string, state: PanelStateSummary): SemanticEventType | undefined { + if (selector === 'terminal-ready' && state.terminalReady) return 'terminal_ready'; + if (selector === 'agent-active' && state.agentActivity === 'active') return 'agent_active'; + if (selector === 'agent-idle') { + if (state.agentActivity === 'exited') return 'panel_exited'; + if (state.blocked) return state.inputRequired ? 'input_required' : 'blocked'; + if (state.agentActivity === 'idle') return 'agent_idle'; + } + if (selector === 'input-required' && state.inputRequired) return 'input_required'; + if (selector === 'blocked' && state.blocked) return 'blocked'; + if (selector === 'unblocked' && state.blocked === false) return 'unblocked'; + if (selector === 'panel-exited' && state.agentActivity === 'exited') return 'panel_exited'; + return undefined; +} +function syntheticEvent(panelId: string, paneId: string | undefined, type: SemanticEventType, state: PanelStateSummary, cursor: string): SemanticEvent { + return { id: cursor, cursor, type, at: new Date().toISOString(), paneId, panelId, state }; +} +function handleStreamError(error: unknown): number { + if (error instanceof CursorExpiredError) { console.error(JSON.stringify(error.payload)); return 3; } + console.error(error instanceof Error ? error.message : String(error)); + return 1; +} +function delay(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } + export async function runAgentsDoctor(parsed: ParsedArgs): Promise<number> { if (!parsed.agent) { throw new Error('runpane agents doctor requires --agent codex|claude.'); diff --git a/packages/runpane/src/localControl.watch.test.ts b/packages/runpane/src/localControl.watch.test.ts new file mode 100644 index 00000000..33be7116 --- /dev/null +++ b/packages/runpane/src/localControl.watch.test.ts @@ -0,0 +1,233 @@ +import fs from 'node:fs'; +import net from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { getPaneDaemonEndpoint } from './daemonClient'; +import { runPanelsAwait, runPanelsEvents, runPanelsWatch } from './localControl'; +import type { ParsedArgs } from './commands'; + +type EventType = 'panel_created' | 'terminal_ready' | 'prompt_staged' | 'prompt_submitted' | 'agent_active' + | 'agent_idle' | 'input_required' | 'blocked' | 'unblocked' | 'panel_exited' | 'panel_archived'; + +class FakeDaemon { + readonly paneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'runpane-watch-test-')); + readonly endpoint = getPaneDaemonEndpoint(this.paneDir).path; + readonly server = net.createServer(); + readonly sockets = new Set<net.Socket>(); + events: Array<ReturnType<typeof semanticEvent>> = []; + state = panelState('active'); + expire = false; + suppressLive = false; + eventRequests = 0; + duringReplay?: ReturnType<typeof semanticEvent>; + + async start(): Promise<void> { + fs.mkdirSync(path.dirname(this.endpoint), { recursive: true }); + this.server.on('connection', socket => { + this.sockets.add(socket); + let buffer = ''; + socket.on('data', chunk => { + buffer += chunk.toString('utf8'); + let newline = buffer.indexOf('\n'); + while (newline >= 0) { + const request = JSON.parse(buffer.slice(0, newline)) as { id: number; channel: string; args: Array<Record<string, unknown>> }; + buffer = buffer.slice(newline + 1); + this.respond(socket, request); + newline = buffer.indexOf('\n'); + } + }); + socket.on('close', () => this.sockets.delete(socket)); + }); + await new Promise<void>((resolve, reject) => { + this.server.once('error', reject); + this.server.listen(this.endpoint, resolve); + }); + } + + close(): void { + for (const socket of this.sockets) socket.destroy(); + this.server.close(); + fs.rmSync(this.paneDir, { recursive: true, force: true }); + fs.rmSync(path.dirname(this.endpoint), { recursive: true, force: true }); + } + + append(type: EventType, state = this.state, live = true): ReturnType<typeof semanticEvent> { + const event = semanticEvent(this.events.length + 1, type, state); + this.events.push(event); + if (live && !this.suppressLive) this.broadcast(event); + return event; + } + + broadcast(event: ReturnType<typeof semanticEvent>): void { + const frame = `${JSON.stringify({ type: 'event', channel: 'panel:semanticEvent', args: [event] })}\n`; + for (const socket of this.sockets) socket.write(frame); + } + + private respond(socket: net.Socket, request: { id: number; channel: string; args: Array<Record<string, unknown>> }): void { + if (request.channel === 'runpane:panels:screen') { + this.writeResponse(socket, request.id, { ok: true, panelId: 'panel-1', paneId: 'pane-1', source: 'empty', limit: 80, returnedLineCount: 0, hasMore: false, text: '', state: this.state }); + return; + } + if (request.channel !== 'runpane:panels:events') throw new Error(`Unexpected channel ${request.channel}`); + this.eventRequests += 1; + if (this.expire) { + this.writeResponse(socket, request.id, { ok: false, error: { code: 'cursor_expired', earliestCursor: 'epoch:5', reconcileCommand: 'runpane panes status --json' } }); + return; + } + const since = request.args[0]?.since; + const n = typeof since === 'string' ? Number(since.split(':')[1]) : this.events.length; + const replay = this.events.filter(event => Number(event.cursor.split(':')[1]) > n); + if (this.duringReplay) { + const interleaved = this.duringReplay; + this.duringReplay = undefined; + this.events.push(interleaved); + this.broadcast(interleaved); + } + this.writeResponse(socket, request.id, { ok: true, events: replay, cursor: `epoch:${this.events.length}` }); + } + + private writeResponse(socket: net.Socket, id: number, result: unknown): void { + socket.write(`${JSON.stringify({ type: 'response', id, ok: true, result })}\n`); + } +} + +function semanticEvent(n: number, type: EventType, state = panelState('active')) { + return { id: `epoch:${n}`, cursor: `epoch:${n}`, type, at: new Date().toISOString(), paneId: 'pane-1', panelId: 'panel-1', state }; +} +function panelState(activity: 'active' | 'idle' | 'exited') { + return { initialized: true, terminalReady: true, agentActivity: activity, blocked: false, inputRequired: false }; +} +function parsed(command: ParsedArgs['command'], paneDir: string, extra: Partial<ParsedArgs> = {}): ParsedArgs { + return { command, target: 'client', paneVersion: 'latest', channel: 'stable', format: 'auto', dryRun: false, yes: false, verbose: false, json: true, remoteSetupArgs: [], paneDir, panelId: 'panel-1', ...extra }; +} + +let daemon: FakeDaemon | undefined; +afterEach(() => { daemon?.close(); daemon = undefined; vi.restoreAllMocks(); }); + +describe('semantic event wrapper', () => { + it('AC1 remains blocked while ready and active, then resolves agent-idle on an idle transition', async () => { + daemon = new FakeDaemon(); await daemon.start(); + const promise = runPanelsAwait(parsed('panels await', daemon.paneDir, { eventSelector: 'agent-idle', timeoutMs: 500 })); + let settled = false; void promise.then(() => { settled = true; }); + await new Promise(resolve => setTimeout(resolve, 40)); expect(settled).toBe(false); + daemon.state = panelState('idle'); daemon.append('agent_idle', daemon.state); + expect(await promise).toBe(0); + }); + + it('AC2/AC3 watch emits one compact complete JSONL record for one transition and none while unchanged', async () => { + daemon = new FakeDaemon(); await daemon.start(); + const writes: string[] = []; + vi.spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { writes.push(String(chunk)); return true; }); + const promise = runPanelsWatch(parsed('panels watch', daemon.paneDir, { jsonl: true })); + await new Promise(resolve => setTimeout(resolve, 40)); + daemon.append('agent_active'); + await new Promise(resolve => setTimeout(resolve, 40)); + process.emit('SIGINT'); + expect(await promise).toBe(0); + expect(writes).toHaveLength(1); + expect(writes[0].split('\n').filter(Boolean)).toHaveLength(1); + const record = JSON.parse(writes[0]) as Record<string, unknown>; + expect(record).toMatchObject({ id: 'epoch:1', cursor: 'epoch:1', type: 'agent_active', paneId: 'pane-1', panelId: 'panel-1' }); + expect(JSON.stringify(record)).not.toContain('screen'); + }); + + it('AC4 merges an event broadcast during replay exactly once in numeric cursor order', async () => { + daemon = new FakeDaemon(); daemon.events.push(semanticEvent(1, 'agent_active')); + daemon.duringReplay = semanticEvent(2, 'agent_idle', panelState('idle')); await daemon.start(); + const writes: string[] = []; + vi.spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { writes.push(String(chunk)); return true; }); + const promise = runPanelsWatch(parsed('panels watch', daemon.paneDir, { since: 'epoch:0' })); + await new Promise(resolve => setTimeout(resolve, 60)); process.emit('SIGINT'); expect(await promise).toBe(0); + expect(writes.map(line => (JSON.parse(line) as { cursor: string }).cursor)).toEqual(['epoch:1', 'epoch:2']); + }); + + it('AC5/AC6 reports structured cursor_expired on stderr with exit 3', async () => { + daemon = new FakeDaemon(); daemon.expire = true; await daemon.start(); + const error = vi.spyOn(console, 'error').mockImplementation(() => undefined); + expect(await runPanelsEvents(parsed('panels events', daemon.paneDir, { since: 'old:1' }))).toBe(3); + expect(error.mock.calls.flat().join(' ')).toContain('cursor_expired'); + expect(error.mock.calls.flat().join(' ')).toContain('reconcileCommand'); + }); + + it.each([['agent-idle', 'agent_idle'], ['prompt-staged', 'prompt_staged']] as const)( + 'AC7 heartbeat recovers suppressed %s delivery and marks reconciliation', async (selector, type) => { + daemon = new FakeDaemon(); daemon.suppressLive = true; await daemon.start(); + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + const promise = runPanelsAwait(parsed('panels await', daemon.paneDir, { eventSelector: selector, timeoutMs: 500, heartbeatMs: 40 })); + await new Promise(resolve => setTimeout(resolve, 15)); + daemon.state = selector === 'agent-idle' ? panelState('idle') : daemon.state; + daemon.append(type, daemon.state); + expect(await promise).toBe(0); + expect(log.mock.calls.flat().join(' ')).toContain('"resolvedBy":"reconciliation"'); + }, + ); + + it('AC8 timeout returns current reconciled state and distinct exit 2', async () => { + daemon = new FakeDaemon(); await daemon.start(); + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + expect(await runPanelsAwait(parsed('panels await', daemon.paneDir, { eventSelector: 'blocked', timeoutMs: 40, heartbeatMs: 1000 }))).toBe(2); + expect(log.mock.calls.flat().join(' ')).toContain('"timedOut":true'); + expect(log.mock.calls.flat().join(' ')).toContain('"agentActivity":"active"'); + }); + + it('heartbeat remains periodic while unrelated panel events keep arriving', async () => { + daemon = new FakeDaemon(); await daemon.start(); + const promise = runPanelsWatch(parsed('panels watch', daemon.paneDir, { heartbeatMs: 30 })); + await new Promise(resolve => setTimeout(resolve, 30)); + for (let n = 1; n <= 5; n += 1) { + daemon.broadcast({ ...semanticEvent(n, 'agent_active'), panelId: 'panel-other' }); + await new Promise(resolve => setTimeout(resolve, 15)); + } + process.emit('SIGINT'); expect(await promise).toBe(0); + expect(daemon.eventRequests).toBeGreaterThanOrEqual(3); + }); + + it('Python parity: replay/live handshake is ordered and clean SIGINT exits 0', async () => { + daemon = new FakeDaemon(); daemon.events.push(semanticEvent(1, 'agent_active')); + daemon.duringReplay = semanticEvent(2, 'agent_idle', panelState('idle')); await daemon.start(); + const result = await runPython(daemon.paneDir, ['panels', 'watch', '--panel', 'panel-1', '--since', 'epoch:0', '--jsonl'], 2); + expect(result.code).toBe(0); + expect(result.stdout.trim().split('\n').map(line => (JSON.parse(line) as { cursor: string }).cursor)).toEqual(['epoch:1', 'epoch:2']); + }); + + it('Python parity: heartbeat recovery, cursor expiry, timeout, and transport use exit codes 0/3/2/1', async () => { + daemon = new FakeDaemon(); daemon.suppressLive = true; await daemon.start(); + const recoveredPromise = runPython(daemon.paneDir, ['panels', 'await', '--panel', 'panel-1', '--event', 'prompt-staged', '--heartbeat-ms', '40', '--timeout-ms', '500', '--json']); + while (daemon.sockets.size === 0) await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise(resolve => setTimeout(resolve, 30)); daemon.append('prompt_staged'); + const recovered = await recoveredPromise; + expect(recovered.code).toBe(0); expect(recovered.stdout).toContain('"resolvedBy":"reconciliation"'); + + daemon.expire = true; + const expired = await runPython(daemon.paneDir, ['panels', 'events', '--since', 'old:1', '--json']); + expect(expired.code).toBe(3); expect(expired.stderr).toContain('cursor_expired'); + daemon.expire = false; + const timeout = await runPython(daemon.paneDir, ['panels', 'await', '--panel', 'panel-1', '--event', 'blocked', '--timeout-ms', '30', '--json']); + expect(timeout.code).toBe(2); expect(timeout.stdout).toContain('"timedOut":true'); + const missingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'runpane-missing-')); + const transport = await runPython(missingDir, ['panels', 'events', '--json']); + fs.rmSync(missingDir, { recursive: true, force: true }); + expect(transport.code).toBe(1); + }); +}); + +function runPython(paneDir: string, args: string[], stopAfterLines = 0): Promise<{ code: number | null; stdout: string; stderr: string }> { + return new Promise((resolve, reject) => { + const child = spawn('python3', ['-m', 'runpane', ...args], { + env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1', PYTHONPATH: path.resolve(process.cwd(), '../runpane-py/src'), PANE_DIR: paneDir }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + let stdout = ''; let stderr = ''; let stopped = false; + child.stdout.on('data', chunk => { + stdout += chunk.toString(); + if (!stopped && stopAfterLines > 0 && stdout.split('\n').filter(Boolean).length >= stopAfterLines) { + stopped = true; child.kill('SIGINT'); + } + }); + child.stderr.on('data', chunk => { stderr += chunk.toString(); }); + child.once('error', reject); + child.once('close', code => resolve({ code, stdout, stderr })); + }); +} diff --git a/packages/runpane/vitest.config.ts b/packages/runpane/vitest.config.ts new file mode 100644 index 00000000..f293f0a8 --- /dev/null +++ b/packages/runpane/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.{test,spec}.ts'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bed755d6..99f6a0de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -416,6 +416,9 @@ importers: typescript-eslint: specifier: ^8.19.0 version: 8.37.0(eslint@9.31.0(jiti@2.6.0))(typescript@5.8.3) + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.16.5) shared: {} diff --git a/scripts/fixtures/runpane-contract.json b/scripts/fixtures/runpane-contract.json index c74d66b7..ca7f8e5e 100644 --- a/scripts/fixtures/runpane-contract.json +++ b/scripts/fixtures/runpane-contract.json @@ -280,6 +280,43 @@ "auto", "--yes", "--json" + ], + [ + "panels", + "events", + "--panel", + "panel-1", + "--event", + "agent-idle", + "--since", + "epoch:1", + "--json" + ], + [ + "panels", + "watch", + "--panel", + "panel-1", + "--event", + "blocked", + "--since", + "epoch:2", + "--heartbeat-ms", + "1000", + "--jsonl" + ], + [ + "panels", + "await", + "--panel", + "panel-1", + "--event", + "agent-idle", + "--timeout-ms", + "5000", + "--heartbeat-ms", + "500", + "--json" ] ], "help": { @@ -304,6 +341,9 @@ "runpane panels submit", "runpane panels submit-composer", "runpane panels wait", + "runpane panels events", + "runpane panels watch", + "runpane panels await", "runpane doctor --json", "runpane agent-context --json", "Agent discovery:", diff --git a/scripts/generate-runpane-contract.js b/scripts/generate-runpane-contract.js index a182e1f8..c39eb787 100644 --- a/scripts/generate-runpane-contract.js +++ b/scripts/generate-runpane-contract.js @@ -156,7 +156,7 @@ function validateContract(contract, schema) { const commandNames = ensureArray(contract.commands, 'commands').map((command) => command.name); assertUnique(commandNames, 'commands'); - for (const required of ['help', 'setup', 'install', 'update', 'version', 'doctor', 'agent-context', 'agents doctor', 'repos list', 'repos add', 'panes list', 'panes create', 'panels create', 'panels list', 'panels output', 'panels input', 'panels screen', 'panels submit', 'panels submit-composer', 'panels wait']) { + for (const required of ['help', 'setup', 'install', 'update', 'version', 'doctor', 'agent-context', 'agents doctor', 'repos list', 'repos add', 'panes list', 'panes create', 'panels create', 'panels list', 'panels output', 'panels input', 'panels screen', 'panels submit', 'panels submit-composer', 'panels wait', 'panels events', 'panels watch', 'panels await']) { if (!commandNames.includes(required)) { throw new Error(`commands must include "${required}"`); } diff --git a/scripts/test-runpane-contract.js b/scripts/test-runpane-contract.js index 1811a17d..3ef5ca4e 100644 --- a/scripts/test-runpane-contract.js +++ b/scripts/test-runpane-contract.js @@ -1118,6 +1118,9 @@ function checkHelpOutput() { assertIncludes(output, 'runpane panels create'); assertIncludes(output, 'runpane panels submit-composer'); assertIncludes(output, 'runpane panels wait'); + assertIncludes(output, 'runpane panels events'); + assertIncludes(output, 'runpane panels watch'); + assertIncludes(output, 'runpane panels await'); } } diff --git a/shared/types/generatedRunpaneContract.ts b/shared/types/generatedRunpaneContract.ts index 1236b483..e1fcb0df 100644 --- a/shared/types/generatedRunpaneContract.ts +++ b/shared/types/generatedRunpaneContract.ts @@ -48,8 +48,34 @@ export const RUNPANE_CONTRACT = { "agents": [ "codex", "claude" + ], + "eventSelectors": [ + "panel-created", + "terminal-ready", + "prompt-staged", + "prompt-submitted", + "agent-active", + "agent-idle", + "input-required", + "blocked", + "unblocked", + "panel-exited", + "panel-archived" ] }, + "eventSelectorMap": { + "panel-created": "panel_created", + "terminal-ready": "terminal_ready", + "prompt-staged": "prompt_staged", + "prompt-submitted": "prompt_submitted", + "agent-active": "agent_active", + "agent-idle": "agent_idle", + "input-required": "input_required", + "blocked": "blocked", + "unblocked": "unblocked", + "panel-exited": "panel_exited", + "panel-archived": "panel_archived" + }, "agentTemplates": { "codex": { "title": "Codex", @@ -308,6 +334,55 @@ export const RUNPANE_CONTRACT = { "jsonSchemas": [ "panelWaitResult" ] + }, + { + "name": "panels events", + "summary": "Replay retained semantic panel events after a cursor.", + "usage": [ + "runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]" + ], + "exitCodes": { + "0": "success", + "1": "transport or other error", + "3": "cursor expired" + }, + "jsonSchemas": [ + "panelEventsResult" + ] + }, + { + "name": "panels watch", + "summary": "Stream semantic panel events as compact JSONL.", + "usage": [ + "runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]" + ], + "outputMode": "jsonl", + "exitCodes": { + "0": "clean stop", + "1": "transport or other error", + "3": "cursor expired" + }, + "jsonSchemas": [ + "semanticEvent", + "cursorExpiredError" + ] + }, + { + "name": "panels await", + "summary": "Wait for the first matching semantic panel event.", + "usage": [ + "runpane panels await --panel <panel-id> --event <selector> [--since <cursor>] [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]" + ], + "exitCodes": { + "0": "matched", + "1": "transport or other error", + "2": "timed out", + "3": "cursor expired" + }, + "jsonSchemas": [ + "panelAwaitResult", + "cursorExpiredError" + ] } ], "flags": { @@ -508,6 +583,21 @@ export const RUNPANE_CONTRACT = { "value": "<milliseconds>", "description": "Polling interval for panels wait." }, + { + "name": "--event", + "value": "<selector>", + "description": "Semantic event selector in kebab-case." + }, + { + "name": "--since", + "value": "<cursor>", + "description": "Replay events strictly after this cursor." + }, + { + "name": "--heartbeat-ms", + "value": "<milliseconds>", + "description": "Periodic event-log and state reconciliation interval." + }, { "name": "--text", "value": "<text>", @@ -553,6 +643,10 @@ export const RUNPANE_CONTRACT = { { "name": "--force", "description": "Archive even if the pane's branch has uncommitted, untracked, or unpushed changes." + }, + { + "name": "--jsonl", + "description": "Print one compact JSON object per line (panels watch always uses JSONL)." } ] }, @@ -581,6 +675,9 @@ export const RUNPANE_CONTRACT = { " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", " runpane help [command]", "", "Quick start:", @@ -821,6 +918,9 @@ export const RUNPANE_CONTRACT = { " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", "", "Run \"runpane help panels create\" or another command-specific topic for options." ], @@ -931,6 +1031,30 @@ export const RUNPANE_CONTRACT = { " --pane-dir <path> Connect to a specific Pane data directory", " --json Print machine-readable output" ], + "panels events": [ + "Usage:", + " runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", + "", + "Replays retained semantic events strictly after a cursor.", + "", + "Exit codes: 0 success, 3 cursor expired, 1 transport/error." + ], + "panels watch": [ + "Usage:", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", + "", + "Streams one compact semantic event JSON object per stdout line.", + "", + "Exit codes: 0 clean stop, 3 cursor expired, 1 transport/error." + ], + "panels await": [ + "Usage:", + " runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]", + "", + "Waits for a matching semantic event and periodically reconciles state.", + "", + "Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport/error." + ], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -990,6 +1114,9 @@ export const RUNPANE_CONTRACT = { " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", " runpane help [command]", "", "Quick start:", @@ -1219,6 +1346,9 @@ export const RUNPANE_CONTRACT = { " runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]", " runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]", " runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]", + " runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]", + " runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]", + " runpane panels await --panel <panel-id> --event <selector> [--json]", "", "Run \"runpane help panels create\" or another command-specific topic for options." ], @@ -1329,6 +1459,24 @@ export const RUNPANE_CONTRACT = { " --pane-dir <path> Connect to a specific Pane data directory", " --json Print machine-readable output" ], + "panels events": [ + "Usage:", + " python -m runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", + "", + "Replays retained semantic events strictly after a cursor." + ], + "panels watch": [ + "Usage:", + " python -m runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", + "", + "Streams one compact semantic event JSON object per stdout line." + ], + "panels await": [ + "Usage:", + " python -m runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]", + "", + "Waits for a matching semantic event and periodically reconciles state." + ], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -1765,6 +1913,43 @@ export const RUNPANE_CONTRACT = { "auto", "--yes", "--json" + ], + [ + "panels", + "events", + "--panel", + "panel-1", + "--event", + "agent-idle", + "--since", + "epoch:1", + "--json" + ], + [ + "panels", + "watch", + "--panel", + "panel-1", + "--event", + "blocked", + "--since", + "epoch:2", + "--heartbeat-ms", + "1000", + "--jsonl" + ], + [ + "panels", + "await", + "--panel", + "panel-1", + "--event", + "agent-idle", + "--timeout-ms", + "5000", + "--heartbeat-ms", + "500", + "--json" ] ], "topLevelHelpIncludes": [ @@ -1788,6 +1973,9 @@ export const RUNPANE_CONTRACT = { "runpane panels submit", "runpane panels submit-composer", "runpane panels wait", + "runpane panels events", + "runpane panels watch", + "runpane panels await", "runpane doctor --json", "runpane agent-context --json", "Agent discovery:", @@ -3384,6 +3572,172 @@ export const RUNPANE_CONTRACT = { }, "additionalProperties": false }, + "semanticEvent": { + "type": "object", + "required": [ + "id", + "cursor", + "type", + "at", + "panelId", + "state" + ], + "properties": { + "id": { + "type": "string" + }, + "cursor": { + "type": "string" + }, + "type": { + "enum": [ + "panel_created", + "terminal_ready", + "prompt_staged", + "prompt_submitted", + "agent_active", + "agent_idle", + "input_required", + "blocked", + "unblocked", + "panel_exited", + "panel_archived" + ] + }, + "at": { + "type": "string" + }, + "paneId": { + "type": "string" + }, + "panelId": { + "type": "string" + }, + "state": { + "$ref": "#/jsonSchemas/panelWaitResult/properties/state" + }, + "resolvedBy": { + "enum": [ + "event", + "reconciliation" + ] + } + }, + "additionalProperties": false + }, + "cursorExpiredError": { + "type": "object", + "required": [ + "code", + "earliestCursor", + "reconcileCommand" + ], + "properties": { + "code": { + "enum": [ + "cursor_expired" + ] + }, + "earliestCursor": { + "type": "string" + }, + "reconcileCommand": { + "type": "string" + } + }, + "additionalProperties": false + }, + "panelEventsResult": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "events", + "cursor" + ], + "properties": { + "ok": { + "enum": [ + true + ] + }, + "events": { + "type": "array", + "items": { + "$ref": "#/jsonSchemas/semanticEvent" + } + }, + "cursor": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ok", + "error" + ], + "properties": { + "ok": { + "enum": [ + false + ] + }, + "error": { + "$ref": "#/jsonSchemas/cursorExpiredError" + } + }, + "additionalProperties": false + } + ] + }, + "panelAwaitResult": { + "type": "object", + "required": [ + "ok", + "timedOut", + "state" + ], + "properties": { + "ok": { + "type": "boolean" + }, + "timedOut": { + "type": "boolean" + }, + "matchedEvent": { + "enum": [ + "panel_created", + "terminal_ready", + "prompt_staged", + "prompt_submitted", + "agent_active", + "agent_idle", + "input_required", + "blocked", + "unblocked", + "panel_exited", + "panel_archived" + ] + }, + "resolvedBy": { + "enum": [ + "event", + "reconciliation" + ] + }, + "event": { + "$ref": "#/jsonSchemas/semanticEvent" + }, + "state": { + "$ref": "#/jsonSchemas/panelWaitResult/properties/state" + } + }, + "additionalProperties": false + }, "panelWaitResult": { "type": "object", "required": [ @@ -4128,6 +4482,39 @@ export const RUNPANE_CONTRACT = { "--json", "--pane-dir <path>" ] + }, + { + "name": "panels events", + "summary": "Replay semantic panel events after a cursor.", + "arguments": [ + "--panel <panel-id>", + "--event <selector>", + "--since <cursor>", + "--json" + ] + }, + { + "name": "panels watch", + "summary": "Stream semantic panel events as JSONL.", + "arguments": [ + "--panel <panel-id>", + "--event <selector>", + "--since <cursor>", + "--heartbeat-ms <ms>", + "--jsonl" + ] + }, + { + "name": "panels await", + "summary": "Wait for a matching semantic panel event.", + "arguments": [ + "--panel <panel-id>", + "--event <selector>", + "--since <cursor>", + "--timeout-ms <ms>", + "--heartbeat-ms <ms>", + "--json" + ] } ] }, @@ -5063,6 +5450,149 @@ export const RUNPANE_CONTRACT = { "The default timeout and screen are intentionally small for agent context safety." ] }, + "panels events": { + "name": "panels events", + "summary": "Replay retained semantic panel events after a cursor.", + "details": "Request/response pull surface for the bounded semantic event ring.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--panel", + "value": "<panel-id>", + "required": false, + "description": "Optional panel filter." + }, + { + "name": "--event", + "value": "<selector>", + "required": false, + "description": "Optional semantic event selector." + }, + { + "name": "--since", + "value": "<cursor>", + "required": false, + "description": "Exclusive replay cursor." + }, + { + "name": "--json", + "required": false, + "description": "Print machine-readable output." + } + ], + "examples": [ + "runpane panels events --since <cursor> --json" + ], + "jsonSchemas": [ + "panelEventsResult" + ], + "notes": [ + "Exit code 3 means cursor_expired; reconcile from the command in the error." + ] + }, + "panels watch": { + "name": "panels watch", + "summary": "Stream semantic panel events as JSONL.", + "details": "Uses replay then live delivery on one retained daemon connection with periodic reconciliation.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--panel", + "value": "<panel-id>", + "required": false, + "description": "Optional panel filter." + }, + { + "name": "--event", + "value": "<selector>", + "required": false, + "description": "Optional semantic event selector." + }, + { + "name": "--since", + "value": "<cursor>", + "required": false, + "description": "Exclusive replay cursor." + }, + { + "name": "--heartbeat-ms", + "value": "<ms>", + "required": false, + "description": "Reconciliation interval." + }, + { + "name": "--jsonl", + "required": false, + "description": "Describes the always-JSONL output." + } + ], + "examples": [ + "runpane panels watch --panel <panel-id> --jsonl" + ], + "jsonSchemas": [ + "semanticEvent", + "cursorExpiredError" + ], + "notes": [ + "Stdout contains compact JSON records only. Exit codes: 0 clean stop, 1 transport error, 3 cursor expired." + ] + }, + "panels await": { + "name": "panels await", + "summary": "Wait for the first matching semantic event.", + "details": "Exit-on-first-match sugar over watch with periodic state reconciliation.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--panel", + "value": "<panel-id>", + "required": true, + "description": "Panel to await." + }, + { + "name": "--event", + "value": "<selector>", + "required": true, + "description": "Semantic event selector." + }, + { + "name": "--since", + "value": "<cursor>", + "required": false, + "description": "Exclusive replay cursor." + }, + { + "name": "--timeout-ms", + "value": "<ms>", + "required": false, + "description": "Wait timeout." + }, + { + "name": "--heartbeat-ms", + "value": "<ms>", + "required": false, + "description": "Reconciliation interval." + }, + { + "name": "--json", + "required": false, + "description": "Print machine-readable output." + } + ], + "examples": [ + "runpane panels await --panel <panel-id> --event agent-idle --json" + ], + "jsonSchemas": [ + "panelAwaitResult", + "cursorExpiredError" + ], + "notes": [ + "Exit codes: 0 matched, 1 transport error, 2 timed out, 3 cursor expired." + ] + }, "panels create": { "name": "panels create", "summary": "Create a reviewer/helper terminal tab inside an existing Pane.", diff --git a/shared/types/runpaneOrchestration.ts b/shared/types/runpaneOrchestration.ts index bd0f063b..4523184d 100644 --- a/shared/types/runpaneOrchestration.ts +++ b/shared/types/runpaneOrchestration.ts @@ -141,6 +141,84 @@ export interface RunpanePanelBlockedState { suggestedCommand?: string; } +export const RUNPANE_EVENT_SELECTOR_TO_TYPE = { + 'panel-created': 'panel_created', + 'terminal-ready': 'terminal_ready', + 'prompt-staged': 'prompt_staged', + 'prompt-submitted': 'prompt_submitted', + 'agent-active': 'agent_active', + 'agent-idle': 'agent_idle', + 'input-required': 'input_required', + blocked: 'blocked', + unblocked: 'unblocked', + 'panel-exited': 'panel_exited', + 'panel-archived': 'panel_archived', +} as const; + +export type RunpaneSemanticEventSelector = keyof typeof RUNPANE_EVENT_SELECTOR_TO_TYPE; +export type RunpaneSemanticEventType = typeof RUNPANE_EVENT_SELECTOR_TO_TYPE[RunpaneSemanticEventSelector]; + +export interface RunpaneSemanticEvent { + id: string; + cursor: string; + type: RunpaneSemanticEventType; + at: string; + paneId?: string; + panelId: string; + state: RunpanePanelStateSummary; + resolvedBy?: 'event' | 'reconciliation'; +} + +export interface RunpaneCursorExpiredError { + code: 'cursor_expired'; + earliestCursor: string; + reconcileCommand: string; +} + +export interface RunpanePanelsEventsRequest { + panelId?: string; + event?: RunpaneSemanticEventSelector; + since?: string; +} + +export interface RunpanePanelsEventsResult { + ok: true; + events: RunpaneSemanticEvent[]; + cursor: string; +} + +export interface RunpanePanelsEventsErrorResult { + ok: false; + error: RunpaneCursorExpiredError; +} + +export type RunpanePanelsEventsResponse = RunpanePanelsEventsResult | RunpanePanelsEventsErrorResult; + +export interface RunpanePanelsWatchRequest extends RunpanePanelsEventsRequest { + heartbeatMs?: number; +} + +export interface RunpanePanelsAwaitRequest extends RunpanePanelsWatchRequest { + panelId: string; + event: RunpaneSemanticEventSelector; + timeoutMs?: number; +} + +export interface RunpanePanelsAwaitResult { + ok: true; + timedOut: false; + matchedEvent: RunpaneSemanticEventType; + resolvedBy: 'event' | 'reconciliation'; + event?: RunpaneSemanticEvent; + state: RunpanePanelStateSummary; +} + +export interface RunpanePanelsAwaitTimeoutResult { + ok: false; + timedOut: true; + state: RunpanePanelStateSummary; +} + export interface RunpanePaneReadiness { ok: boolean; condition: RunpanePanelWaitCondition; From 24813b59597cb69d2213ccdbcb539fc7ab7af538 Mon Sep 17 00:00:00 2001 From: parsakhaz <khazapar7@gmail.com> Date: Thu, 23 Jul 2026 00:32:45 -0700 Subject: [PATCH 3/4] feat: multi-target supervision and verified agent startup for RunPane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compressed final phase of the event-driven RunPane orchestration epic (#356), folding the epic's original phases 3 and 4. Multi-target supervision: - `panels await-any` across repeated --panel/--pane targets, identifying the winning panelId and paneId. - `panes watch --include-future-panels` extends the watched set when a panel is created inside the pane, without restarting the watch. - `panes status --changed-since` returns only panels with transitions after the cursor, plus a snapshot cursor taken after the state read so a subsequent watch observes no gap (D3 snapshot-then-subscribe, D8 pull surface). - Both wrappers open the retained stream before resolving pane targets, so events emitted during resolution are not lost. Verified startup and safe interstitials: - `panes create --start-agent --wait-active` reports ok:true only after verifiedSubmitted and an agent_active transition causally observed after the prompt was accepted — a rendering interstitial no longer counts as "active". - All create-time automatic input flows through a single CreateInputGuard chokepoint that classifies the current screen deny-list-first immediately before every write and submit sequence. Consequential interstitials — auth, permissions, destructive confirmations, terms, payments and directory trust — are never auto-answered and emit input_required instead (D6). Only an explicit allowlist of reversible responses is handled, under caller opt-in, and the original prompt is then submitted exactly once. - Readiness failure keeps the delivery premark and marks input as create-owned, so the terminal manager's delayed CLI-ready callback cannot deliver it unguarded after the create returns. Repeatable target flags are declared in the canonical contract and mirrored in both wrappers with parity coverage. Everything is additive; `panels wait` stays frozen (D2). Old phase 5 — milestones, warning severity separation, and `panels output --since` — is not implemented; see the PR body. Refs #356 --- contracts/runpane/contract.json | 81 ++- contracts/runpane/schema.json | 9 + docs/RUNPANE_CLI_CONTRACT.md | 5 + main/src/ipc/runpane.test.ts | 539 +++++++++++++++++- main/src/ipc/runpane.ts | 322 ++++++++++- main/src/services/runpaneEventLog.ts | 5 + .../src/services/runpaneInterstitials.test.ts | 34 ++ main/src/services/runpaneInterstitials.ts | 37 ++ .../src/services/terminalPanelManager.test.ts | 76 +++ main/src/services/terminalPanelManager.ts | 6 +- packages/runpane-py/src/runpane/cli.py | 48 +- .../src/runpane/generated_contract.py | 2 +- .../runpane-py/src/runpane/local_control.py | 87 ++- packages/runpane/src/cli.ts | 8 + packages/runpane/src/commands.ts | 38 +- packages/runpane/src/generated/contract.ts | 269 +++++++++ packages/runpane/src/localControl.ts | 103 +++- .../runpane/src/localControl.watch.test.ts | 194 ++++++- scripts/fixtures/runpane-contract.json | 32 ++ scripts/test-runpane-contract.js | 8 + shared/types/generatedRunpaneContract.ts | 269 +++++++++ shared/types/panels.ts | 1 + shared/types/runpaneOrchestration.ts | 24 + 23 files changed, 2115 insertions(+), 82 deletions(-) create mode 100644 main/src/services/runpaneInterstitials.test.ts create mode 100644 main/src/services/runpaneInterstitials.ts diff --git a/contracts/runpane/contract.json b/contracts/runpane/contract.json index ace36b60..b22588c0 100644 --- a/contracts/runpane/contract.json +++ b/contracts/runpane/contract.json @@ -228,6 +228,20 @@ "panePinResult" ] }, + { + "name": "panes status", + "summary": "Read a compact semantic-state snapshot for a Pane.", + "usage": ["runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]"], + "jsonSchemas": ["panelStateSummary"] + }, + { + "name": "panes watch", + "summary": "Stream semantic events for panels in a Pane.", + "usage": ["runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]"], + "outputMode": "jsonl", + "exitCodes": {"0":"clean stop", "1":"transport or other error", "3":"cursor expired"}, + "jsonSchemas": ["semanticEvent", "cursorExpiredError"] + }, { "name": "panels create", "summary": "Create a terminal-backed tool panel inside an existing Pane session.", @@ -338,6 +352,13 @@ "usage": ["runpane panels await --panel <panel-id> --event <selector> [--since <cursor>] [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]"], "exitCodes": {"0":"matched", "1":"transport or other error", "2":"timed out", "3":"cursor expired"}, "jsonSchemas": ["panelAwaitResult", "cursorExpiredError"] + }, + { + "name": "panels await-any", + "summary": "Wait for the first matching event across panels or Panes.", + "usage": ["runpane panels await-any (--panel <panel-id>|--pane <pane-id>)... --event <selector> [--timeout-ms <ms>] [--json]"], + "exitCodes": {"0":"matched", "1":"transport or other error", "2":"timed out", "3":"cursor expired"}, + "jsonSchemas": ["panelAwaitResult", "cursorExpiredError"] } ], "flags": { @@ -443,11 +464,13 @@ { "name": "--pane", "value": "<pane-id>", + "repeatable": true, "description": "Pane/session id to inspect." }, { "name": "--panel", "value": "<panel-id>", + "repeatable": true, "description": "Tool panel id to inspect or control." }, { @@ -548,11 +571,21 @@ "value": "<cursor>", "description": "Replay events strictly after this cursor." }, + { + "name": "--changed-since", + "value": "<cursor>", + "description": "Return panels with semantic transitions after this lifecycle cursor." + }, { "name": "--heartbeat-ms", "value": "<milliseconds>", "description": "Periodic event-log and state reconciliation interval." }, + { + "name": "--handle-known-interstitials", + "value": "<safe>", + "description": "Opt in to the explicit reversible interstitial allowlist." + }, { "name": "--text", "value": "<text>", @@ -603,6 +636,12 @@ "name": "--jsonl", "description": "Print one compact JSON object per line (panels watch always uses JSONL)." } + ,{ + "name": "--include-future-panels", + "description": "Keep watching panels created in the selected Pane after the stream starts." + } + ,{"name":"--start-agent","description":"Require verified initial prompt submission and active agent work."} + ,{"name":"--wait-active","description":"Wait for agent activity after initial prompt delivery."} ] }, "help": { @@ -990,6 +1029,8 @@ "Usage:", " runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", "", "Replays retained semantic events strictly after a cursor.", "", "Exit codes: 0 success, 3 cursor expired, 1 transport/error." ], + "panes status": ["Usage:", " runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]", "", "Returns compact per-panel semantic state and a race-free snapshot cursor."], + "panes watch": ["Usage:", " runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]", "", "Streams semantic events for a Pane."], "panels watch": [ "Usage:", " runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", "", "Streams one compact semantic event JSON object per stdout line.", "", "Exit codes: 0 clean stop, 3 cursor expired, 1 transport/error." @@ -998,6 +1039,7 @@ "Usage:", " runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]", "", "Waits for a matching semantic event and periodically reconciles state.", "", "Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport/error." ], + "panels await-any": ["Usage:", " runpane panels await-any (--panel <id>|--pane <id>)... --event <selector> [--timeout-ms <ms>] [--json]", "", "Waits for the first matching event across all selected targets."], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -1402,9 +1444,12 @@ " --pane-dir <path> Connect to a specific Pane data directory", " --json Print machine-readable output" ], + "panes status": ["Usage:", " python -m runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]", "", "Returns compact per-panel semantic state and a race-free snapshot cursor."], + "panes watch": ["Usage:", " python -m runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]", "", "Streams semantic events for a Pane."], "panels events": ["Usage:", " python -m runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", "", "Replays retained semantic events strictly after a cursor."], "panels watch": ["Usage:", " python -m runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", "", "Streams one compact semantic event JSON object per stdout line."], "panels await": ["Usage:", " python -m runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]", "", "Waits for a matching semantic event and periodically reconciles state."], + "panels await-any": ["Usage:", " python -m runpane panels await-any (--panel <id>|--pane <id>)... --event <selector> [--timeout-ms <ms>] [--json]", "", "Waits for the first matching event across all selected targets."], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -1844,7 +1889,10 @@ ], ["panels", "events", "--panel", "panel-1", "--event", "agent-idle", "--since", "epoch:1", "--json"], ["panels", "watch", "--panel", "panel-1", "--event", "blocked", "--since", "epoch:2", "--heartbeat-ms", "1000", "--jsonl"], - ["panels", "await", "--panel", "panel-1", "--event", "agent-idle", "--timeout-ms", "5000", "--heartbeat-ms", "500", "--json"] + ["panels", "await", "--panel", "panel-1", "--event", "agent-idle", "--timeout-ms", "5000", "--heartbeat-ms", "500", "--json"], + ["panels", "await-any", "--panel", "panel-1", "--panel", "panel-2", "--pane", "pane-1", "--pane", "pane-2", "--event", "agent-idle", "--json"], + ["panes", "status", "--pane", "pane-1", "--changed-since", "epoch:4", "--json"], + ["panes", "watch", "--pane", "pane-1", "--include-future-panels", "--jsonl"] ], "topLevelHelpIncludes": [ "runpane setup", @@ -2384,7 +2432,10 @@ "user", "agent" ] - } + }, + "startAgent": { "type": "boolean" }, + "waitActive": { "type": "boolean" }, + "handleKnownInterstitials": { "enum": ["safe"] } }, "additionalProperties": false }, @@ -2470,6 +2521,8 @@ "focused": { "type": "boolean" }, + "verifiedSubmitted": { "type": "boolean" }, + "agentActivity": { "enum": ["unknown", "starting", "active", "idle", "exited"] }, "readiness": { "type": "object", "required": [ @@ -2563,6 +2616,15 @@ }, "nextCommand": { "type": "string" + }, + "handledInterstitials": { + "type": "array", + "items": { + "type": "object", + "required": ["kind", "response"], + "properties": {"kind":{"type":"string"},"response":{"type":"string"}}, + "additionalProperties": false + } } }, "additionalProperties": false @@ -4879,6 +4941,16 @@ "Unpinning does not focus the Pane." ] }, + "panes status": { + "name":"panes status","summary":"Read semantic state for a Pane.","details":"Returns a compact pull snapshot and lifecycle cursor.","requiresPaneDaemon":true,"mutates":false, + "arguments":[{"name":"--pane","value":"<pane-id>","required":true,"description":"Pane to inspect."},{"name":"--changed-since","value":"<cursor>","required":false,"description":"Only panels changed after this cursor."},{"name":"--json","required":false,"description":"Print machine-readable output."}], + "examples":["runpane panes status --pane <pane-id> --json"],"notes":[] + }, + "panes watch": { + "name":"panes watch","summary":"Stream semantic events for a Pane.","details":"Filters the semantic event stream by Pane and can include panels created later.","requiresPaneDaemon":true,"mutates":false, + "arguments":[{"name":"--pane","value":"<pane-id>","required":true,"description":"Pane to watch; repeatable."},{"name":"--include-future-panels","required":false,"description":"Include panels created after watch starts."},{"name":"--event","value":"<selector>","required":false,"description":"Optional event filter."}], + "examples":["runpane panes watch --pane <pane-id> --include-future-panels --jsonl"],"notes":[] + }, "panels list": { "name": "panels list", "summary": "List tool panels inside a Pane session.", @@ -5257,6 +5329,11 @@ "examples": ["runpane panels await --panel <panel-id> --event agent-idle --json"], "jsonSchemas": ["panelAwaitResult", "cursorExpiredError"], "notes": ["Exit codes: 0 matched, 1 transport error, 2 timed out, 3 cursor expired."] }, + "panels await-any": { + "name":"panels await-any","summary":"Wait across multiple panels or Panes.","details":"Resolves Pane targets to their terminal panels and exits on the first matching event.","requiresPaneDaemon":true,"mutates":false, + "arguments":[{"name":"--panel","value":"<panel-id>","required":false,"description":"Panel target; repeatable."},{"name":"--pane","value":"<pane-id>","required":false,"description":"Pane target; repeatable."},{"name":"--event","value":"<selector>","required":true,"description":"Semantic event selector."}], + "examples":["runpane panels await-any --panel p1 --panel p2 --event agent-idle --json"],"notes":[] + }, "panels create": { "name": "panels create", "summary": "Create a reviewer/helper terminal tab inside an existing Pane.", diff --git a/contracts/runpane/schema.json b/contracts/runpane/schema.json index 648653b2..f019395e 100644 --- a/contracts/runpane/schema.json +++ b/contracts/runpane/schema.json @@ -344,6 +344,9 @@ }, "description": { "type": "string" + }, + "repeatable": { + "type": "boolean" } }, "additionalProperties": false @@ -366,6 +369,8 @@ "panes archive", "panes pin", "panes unpin", + "panes status", + "panes watch", "panels", "panels create", "panels list", @@ -378,6 +383,7 @@ "panels events", "panels watch", "panels await", + "panels await-any", "agent-context" ], "properties": { @@ -426,6 +432,8 @@ "panes unpin": { "$ref": "#/$defs/helpLines" }, + "panes status": { "$ref": "#/$defs/helpLines" }, + "panes watch": { "$ref": "#/$defs/helpLines" }, "panels": { "$ref": "#/$defs/helpLines" }, @@ -462,6 +470,7 @@ "panels await": { "$ref": "#/$defs/helpLines" }, + "panels await-any": { "$ref": "#/$defs/helpLines" }, "agent-context": { "$ref": "#/$defs/helpLines" } diff --git a/docs/RUNPANE_CLI_CONTRACT.md b/docs/RUNPANE_CLI_CONTRACT.md index 247cbd31..65b337dd 100644 --- a/docs/RUNPANE_CLI_CONTRACT.md +++ b/docs/RUNPANE_CLI_CONTRACT.md @@ -301,7 +301,9 @@ These flags are consumed by local daemon-control commands: --interval-ms <milliseconds> --event <selector> --since <cursor> +--changed-since <cursor> --heartbeat-ms <milliseconds> +--handle-known-interstitials <safe> --text <text> --input-file <path|-> --source <user|agent> @@ -313,6 +315,9 @@ These flags are consumed by local daemon-control commands: --pinned --force --jsonl +--include-future-panels +--start-agent +--wait-active ``` `runpane doctor --json`, `runpane repos list`, `runpane panes list`, `runpane panes create`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues. diff --git a/main/src/ipc/runpane.test.ts b/main/src/ipc/runpane.test.ts index e1ebb151..ed0b1d4b 100644 --- a/main/src/ipc/runpane.test.ts +++ b/main/src/ipc/runpane.test.ts @@ -13,6 +13,7 @@ import type { RunpaneToolSpec } from '../../../shared/types/runpaneOrchestration const semanticEventAppend = vi.hoisted(() => vi.fn()); const semanticEventReplay = vi.hoisted(() => vi.fn()); const semanticCurrentCursor = vi.hoisted(() => vi.fn(() => 'epoch:0')); +const semanticPanelIdsChangedSince = vi.hoisted(() => vi.fn()); vi.mock('../services/panelManager', () => ({ panelManager: { @@ -34,11 +35,17 @@ vi.mock('../services/terminalPanelManager', () => ({ getLastOutputAt: vi.fn(), getOutputGeneration: vi.fn(), deliverPendingInitialInput: vi.fn(), + getLastKnownBlocker: vi.fn(), }, })); vi.mock('../core/runtime', () => ({ - getRuntimeRunpaneEventLog: () => ({ append: semanticEventAppend, replaySince: semanticEventReplay, currentCursor: semanticCurrentCursor }), + getRuntimeRunpaneEventLog: () => ({ + append: semanticEventAppend, + replaySince: semanticEventReplay, + currentCursor: semanticCurrentCursor, + panelIdsChangedSince: semanticPanelIdsChangedSince, + }), })); import { RUNPANE_CONTRACT } from '../../../shared/types/generatedRunpaneContract'; @@ -239,6 +246,7 @@ describe('runpane IPC handlers', () => { beforeEach(() => { semanticEventAppend.mockReset(); semanticEventReplay.mockReset(); + semanticPanelIdsChangedSince.mockReset(); semanticCurrentCursor.mockReturnValue('epoch:0'); session.isFavorite = undefined; session.favoritePinnedAt = undefined; @@ -254,7 +262,9 @@ describe('runpane IPC handlers', () => { vi.mocked(terminalPanelManager.getLastOutputAt).mockReset(); vi.mocked(terminalPanelManager.getOutputGeneration).mockReset(); vi.mocked(terminalPanelManager.deliverPendingInitialInput).mockReset(); + vi.mocked(terminalPanelManager.getLastKnownBlocker).mockReset(); vi.mocked(terminalPanelManager.getOutputGeneration).mockReturnValue(0); + vi.mocked(terminalPanelManager.getLastKnownBlocker).mockReturnValue(undefined); vi.mocked(panelManager.getPanel).mockImplementation((panelId: string) => panelId === terminalPanel.id ? terminalPanel : undefined @@ -323,6 +333,48 @@ describe('runpane IPC handlers', () => { expect(semanticEventAppend).not.toHaveBeenCalled(); }); + it('phase3 AC3: panes status changed-since filters changed panels and takes cursor after state read', async () => { + const secondPanel: ToolPanel = { + ...terminalPanel, + id: 'panel-2', + title: 'Codex 2', + }; + let stateWasRead = false; + vi.mocked(panelManager.getPanelsForSession).mockReturnValue([terminalPanel, secondPanel]); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockImplementation((panelId: string) => { + if (panelId === secondPanel.id) stateWasRead = true; + return terminalSnapshot(`${panelId} ready\n`, panelId === secondPanel.id ? 'idle' : 'active'); + }); + semanticPanelIdsChangedSince.mockImplementation(() => { + expect(stateWasRead).toBe(true); + return new Set([secondPanel.id]); + }); + semanticCurrentCursor.mockImplementation(() => { + expect(stateWasRead).toBe(true); + return 'epoch:2'; + }); + const registry = createRegistry(); + + const result = await registry.invoke('runpane:panes:status', [{ + paneId: session.id, + changedSince: 'epoch:1', + }]); + + expect(semanticPanelIdsChangedSince).toHaveBeenCalledWith('epoch:1'); + expect(result).toMatchObject({ + ok: true, + paneId: session.id, + cursor: 'epoch:2', + panels: [{ + panelId: secondPanel.id, + paneId: session.id, + state: { + agentActivity: 'idle', + }, + }], + }); + }); + it('lists saved Pane repositories with session counts', async () => { const registry = createRegistry(); @@ -586,6 +638,7 @@ describe('runpane IPC handlers', () => { initialInput: '/review', initialInputMode: 'argument', initialInputSubmitStrategy: 'enter', + initialInputDeliveryOwner: 'runpane-create', agentType: 'claude', isCliPanel: true, }, @@ -1306,6 +1359,7 @@ describe('runpane IPC handlers', () => { initialInput: '$discussion https://github.com/dcouple/Pane/issues/252', initialInputMode: 'argument', initialInputSubmitStrategy: 'enter', + initialInputDeliveryOwner: 'runpane-create', agentType: 'codex', isCliPanel: true, }, @@ -1665,6 +1719,159 @@ describe('runpane IPC handlers', () => { }); }); + it('phase4 AC1: high-level start ok includes verifiedSubmitted true and active agentActivity', async () => { + vi.mocked(terminalPanelManager.getOutputGeneration) + .mockReturnValueOnce(1) + .mockReturnValueOnce(2); + const claudePanel = { + id: 'panel-1', + sessionId: session.id, + type: 'terminal', + title: 'Claude Code', + state: { + customState: { + initialInputSentAt: '2026-01-01T00:02:00.000Z', + }, + }, + metadata: {}, + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + } as never; + vi.mocked(panelManager.createPanel).mockResolvedValue(claudePanel); + vi.mocked(panelManager.getPanel).mockReturnValue(claudePanel); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockReturnValue( + terminalSnapshot('Claude is working\n', 'active', 'claude'), + ); + + const result = await createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + startAgent: true, + waitActive: true, + readyTimeoutMs: 100, + panes: [{ + name: 'issue-302', + tool: { + agent: 'claude', + initialInput: 'Please start issue 302', + }, + }], + }]); + + expect(result).toMatchObject({ + ok: true, + items: [{ + ok: true, + verifiedSubmitted: true, + agentActivity: 'active', + initialInput: { + submitted: true, + verifiedSubmitted: true, + }, + }], + }); + expect(terminalPanelManager.getOutputGeneration).toHaveBeenCalled(); + }); + + it('MF-2: argument-delivery start blocks directory trust instead of treating active output as success', async () => { + const codexPanel = { + ...terminalPanel, + state: { + customState: { + agentType: 'codex', + isCliPanel: true, + initialInputSentAt: '2026-01-01T00:02:00.000Z', + }, + }, + }; + vi.mocked(panelManager.createPanel).mockResolvedValue(codexPanel); + vi.mocked(panelManager.getPanel).mockReturnValue(codexPanel); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockReturnValue( + terminalSnapshot('Do you trust this directory?\n1. Yes\n2. No\n', 'active'), + ); + vi.mocked(terminalPanelManager.getOutputGeneration).mockReturnValue(2); + + const result = await createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + startAgent: true, + readyTimeoutMs: 100, + panes: [{ + name: 'issue-302', + tool: { + agent: 'codex', + initialInput: 'Please start issue 302', + }, + }], + }]); + + expect(semanticEventAppend).toHaveBeenCalledWith('input_required', codexPanel, { paneId: session.id }); + expect(result).toMatchObject({ + ok: false, + items: [{ + ok: false, + verifiedSubmitted: false, + agentActivity: 'active', + initialInput: { + delivered: true, + submitted: false, + verifiedSubmitted: false, + strategy: 'argument', + blocked: { kind: 'unknown', message: expect.stringContaining('Directory trust') }, + }, + }], + }); + expect(semanticEventAppend).not.toHaveBeenCalledWith('prompt_submitted', expect.anything(), expect.anything()); + }); + + it('MF-2: argument-delivery start reclassifies late trust prompt during active wait before prompt_submitted', async () => { + const codexPanel = { + ...terminalPanel, + state: { + customState: { + agentType: 'codex', + isCliPanel: true, + initialInputSentAt: '2026-01-01T00:02:00.000Z', + }, + }, + }; + vi.mocked(panelManager.createPanel).mockResolvedValue(codexPanel); + vi.mocked(panelManager.getPanel).mockReturnValue(codexPanel); + vi.mocked(terminalPanelManager.getOutputGeneration) + .mockReturnValueOnce(1) + .mockReturnValue(2); + vi.mocked(terminalPanelManager.getTerminalSnapshot) + .mockReturnValueOnce(terminalSnapshot('Codex starting\n', 'idle')) + .mockReturnValueOnce(terminalSnapshot('Codex starting\n', 'idle')) + .mockReturnValueOnce(terminalSnapshot('Codex starting\n', 'idle')) + .mockReturnValue(terminalSnapshot('Do you trust this directory?\n1. Yes\n2. No\n', 'active')); + + const result = await createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + startAgent: true, + readyTimeoutMs: 100, + panes: [{ + name: 'issue-302', + tool: { + agent: 'codex', + initialInput: 'Please start issue 302', + }, + }], + }]); + + expect(semanticEventAppend).toHaveBeenCalledWith('input_required', codexPanel, { paneId: session.id }); + expect(semanticEventAppend).not.toHaveBeenCalledWith('prompt_submitted', expect.anything(), expect.anything()); + expect(result).toMatchObject({ + ok: false, + items: [{ + ok: false, + initialInput: { + submitted: false, + verifiedSubmitted: false, + blocked: { kind: 'unknown', message: expect.stringContaining('Directory trust') }, + }, + }], + }); + }); + it('marks pane creation unsuccessful when Claude argument delivery is unverified', async () => { const claudePanel = { id: 'panel-1', @@ -1745,6 +1952,9 @@ describe('runpane IPC handlers', () => { .mockReturnValueOnce(terminalSnapshot('› /do TM-x', 'idle')) .mockReturnValueOnce(terminalSnapshot('› /do TM-x', 'idle')) .mockReturnValueOnce(terminalSnapshot('› /do TM-x', 'idle')) + .mockReturnValueOnce(terminalSnapshot('› /do TM-x', 'idle')) + .mockReturnValueOnce(terminalSnapshot('› /do TM-x', 'idle')) + .mockReturnValueOnce(terminalSnapshot('› /do TM-x', 'idle')) .mockReturnValueOnce(terminalSnapshot('Working on TM-x\n›', 'active')); const resultPromise = createRegistry(createServices()).invoke('runpane:panes:create', [{ @@ -1858,7 +2068,7 @@ describe('runpane IPC handlers', () => { }); }); - it('returns a bounded blocker after three confirmed staged submit attempts', async () => { + it('phase4 AC2: create returns submission_unverified with attempt count after bounded staged retries', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-01-01T00:01:59.000Z')); const codexPanel = { ...terminalPanel, state: { customState: { agentType: 'codex' } } }; @@ -1908,7 +2118,313 @@ describe('runpane IPC handlers', () => { }); }); - it('clears the composer premark when wait-ready times out before staging initial input', async () => { + it('phase4 AC3: safe allowlisted interstitial is handled before the original prompt is submitted exactly once', async () => { + vi.useFakeTimers(); + const codexPanel = { ...terminalPanel, state: { customState: { agentType: 'codex', isCliPanel: true } } }; + vi.mocked(panelManager.createPanel).mockResolvedValue(codexPanel); + vi.mocked(panelManager.getPanel).mockReturnValue(codexPanel); + vi.mocked(terminalPanelManager.getOutputGeneration).mockReturnValue(0); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockImplementation(() => { + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + if (!writes.includes('2\r')) { + return terminalSnapshot('Update available! 0.136.0 -> 0.141.0\n2. Skip\nPress enter to continue\n', 'idle'); + } + if (!writes.includes('/do delete obsolete migration files')) { + return terminalSnapshot('›', 'idle'); + } + if (!writes.includes('\x1b[13;5u\r')) { + return terminalSnapshot('› /do delete obsolete migration files', 'idle'); + } + return terminalSnapshot('Working on cleanup\n›', 'active'); + }); + + const resultPromise = createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + startAgent: true, + handleKnownInterstitials: 'safe', + readyTimeoutMs: 100, + panes: [{ name: 'issue-358', tool: { agent: 'codex', initialInput: '/do delete obsolete migration files' } }], + }]); + await vi.runAllTimersAsync(); + const result = await resultPromise; + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + + expect(writes.filter(input => input === '/do delete obsolete migration files')).toHaveLength(1); + expect(writes).toEqual(['2\r', '/do delete obsolete migration files', '\x1b[13;5u\r']); + expect(result).toMatchObject({ + ok: true, + items: [{ + ok: true, + initialInput: { + submitted: true, + verifiedSubmitted: true, + handledInterstitials: [{ kind: 'codex-update', response: '2' }], + }, + }], + }); + }); + + it('MF-6: staged-input exclusion preserves a separate real interstitial sharing the staged substring', async () => { + vi.useFakeTimers(); + const codexPanel = { ...terminalPanel, state: { customState: { agentType: 'codex', isCliPanel: true } } }; + vi.mocked(panelManager.createPanel).mockResolvedValue(codexPanel); + vi.mocked(panelManager.getPanel).mockReturnValue(codexPanel); + vi.mocked(terminalPanelManager.getOutputGeneration).mockReturnValue(1); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockImplementation(() => { + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + if (!writes.includes('/permission')) { + return terminalSnapshot('›', 'idle'); + } + return terminalSnapshot('Review /permission before continuing\n› /permission', 'idle'); + }); + + const resultPromise = createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + startAgent: true, + readyTimeoutMs: 100, + panes: [{ name: 'issue-358', tool: { agent: 'codex', initialInput: '/permission' } }], + }]); + await vi.runAllTimersAsync(); + const result = await resultPromise; + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + + expect(writes.filter(input => input === '/permission')).toHaveLength(1); + expect(writes).not.toContain('\x1b[13;5u\r'); + expect(semanticEventAppend).toHaveBeenCalledWith('input_required', codexPanel, { paneId: session.id }); + expect(result).toMatchObject({ + ok: false, + items: [{ + initialInput: { + submitted: false, + verifiedSubmitted: false, + blocked: { kind: 'unknown', message: expect.stringContaining('permission') }, + }, + }], + }); + }); + + it('MF-9: multiline staged prompts with deny words are removed as one composer region before classification', async () => { + vi.useFakeTimers(); + const initialInput = '/do\nDelete obsolete migration files'; + const codexPanel = { ...terminalPanel, state: { customState: { agentType: 'codex', isCliPanel: true } } }; + vi.mocked(panelManager.createPanel).mockResolvedValue(codexPanel); + vi.mocked(panelManager.getPanel).mockReturnValue(codexPanel); + vi.mocked(terminalPanelManager.getOutputGeneration).mockImplementation(() => { + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + return writes.includes('\x1b[13;5u\r') ? 1 : 0; + }); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockImplementation(() => { + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + if (!writes.includes(initialInput)) { + return terminalSnapshot('›', 'idle'); + } + if (!writes.includes('\x1b[13;5u\r')) { + return terminalSnapshot('› /do\nDelete obsolete migration files', 'idle'); + } + return terminalSnapshot('Working on cleanup\n›', 'active'); + }); + + const resultPromise = createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + startAgent: true, + readyTimeoutMs: 100, + panes: [{ name: 'issue-358', tool: { agent: 'codex', initialInput } }], + }]); + await vi.runAllTimersAsync(); + const result = await resultPromise; + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + + expect(writes.filter(input => input === initialInput)).toHaveLength(1); + expect(writes.filter(input => input === '\x1b[13;5u\r')).toHaveLength(1); + expect(semanticEventAppend).not.toHaveBeenCalledWith('input_required', expect.anything(), expect.anything()); + expect(result).toMatchObject({ + ok: true, + items: [{ + ok: true, + initialInput: { + submitted: true, + verifiedSubmitted: true, + }, + }], + }); + }); + + it('MF-9: visually wrapped staged prompts with deny words are removed as one composer region before classification', async () => { + vi.useFakeTimers(); + const initialInput = '/do Delete obsolete migration files'; + const codexPanel = { ...terminalPanel, state: { customState: { agentType: 'codex', isCliPanel: true } } }; + vi.mocked(panelManager.createPanel).mockResolvedValue(codexPanel); + vi.mocked(panelManager.getPanel).mockReturnValue(codexPanel); + vi.mocked(terminalPanelManager.getOutputGeneration).mockImplementation(() => { + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + return writes.includes('\x1b[13;5u\r') ? 1 : 0; + }); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockImplementation(() => { + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + if (!writes.includes(initialInput)) { + return terminalSnapshot('›', 'idle'); + } + if (!writes.includes('\x1b[13;5u\r')) { + return terminalSnapshot('› /do Delete obsolete\nmigration files', 'idle'); + } + return terminalSnapshot('Working on cleanup\n›', 'active'); + }); + + const resultPromise = createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + startAgent: true, + readyTimeoutMs: 100, + panes: [{ name: 'issue-358', tool: { agent: 'codex', initialInput } }], + }]); + await vi.runAllTimersAsync(); + const result = await resultPromise; + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + + expect(writes.filter(input => input === initialInput)).toHaveLength(1); + expect(writes.filter(input => input === '\x1b[13;5u\r')).toHaveLength(1); + expect(semanticEventAppend).not.toHaveBeenCalledWith('input_required', expect.anything(), expect.anything()); + expect(result).toMatchObject({ + ok: true, + items: [{ + ok: true, + initialInput: { + submitted: true, + verifiedSubmitted: true, + }, + }], + }); + }); + + it('MF-8: composer verification reclassifies a late directory-trust prompt before accepting cleared input', async () => { + vi.useFakeTimers(); + const initialInput = '/do TM-x'; + const codexPanel = { ...terminalPanel, state: { customState: { agentType: 'codex', isCliPanel: true } } }; + vi.mocked(panelManager.createPanel).mockResolvedValue(codexPanel); + vi.mocked(panelManager.getPanel).mockReturnValue(codexPanel); + vi.mocked(terminalPanelManager.getOutputGeneration).mockImplementation(() => { + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + return writes.includes('\x1b[13;5u\r') ? 1 : 0; + }); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockImplementation(() => { + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + if (!writes.includes(initialInput)) { + return terminalSnapshot('›', 'idle'); + } + if (!writes.includes('\x1b[13;5u\r')) { + return terminalSnapshot('› /do TM-x', 'idle'); + } + return terminalSnapshot('Do you trust this directory?\n1. Yes\n2. No\n', 'active'); + }); + + const resultPromise = createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + startAgent: true, + readyTimeoutMs: 100, + panes: [{ name: 'issue-358', tool: { agent: 'codex', initialInput } }], + }]); + await vi.runAllTimersAsync(); + const result = await resultPromise; + const writes = vi.mocked(terminalPanelManager.writeToTerminal).mock.calls.map(call => call[1]); + + expect(writes).toEqual([initialInput, '\x1b[13;5u\r']); + expect(semanticEventAppend).toHaveBeenCalledWith('input_required', codexPanel, { paneId: session.id }); + expect(semanticEventAppend).not.toHaveBeenCalledWith('prompt_submitted', expect.anything(), expect.anything()); + expect(result).toMatchObject({ + ok: false, + items: [{ + ok: false, + initialInput: { + submitted: false, + verifiedSubmitted: false, + blocked: { kind: 'unknown', message: expect.stringContaining('Directory trust') }, + }, + }], + }); + }); + + it('MF-1: no-wait Codex slash create premarks input and routes trust screen through classifier without PTY write', async () => { + vi.useFakeTimers(); + const codexPanel = { ...terminalPanel, state: { customState: { agentType: 'codex', isCliPanel: true } } }; + vi.mocked(panelManager.createPanel).mockImplementation(async (request) => ({ + ...codexPanel, + state: { + ...codexPanel.state, + customState: { + ...codexPanel.state.customState, + ...request.initialState, + }, + }, + })); + vi.mocked(panelManager.getPanel).mockImplementation(() => ({ + ...codexPanel, + state: { + ...codexPanel.state, + customState: { + ...codexPanel.state.customState, + initialInput: '/do TM-x', + initialInputSentAt: '2026-01-01T00:02:00.000Z', + initialInputSubmitStrategy: 'codex-ctrl-enter', + }, + }, + })); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockReturnValue( + terminalSnapshot('Do you trust this directory?\n1. Yes\n2. No\n', 'idle'), + ); + + const result = await createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + panes: [{ name: 'issue-358', tool: { agent: 'codex', initialInput: '/do TM-x' } }], + }]); + await vi.runAllTimersAsync(); + + expect(panelManager.createPanel).toHaveBeenCalledWith(expect.objectContaining({ + initialState: expect.objectContaining({ + initialInput: '/do TM-x', + initialInputSentAt: expect.any(String), + }), + })); + expect(result).toMatchObject({ ok: true, items: [{ ok: true, initialInput: undefined }] }); + expect(terminalPanelManager.writeToTerminal).not.toHaveBeenCalled(); + expect(semanticEventAppend).toHaveBeenCalledWith('input_required', expect.objectContaining({ id: codexPanel.id }), { paneId: session.id }); + }); + + it('phase4 AC4: directory-trust interstitial with safe flag emits input_required and performs no PTY write', async () => { + const codexPanel = { ...terminalPanel, state: { customState: { agentType: 'codex', isCliPanel: true } } }; + vi.mocked(panelManager.createPanel).mockResolvedValue(codexPanel); + vi.mocked(panelManager.getPanel).mockReturnValue(codexPanel); + vi.mocked(terminalPanelManager.getTerminalSnapshot).mockReturnValue( + terminalSnapshot('Do you trust this directory?\n1. Yes\n2. No\n', 'idle'), + ); + + const result = await createRegistry(createServices()).invoke('runpane:panes:create', [{ + repo: { id: project.id }, + startAgent: true, + handleKnownInterstitials: 'safe', + readyTimeoutMs: 100, + panes: [{ name: 'issue-358', tool: { agent: 'codex', initialInput: '/do TM-x' } }], + }]); + + expect(terminalPanelManager.writeToTerminal).not.toHaveBeenCalled(); + expect(semanticEventAppend).toHaveBeenCalledWith('input_required', codexPanel, { paneId: session.id }); + expect(result).toMatchObject({ + ok: false, + items: [{ + ok: false, + initialInput: { + delivered: false, + submitted: false, + verifiedSubmitted: false, + blocked: { + kind: 'unknown', + message: expect.stringContaining('Directory trust'), + suggestedCommand: `runpane panels screen --panel ${codexPanel.id} --json`, + }, + }, + }], + }); + }); + + it('MF-7: readiness failure preserves the RunPane create premark and owner marker', async () => { const createdPanel: ToolPanel = { ...terminalPanel, state: { @@ -1952,14 +2468,15 @@ describe('runpane IPC handlers', () => { }]); expect(terminalPanelManager.writeToTerminal).not.toHaveBeenCalled(); - expect(terminalPanelManager.deliverPendingInitialInput).toHaveBeenCalledWith(createdPanel.id); - expect(panelManager.updatePanel).toHaveBeenCalledWith(createdPanel.id, { - state: expect.objectContaining({ - customState: expect.not.objectContaining({ - initialInputSentAt: expect.any(String), - }), + expect(terminalPanelManager.deliverPendingInitialInput).not.toHaveBeenCalled(); + expect(panelManager.updatePanel).not.toHaveBeenCalled(); + expect(panelManager.createPanel).toHaveBeenCalledWith(expect.objectContaining({ + initialState: expect.objectContaining({ + initialInput: '/do TM-x', + initialInputDeliveryOwner: 'runpane-create', + initialInputSentAt: expect.any(String), }), - }); + })); expect(result).toMatchObject({ ok: false, items: [{ @@ -2076,7 +2593,7 @@ describe('runpane IPC handlers', () => { const result = await resultPromise; const initialState = createRequest?.initialState; const useArgument = toolKind === 'claude' || (toolKind === 'codex' && shape.name !== 'slash'); - const premarkedComposer = waitReady && toolKind === 'codex' && shape.name === 'slash'; + const premarkedComposer = toolKind === 'codex' && shape.name === 'slash'; expect(initialState?.initialInputMode, `${toolKind}/${waitReady}/${shape.name} mode`).toBe( useArgument ? 'argument' : undefined, diff --git a/main/src/ipc/runpane.ts b/main/src/ipc/runpane.ts index f53c0fbc..b91457b7 100644 --- a/main/src/ipc/runpane.ts +++ b/main/src/ipc/runpane.ts @@ -14,6 +14,7 @@ import { assessComposerEvidence, isSlashCommandInput } from './runpaneComposerEv import { boundSanitizedLines, detectPanelBlocker } from '../services/runpaneBlockerDetection'; import { getTerminalCustomState, panelStateSummary } from '../services/runpanePanelState'; import { getRuntimeRunpaneEventLog } from '../core/runtime'; +import { classifyRunpaneInterstitial } from '../services/runpaneInterstitials'; import type { ArchiveProgressManager, SerializedArchiveTask } from '../services/archiveProgressManager'; import type { Project } from '../database/models'; import type { Session, SessionOutput } from '../types/session'; @@ -73,6 +74,8 @@ import type { RunpaneWorktreeCleanupState, RunpanePanelsEventsRequest, RunpanePanelsEventsResponse, + RunpanePaneStatusRequest, + RunpanePaneStatusResult, } from '../../../shared/types/runpaneOrchestration'; import { RUNPANE_EVENT_SELECTOR_TO_TYPE } from '../../../shared/types/runpaneOrchestration'; @@ -83,6 +86,7 @@ const RUNPANE_CHANNELS = [ 'runpane:panes:list', 'runpane:panes:create', 'runpane:panes:pin', + 'runpane:panes:status', 'runpane:panes:archive', 'runpane:panels:create', 'runpane:panels:list', @@ -167,6 +171,31 @@ export function registerRunpaneHandlers( }; }); + commandRegistry.register('runpane:panes:status', (request: unknown): RunpanePaneStatusResult => { + const normalized = parsePaneStatusRequest(request); + const pane = resolvePane(sessionManager, normalized.paneId); + const log = getRuntimeRunpaneEventLog(); + const panelStates = panelManager.getPanelsForSession(pane.id) + .filter(panel => panel.type === 'terminal') + .map(panel => ({ + paneId: pane.id, + panelId: panel.id, + state: panelStateSummary( + panel, + terminalPanelManager.getTerminalSnapshot(panel.id), + undefined, + terminalPanelManager.getLastKnownBlocker(panel.id), + ), + })); + const cursor = log.currentCursor(); + const changed = normalized.changedSince ? log.panelIdsChangedSince(normalized.changedSince) : null; + if (normalized.changedSince && changed === null) { + throw new Error('changedSince cursor is expired; take a fresh panes status snapshot.'); + } + const panels = changed ? panelStates.filter(panel => changed.has(panel.panelId)) : panelStates; + return { ok: true, paneId: pane.id, panels, cursor }; + }); + commandRegistry.register('runpane:repos:list', async (): Promise<RunpaneRepoListResult> => { return withRunpaneAction(services, 'repos:list', {}, () => { const repos = databaseService.getAllProjects().map((project) => @@ -328,6 +357,9 @@ export function registerRunpaneHandlers( waitReady: normalized.waitReady, readyTimeoutMs: normalized.readyTimeoutMs, activate: resolvePaneCreateActivation(normalized, item), + startAgent: normalized.startAgent, + waitActive: normalized.waitActive, + handleKnownInterstitials: normalized.handleKnownInterstitials, }), ); @@ -674,12 +706,17 @@ interface PaneCreateItemOptions { waitReady?: boolean; readyTimeoutMs?: number; activate?: boolean; + startAgent?: boolean; + waitActive?: boolean; + handleKnownInterstitials?: 'safe'; } interface TerminalPanelCreateOptions { activate?: boolean; waitReady?: boolean; readyTimeoutMs?: number; + waitActive?: boolean; + handleKnownInterstitials?: 'safe'; } interface TerminalPanelCreateResult { @@ -696,7 +733,6 @@ async function createTerminalPanelForSession( ): Promise<TerminalPanelCreateResult> { const useArgumentDelivery = shouldUseArgumentDelivery(tool); const shouldCreateSubmitInitialInput = Boolean( - options.waitReady && tool.agent && tool.initialInput && !useArgumentDelivery, @@ -705,6 +741,7 @@ async function createTerminalPanelForSession( initialCommand: tool.command, initialInput: tool.initialInput, ...(useArgumentDelivery ? { initialInputMode: 'argument' as const } : {}), + ...(tool.initialInput && tool.agent ? { initialInputDeliveryOwner: 'runpane-create' as const } : {}), initialInputSubmitStrategy: tool.agent === 'codex' && !useArgumentDelivery ? 'codex-ctrl-enter' : 'enter', @@ -733,6 +770,18 @@ async function createTerminalPanelForSession( context?.commandRunner.wslContext ?? null, ); + const guard = new CreateInputGuard(panel, tool, options.handleKnownInterstitials); + if (options.handleKnownInterstitials && tool.initialInput && tool.agent && !shouldUseArgumentDelivery(tool)) { + const gate = await guard.ensureClear(); + if (gate.blocked) { + return { + panel, + readiness: undefined, + initialInput: blockedInitialInput(tool.initialInput, gate.blocked, guard.handledInterstitials()), + }; + } + } + const readiness = options.waitReady ? toPaneReadiness(await waitForPanel(panel, { panelId: panel.id, @@ -742,7 +791,15 @@ async function createTerminalPanelForSession( })) : undefined; - const initialInput = readiness ? await submitCreateInitialInput(panel, tool, readiness) : undefined; + if (!readiness && tool.initialInput && tool.agent && !useArgumentDelivery) { + void deliverCreateInitialInputWhenReady(panel, tool, options, guard).catch((error: unknown) => { + console.warn(`RunPane failed to deliver guarded initial input for panel ${panel.id}:`, error); + }); + } + + const initialInput = readiness + ? await submitCreateInitialInput(panel, tool, readiness, options, guard) + : undefined; return { panel, readiness, initialInput }; } @@ -751,6 +808,8 @@ async function submitCreateInitialInput( panel: ToolPanel, tool: RunpaneResolvedTool, readiness?: RunpanePaneReadiness, + options: Pick<TerminalPanelCreateOptions, 'waitActive' | 'readyTimeoutMs' | 'handleKnownInterstitials'> = {}, + guard = new CreateInputGuard(panel, tool, options.handleKnownInterstitials), ): Promise<RunpaneInitialInputDeliveryResult | undefined> { if (!tool.initialInput) { return undefined; @@ -764,20 +823,32 @@ async function submitCreateInitialInput( const sentAt = optionalString(customState.initialInputSentAt); const deliveryError = optionalString(customState.initialInputError); const delivered = Boolean(sentAt) && !deliveryError; - if (delivered && currentPanel) { + if (delivered) { + const gate = await guard.ensureClear(); + if (gate.blocked) return blockedArgumentInitialInput(tool.initialInput, gate.blocked, guard.handledInterstitials(), sentAt); + } + const baselineGeneration = terminalPanelManager.getOutputGeneration(panel.id); + const activeResult = delivered && options.waitActive + ? await waitForAgentActiveAfter(panel, options, guard, baselineGeneration) + : { active: delivered }; + if (activeResult.blocked) { + return blockedArgumentInitialInput(tool.initialInput, activeResult.blocked, guard.handledInterstitials(), sentAt); + } + const active = activeResult.active; + if (active && currentPanel) { getRuntimeRunpaneEventLog().append('prompt_submitted', currentPanel, { paneId: panel.sessionId }); } return { delivered, - submitted: delivered, + submitted: active, inputBytes: Buffer.byteLength(tool.initialInput, 'utf8'), strategy: 'argument', sequenceName: 'argument', - verifiedSubmitted: delivered, + verifiedSubmitted: active, sentAt, - ...(delivered ? {} : { + ...(active ? {} : { error: { - message: deliveryError ?? 'Initial input was not attached to the agent launch command.', + message: deliveryError ?? (delivered ? 'Initial input was attached, but agent activity did not begin.' : 'Initial input was not attached to the agent launch command.'), }, }), nextCommand: readiness?.nextCommand ?? panelWaitCommand(panel.id), @@ -788,40 +859,203 @@ async function submitCreateInitialInput( return undefined; } - if (!readiness.ok) { - await clearInitialInputSentPremark(panel); - terminalPanelManager.deliverPendingInitialInput(panel.id); + let effectiveReadiness = readiness; + if (!effectiveReadiness.ok && options.handleKnownInterstitials) { + const gate = await guard.ensureClear(); + if (gate.blocked) return blockedInitialInput(tool.initialInput, gate.blocked, guard.handledInterstitials()); + effectiveReadiness = toPaneReadiness(await waitForPanel(panel, { + panelId: panel.id, + condition: 'ready', + timeoutMs: options.readyTimeoutMs ?? DEFAULT_PANEL_WAIT_TIMEOUT_MS, + intervalMs: DEFAULT_PANEL_WAIT_INTERVAL_MS, + })); + } + + if (!effectiveReadiness.ok) { return { delivered: false, submitted: false, inputBytes: Buffer.byteLength(tool.initialInput, 'utf8'), error: { message: 'Initial input was not sent because the terminal panel did not become ready.' }, - nextCommand: readiness.nextCommand ?? panelWaitCommand(panel.id), + nextCommand: effectiveReadiness.nextCommand ?? panelWaitCommand(panel.id), }; } - terminalPanelManager.writeToTerminal(panel.id, tool.initialInput); + const stage = await guard.write(tool.initialInput); + if (stage.blocked) return blockedInitialInput(tool.initialInput, stage.blocked, guard.handledInterstitials()); getRuntimeRunpaneEventLog().append('prompt_staged', panel, { paneId: panel.sessionId }); await sleep(300); - return submitCreateComposerInput(panel, tool); + return submitCreateComposerInput(panel, tool, guard); +} + +async function deliverCreateInitialInputWhenReady( + panel: ToolPanel, + tool: RunpaneResolvedTool, + options: Pick<TerminalPanelCreateOptions, 'waitActive' | 'readyTimeoutMs' | 'handleKnownInterstitials'>, + guard: CreateInputGuard, +): Promise<void> { + const readiness = toPaneReadiness(await waitForPanel(panel, { + panelId: panel.id, + condition: 'ready', + timeoutMs: options.readyTimeoutMs ?? DEFAULT_PANEL_WAIT_TIMEOUT_MS, + intervalMs: DEFAULT_PANEL_WAIT_INTERVAL_MS, + })); + await submitCreateInitialInput(panel, tool, readiness, options, guard); } -async function clearInitialInputSentPremark(panel: ToolPanel): Promise<void> { - const currentPanel = panelManager.getPanel(panel.id) ?? panel; - const state = currentPanel.state ?? {}; - const customState = isRecord(state.customState) ? state.customState : {}; - if (!Object.prototype.hasOwnProperty.call(customState, 'initialInputSentAt')) { - return; +async function waitForAgentActiveAfter( + panel: ToolPanel, + options: Pick<TerminalPanelCreateOptions, 'readyTimeoutMs' | 'handleKnownInterstitials'>, + guard: CreateInputGuard, + baselineGeneration: number, +): Promise<{ active: boolean; blocked?: RunpanePanelBlockedState }> { + const timeoutMs = options.readyTimeoutMs ?? DEFAULT_PANEL_WAIT_TIMEOUT_MS; + const deadline = Date.now() + timeoutMs; + while (Date.now() <= deadline) { + const current = panelManager.getPanel(panel.id) ?? panel; + const screen = await buildPanelScreenResult(current, DEFAULT_PANEL_SCREEN_LIMIT); + const state = screen.state; + if (terminalPanelManager.getOutputGeneration(panel.id) > baselineGeneration) { + const gate = await guard.ensureScreenClear(screen.text, current); + if (gate.blocked) return { active: false, blocked: gate.blocked }; + if (state.agentActivity === 'active') return { active: true }; + } + if (state.agentActivity === 'exited') return { active: false }; + await sleep(DEFAULT_COMPOSER_VERIFY_INTERVAL_MS); } + return { active: false }; +} - const nextCustomState = { ...customState }; - delete nextCustomState.initialInputSentAt; - await panelManager.updatePanel(panel.id, { - state: { - ...state, - customState: nextCustomState, - }, +function classifyCreateInterstitial(text: string, tool: RunpaneResolvedTool, panelId: string): ReturnType<typeof classifyRunpaneInterstitial> { + return classifyRunpaneInterstitial(textWithoutKnownStagedInput(text, tool.initialInput), tool.agent, panelId); +} + +class CreateInputGuard { + private readonly handledKinds = new Set<string>(); + + constructor( + private readonly panel: ToolPanel, + private readonly tool: RunpaneResolvedTool, + private readonly mode: 'safe' | undefined, + ) {} + + handledInterstitials(): Array<{ kind: string; response: string }> { + return [...this.handledKinds].map(kind => ({ kind, response: '2' })); + } + + async write(input: string, panel: ToolPanel = this.panel): Promise<{ blocked?: RunpanePanelBlockedState }> { + const gate = await this.ensureClear(panel); + if (gate.blocked) return gate; + terminalPanelManager.writeToTerminal(panel.id, input); + return {}; + } + + async ensureClear(panel: ToolPanel = this.panel): Promise<{ blocked?: RunpanePanelBlockedState }> { + const screen = await buildPanelScreenResult(panel, DEFAULT_PANEL_SCREEN_LIMIT); + return this.ensureScreenClear(screen.text, panel); + } + + async ensureScreenClear(text: string, panel: ToolPanel = this.panel): Promise<{ blocked?: RunpanePanelBlockedState }> { + const classification = classifyCreateInterstitial(text, this.tool, panel.id); + if (classification.disposition === 'clear') return {}; + if (classification.disposition === 'allow' && this.mode === 'safe' && !this.handledKinds.has(classification.kind)) { + this.handledKinds.add(classification.kind); + terminalPanelManager.writeToTerminal(panel.id, `${classification.response}\r`); + const deadline = Date.now() + DEFAULT_COMPOSER_VERIFY_TIMEOUT_MS; + while (Date.now() < deadline) { + await sleep(DEFAULT_COMPOSER_VERIFY_INTERVAL_MS); + const nextScreen = await buildPanelScreenResult(panel, DEFAULT_PANEL_SCREEN_LIMIT); + const next = classifyCreateInterstitial(nextScreen.text, this.tool, panel.id); + if (next.disposition !== 'allow') return this.ensureClear(panel); + } + } + const blocker = classification.disposition === 'allow' + ? { kind: 'codex-update' as const, message: 'The optional Codex update prompt requires opt-in safe handling.', suggestedCommand: panelScreenCommand(panel.id) } + : classification.blocker; + getRuntimeRunpaneEventLog().append('input_required', panel, { paneId: panel.sessionId }); + return { blocked: blocker }; + } +} + +function textWithoutKnownStagedInput(text: string, stagedInput: string | undefined): string { + const trimmed = stagedInput?.trim(); + if (!trimmed) return text; + const lines = text.split('\n'); + const stagedLines = trimmed.split(/\r?\n/).map(line => line.trim()).filter(Boolean); + if (stagedLines.length === 0) return text; + const stagedFlat = stagedLines.join(' '); + const start = lines.findIndex((line) => { + const strippedLine = stripComposerStagingPrefix(line); + return looksLikeComposerStagingLine(line) && + (line.includes(stagedLines[0]) || (Boolean(strippedLine) && stagedFlat.includes(strippedLine))); }); + if (start < 0) return text; + const nextLines = [...lines]; + const compactStaged = stagedFlat.replace(/\s+/g, ''); + let compactRemoved = ''; + let stagedIndex = 0; + for (let index = start; index < nextLines.length; index += 1) { + const strippedLine = stripComposerStagingPrefix(nextLines[index]); + if (stagedIndex < stagedLines.length && nextLines[index].includes(stagedLines[stagedIndex])) { + nextLines[index] = nextLines[index].split(stagedLines[stagedIndex]).join(''); + compactRemoved += stagedLines[stagedIndex].replace(/\s+/g, ''); + stagedIndex += 1; + if (stagedIndex >= stagedLines.length) break; + continue; + } + if (strippedLine && stagedFlat.includes(strippedLine)) { + nextLines[index] = ''; + compactRemoved += strippedLine.replace(/\s+/g, ''); + if (compactRemoved.length >= compactStaged.length) break; + continue; + } + if (compactRemoved) break; + } + return nextLines.join('\n'); +} + +function stripComposerStagingPrefix(line: string): string { + return line.trim().replace(/^[›>]\s*/, '').trim(); +} + +function looksLikeComposerStagingLine(line: string): boolean { + return /^[›>]\s*/.test(line.trim()) || + /(?:press\s+)?(?:ctrl|control)\+enter\s+to\s+submit/i.test(line) || + /\[Pasted Content[^\]]*\]/i.test(line); +} + +type HandledCreateInterstitial = { kind: string; response: string }; + +function blockedInitialInput(input: string, blocked: RunpanePanelBlockedState, handledInterstitials: HandledCreateInterstitial[]): RunpaneInitialInputDeliveryResult { + return { + delivered: false, + submitted: false, + verifiedSubmitted: false, + inputBytes: Buffer.byteLength(input, 'utf8'), + blocked, + handledInterstitials, + nextCommand: blocked.suggestedCommand, + }; +} + +function blockedArgumentInitialInput( + input: string, + blocked: RunpanePanelBlockedState, + handledInterstitials: HandledCreateInterstitial[], + sentAt: string | undefined, +): RunpaneInitialInputDeliveryResult { + return { + delivered: true, + submitted: false, + verifiedSubmitted: false, + inputBytes: Buffer.byteLength(input, 'utf8'), + strategy: 'argument', + sequenceName: 'argument', + sentAt, + blocked, + handledInterstitials, + nextCommand: blocked.suggestedCommand, + }; } function shouldUseArgumentDelivery(tool: RunpaneResolvedTool): boolean { @@ -835,24 +1069,31 @@ function shouldUseArgumentDelivery(tool: RunpaneResolvedTool): boolean { async function submitCreateComposerInput( panel: ToolPanel, tool: RunpaneResolvedTool, + guard: CreateInputGuard, ): Promise<RunpaneInitialInputDeliveryResult> { const input = tool.initialInput ?? ''; const submit = resolveComposerSubmit('auto', tool.agent); const nextCommand = panelScreenCommand(panel.id); let lastVerdict: ReturnType<typeof assessComposerEvidence> = 'unknown'; + let observedStaged = false; let attempts = 0; for (let attempt = 1; attempt <= MAX_CREATE_SUBMIT_ATTEMPTS; attempt += 1) { attempts = attempt; const beforeScreen = await buildPanelScreenResult(panel, DEFAULT_PANEL_SCREEN_LIMIT); const outputGenerationBeforeSubmit = terminalPanelManager.getOutputGeneration(panel.id); - terminalPanelManager.writeToTerminal(panel.id, submit.input); + const submitWrite = await guard.write(submit.input); + if (submitWrite.blocked) return blockedInitialInput(input, submitWrite.blocked, guard.handledInterstitials()); const attemptStartedAt = Date.now(); let retryConfirmed = false; while (Date.now() - attemptStartedAt <= DEFAULT_COMPOSER_VERIFY_TIMEOUT_MS) { await sleep(DEFAULT_COMPOSER_VERIFY_INTERVAL_MS); const afterScreen = await buildPanelScreenResult(panel, DEFAULT_PANEL_SCREEN_LIMIT); + if (panelHasFreshOutputSince(panel.id, outputGenerationBeforeSubmit)) { + const gate = await guard.ensureScreenClear(afterScreen.text, panel); + if (gate.blocked) return blockedInitialInput(input, gate.blocked, guard.handledInterstitials()); + } lastVerdict = assessComposerEvidence({ beforeText: beforeScreen.text, afterText: afterScreen.text, @@ -872,6 +1113,7 @@ async function submitCreateComposerInput( attempts, sentAt: new Date().toISOString(), nextCommand, + handledInterstitials: guard.handledInterstitials(), }; } @@ -880,6 +1122,7 @@ async function submitCreateComposerInput( } const afterScreenHasFreshOutput = panelHasFreshOutputSince(panel.id, outputGenerationBeforeSubmit); + observedStaged ||= lastVerdict === 'staged' && afterScreenHasFreshOutput; if (!afterScreenHasFreshOutput) { lastVerdict = 'unknown'; continue; @@ -899,6 +1142,7 @@ async function submitCreateComposerInput( }) === 'staged'; const confirmationScreenHasFreshOutput = panelHasFreshOutputSince(panel.id, outputGenerationBeforeSubmit); lastVerdict = confirmationVerdict; + observedStaged ||= confirmationVerdict === 'staged' && confirmationScreenHasFreshOutput; if (confirmationVerdict === 'staged' && unchangedSinceFirstSample && confirmationScreenHasFreshOutput) { retryConfirmed = true; @@ -918,7 +1162,7 @@ async function submitCreateComposerInput( strategy: submit.strategy, sequenceName: submit.sequenceName, verifiedSubmitted: false, - staged: lastVerdict === 'staged', + staged: observedStaged || lastVerdict === 'staged', attempts, sentAt: new Date().toISOString(), blocked: { @@ -927,6 +1171,7 @@ async function submitCreateComposerInput( suggestedCommand: nextCommand, }, nextCommand, + handledInterstitials: guard.handledInterstitials(), }; } @@ -965,11 +1210,16 @@ async function createPaneItem( const { panel, readiness, initialInput } = await createTerminalPanelForSession(services, session, tool, { activate: options.activate, - waitReady: options.waitReady, + waitReady: options.waitReady || options.startAgent || options.waitActive, readyTimeoutMs: options.readyTimeoutMs, + waitActive: options.waitActive || options.startAgent, + handleKnownInterstitials: options.handleKnownInterstitials, }); - const itemOk = Boolean((!readiness || readiness.ok) && (!initialInput || initialInput.submitted)); + const state = panelStateSummary(panelManager.getPanel(panel.id) ?? panel, terminalPanelManager.getTerminalSnapshot(panel.id)); + const verifiedSubmitted = initialInput?.verifiedSubmitted; + const itemOk = Boolean((!readiness || readiness.ok) && (!initialInput || initialInput.submitted) + && (!options.startAgent || (verifiedSubmitted === true && state.agentActivity === 'active'))); return { ok: itemOk, index, @@ -985,6 +1235,8 @@ async function createPaneItem( focused: Boolean(panel.state.isActive), readiness, initialInput, + verifiedSubmitted, + agentActivity: state.agentActivity, }; } catch (error) { return createFailureItem(index, item, error); @@ -1519,6 +1771,9 @@ function parsePaneCreateRequest(value: unknown): RunpanePaneCreateRequest { noFocus: typeof value.noFocus === 'boolean' ? value.noFocus : undefined, focus: typeof value.focus === 'boolean' ? value.focus : undefined, source: value.source === 'user' || value.source === 'agent' ? value.source : undefined, + startAgent: typeof value.startAgent === 'boolean' ? value.startAgent : undefined, + waitActive: typeof value.waitActive === 'boolean' ? value.waitActive : undefined, + handleKnownInterstitials: value.handleKnownInterstitials === 'safe' ? 'safe' : undefined, }; } @@ -1700,6 +1955,13 @@ function parsePanelsEventsRequest(value: unknown): RunpanePanelsEventsRequest { }; } +function parsePaneStatusRequest(value: unknown): RunpanePaneStatusRequest { + if (!isRecord(value)) throw new Error('Pane status request must be an object'); + const paneId = optionalString(value.paneId)?.trim(); + if (!paneId) throw new Error('Pane status request must include paneId'); + return { paneId, changedSince: optionalString(value.changedSince) }; +} + function parseAgentDoctorRequest(value: unknown): RunpaneAgentDoctorRequest { if (!isRecord(value)) { throw new Error('Agent doctor request must be an object'); diff --git a/main/src/services/runpaneEventLog.ts b/main/src/services/runpaneEventLog.ts index 4da072d5..9914b476 100644 --- a/main/src/services/runpaneEventLog.ts +++ b/main/src/services/runpaneEventLog.ts @@ -67,6 +67,11 @@ export class RunpaneEventLog { }; } + panelIdsChangedSince(cursor: string): Set<string> | null { + const replay = this.replaySince(cursor); + return replay.ok ? new Set(replay.events.map(event => event.panelId)) : null; + } + private parseCursor(cursor: string): { epoch: string; n: number } | null { const separator = cursor.lastIndexOf(':'); if (separator <= 0) return null; diff --git a/main/src/services/runpaneInterstitials.test.ts b/main/src/services/runpaneInterstitials.test.ts new file mode 100644 index 00000000..09a452cb --- /dev/null +++ b/main/src/services/runpaneInterstitials.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { classifyRunpaneInterstitial } from './runpaneInterstitials'; + +describe('classifyRunpaneInterstitial', () => { + it('allowlists only the reversible Codex update skip', () => { + expect(classifyRunpaneInterstitial('Codex update available — Skip', 'codex', 'p1')).toMatchObject({ + disposition: 'allow', response: '2', kind: 'codex-update', + }); + }); + + it('evaluates directory trust before the allowlist', () => { + expect(classifyRunpaneInterstitial('Codex update available — Skip. Do you trust this directory?', 'codex', 'p1')).toMatchObject({ + disposition: 'deny', blocker: { kind: 'unknown' }, + }); + }); + + it.each([ + 'Authentication required', + 'Grant permission to continue', + 'This cannot be undone. Continue?', + 'Accept terms of service', + 'Enter payment information', + ])('denies consequential prompt: %s', (screen) => { + expect(classifyRunpaneInterstitial(screen, 'codex', 'p1').disposition).toBe('deny'); + }); + + it('stops on an unknown planning modal', () => { + expect(classifyRunpaneInterstitial('Planning suggestion: would you like to switch modes?', 'codex', 'p1').disposition).toBe('unknown'); + }); + + it('does not treat a passive MCP authentication banner as an interstitial', () => { + expect(classifyRunpaneInterstitial('4 MCP servers need authentication\n›', 'claude', 'p1').disposition).toBe('clear'); + }); +}); diff --git a/main/src/services/runpaneInterstitials.ts b/main/src/services/runpaneInterstitials.ts new file mode 100644 index 00000000..ae1dbebc --- /dev/null +++ b/main/src/services/runpaneInterstitials.ts @@ -0,0 +1,37 @@ +import type { RunpaneAgentId, RunpanePanelBlockedState } from '../../../shared/types/runpaneOrchestration'; + +export type RunpaneInterstitialClassification = + | { disposition: 'clear' } + | { disposition: 'allow'; kind: 'codex-update'; response: '2'; justification: string } + | { disposition: 'deny' | 'unknown'; blocker: RunpanePanelBlockedState }; + +const DENY_PATTERNS: Array<{ pattern: RegExp; message: string }> = [ + { pattern: /(?:trust|do you trust).*(?:directory|folder|workspace)|(?:directory|folder|workspace).*(?:trust|trusted)/i, message: 'Directory trust requires explicit human input.' }, + { pattern: /authentication required|authenticate to continue|authorization required|sign[ -]?in to continue|log[ -]?in to continue|enter (?:an? )?(?:api )?(?:key|token)/i, message: 'Authentication requires explicit human input.' }, + { pattern: /permission|grant access|allow access/i, message: 'A permission decision requires explicit human input.' }, + { pattern: /delete|destroy|overwrite|irreversible|cannot be undone/i, message: 'A destructive confirmation requires explicit human input.' }, + { pattern: /terms (?:of|and)|privacy policy|payment|purchase|billing/i, message: 'Terms or payment decisions require explicit human input.' }, +]; + +export function classifyRunpaneInterstitial( + text: string, + agentType: RunpaneAgentId | undefined, + panelId: string, +): RunpaneInterstitialClassification { + for (const denied of DENY_PATTERNS) { + if (denied.pattern.test(text)) return { + disposition: 'deny', + blocker: { kind: 'unknown', message: denied.message, suggestedCommand: `runpane panels screen --panel ${panelId} --json` }, + }; + } + if ((agentType === 'codex' || /codex/i.test(text)) && /update available/i.test(text) && /skip/i.test(text)) { + return { disposition: 'allow', kind: 'codex-update', response: '2', justification: 'Skipping an optional update is reversible and leaves the installed agent unchanged.' }; + } + if (/planning suggestion|press enter to continue|would you like to|(?:choose|select) (?:an?|one)|\[[yn]\/?[yn]?\]/i.test(text)) { + return { + disposition: 'unknown', + blocker: { kind: 'unknown', message: 'An unrecognized interactive prompt requires explicit input.', suggestedCommand: `runpane panels screen --panel ${panelId} --json` }, + }; + } + return { disposition: 'clear' }; +} diff --git a/main/src/services/terminalPanelManager.test.ts b/main/src/services/terminalPanelManager.test.ts index 9926d08a..868fa48e 100644 --- a/main/src/services/terminalPanelManager.test.ts +++ b/main/src/services/terminalPanelManager.test.ts @@ -358,6 +358,82 @@ describe('TerminalPanelManager semantic agent state', () => { disposeFlowControlRecord(terminal!.flowControl); }); + it('MF-1: CLI-ready callback does not blindly write a premarked create initial input', async () => { + vi.useFakeTimers(); + installRuntime(1_000); + const panel = createPanel({ + initialCommand: 'codex', + initialInput: '/do TM-x', + initialInputSubmitStrategy: 'codex-ctrl-enter', + initialInputSentAt: '2026-01-01T00:02:00.000Z', + isCliPanel: true, + isCliReady: false, + agentType: 'codex', + }); + vi.mocked(panelManager.getPanel).mockReturnValue(panel); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const spawnedPty = createTerminal({ outputBuffer: '' }).pty; + ptySpawn.mockReturnValue(spawnedPty); + + await manager.initializeTerminal(panel, process.cwd()); + const terminal = manager.terminals.get(panel.id); + expect(terminal).toBeDefined(); + terminal!.pty.emitData('$ '); + await vi.advanceTimersByTimeAsync(50); + expect(terminal!.pty.write).toHaveBeenCalledWith('codex\r'); + terminal!.pty.write.mockClear(); + + terminal!.pty.emitData('Do you trust this directory?\n1. Yes\n2. No\n'); + await vi.advanceTimersByTimeAsync(1_000); + await flushPromises(); + + expect(terminal!.pty.write).not.toHaveBeenCalledWith('/do TM-x'); + expect(terminal!.pty.write).not.toHaveBeenCalledWith('\x1b[13;5u\r'); + expect(terminal!.pty.write).not.toHaveBeenCalled(); + disposeFlowControlRecord(terminal!.flowControl); + }); + + it('MF-7: delayed CLI-ready callback refuses RunPane create-owned initial input after readiness failure', async () => { + vi.useFakeTimers(); + installRuntime(1_000); + const panel = createPanel({ + initialCommand: 'codex', + initialInput: '/do TM-x', + initialInputSubmitStrategy: 'codex-ctrl-enter', + initialInputDeliveryOwner: 'runpane-create', + isCliPanel: true, + isCliReady: false, + agentType: 'codex', + }); + vi.mocked(panelManager.getPanel).mockReturnValue(panel); + vi.mocked(panelManager.updatePanel).mockResolvedValue(undefined); + const manager = new TerminalPanelManager() as unknown as SemanticStateAccess; + const spawnedPty = createTerminal({ outputBuffer: '' }).pty; + ptySpawn.mockReturnValue(spawnedPty); + + await manager.initializeTerminal(panel, process.cwd()); + const terminal = manager.terminals.get(panel.id); + expect(terminal).toBeDefined(); + terminal!.pty.emitData('$ '); + await vi.advanceTimersByTimeAsync(50); + expect(terminal!.pty.write).toHaveBeenCalledWith('codex\r'); + terminal!.pty.write.mockClear(); + + terminal!.pty.emitData('Do you trust this directory?\n1. Yes\n2. No\n'); + await vi.advanceTimersByTimeAsync(300); + await flushPromises(); + await vi.advanceTimersByTimeAsync(500); + await flushPromises(); + await vi.advanceTimersByTimeAsync(10_000); + await flushPromises(); + + expect(terminal!.pty.write).not.toHaveBeenCalledWith('/do TM-x'); + expect(terminal!.pty.write).not.toHaveBeenCalledWith('\x1b[13;5u\r'); + expect(terminal!.pty.write).not.toHaveBeenCalled(); + disposeFlowControlRecord(terminal!.flowControl); + }); + it('AC4 persists exited activity without an intermediate semantic idle and preserves the legacy idle edge', () => { vi.useFakeTimers(); const eventSink = installRuntime(5_000); diff --git a/main/src/services/terminalPanelManager.ts b/main/src/services/terminalPanelManager.ts index 77d2fe0a..e8f69dfd 100644 --- a/main/src/services/terminalPanelManager.ts +++ b/main/src/services/terminalPanelManager.ts @@ -360,7 +360,11 @@ export class TerminalPanelManager { const state = currentPanel.state; const customState = (state.customState || {}) as TerminalPanelState; - if (!customState.initialInput || customState.initialInputSentAt) { + if ( + !customState.initialInput || + customState.initialInputSentAt || + customState.initialInputDeliveryOwner === 'runpane-create' + ) { return null; } diff --git a/packages/runpane-py/src/runpane/cli.py b/packages/runpane-py/src/runpane/cli.py index 487a8b61..2031a195 100644 --- a/packages/runpane-py/src/runpane/cli.py +++ b/packages/runpane-py/src/runpane/cli.py @@ -30,7 +30,10 @@ run_panels_events, run_panels_watch, run_panels_await, + run_panels_await_any, run_panes_archive, + run_panes_status, + run_panes_watch, run_panes_create, run_panes_list, run_panes_pin, @@ -95,6 +98,8 @@ class ParsedArgs: repo: Optional[str] = None pane_id: Optional[str] = None panel_id: Optional[str] = None + pane_ids: List[str] = field(default_factory=list) + panel_ids: List[str] = field(default_factory=list) repo_path: Optional[str] = None name: Optional[str] = None worktree_name: Optional[str] = None @@ -125,6 +130,11 @@ class ParsedArgs: since: Optional[str] = None jsonl: bool = False heartbeat_ms: Optional[float] = None + changed_since: Optional[str] = None + include_future_panels: bool = False + start_agent: bool = False + wait_active: bool = False + handle_known_interstitials: Optional[str] = None help_topic: Optional[str] = None remote_setup_args: List[str] = field(default_factory=list) @@ -186,6 +196,10 @@ def dispatch_parsed_command(parsed: ParsedArgs, telemetry_context: WrapperTeleme return run_panes_pin(parsed, True) if parsed.command == "panes unpin": return run_panes_pin(parsed, False) + if parsed.command == "panes status": + return run_panes_status(parsed) + if parsed.command == "panes watch": + return run_panes_watch(parsed) if parsed.command == "panels list": return run_panels_list(parsed) if parsed.command == "panels create": @@ -208,6 +222,8 @@ def dispatch_parsed_command(parsed: ParsedArgs, telemetry_context: WrapperTeleme return run_panels_watch(parsed) if parsed.command == "panels await": return run_panels_await(parsed) + if parsed.command == "panels await-any": + return run_panels_await_any(parsed) if parsed.command == "agents doctor": return run_agents_doctor(parsed) if parsed.command in {"install", "update"}: @@ -395,10 +411,16 @@ def parse_args(argv: List[str]) -> ParsedArgs: parsed.target = "client" parse_flags(args, parsed) - if parsed.jsonl and parsed.command != "panels watch": - raise ValueError("--jsonl is only valid with panels watch.") + if parsed.jsonl and parsed.command not in {"panels watch", "panes watch"}: + raise ValueError("--jsonl is only valid with panels watch or panes watch.") if parsed.command == "panels await" and (not parsed.panel_id or not parsed.event_selector): raise ValueError("panels await requires --panel and --event.") + if parsed.command == "panels await-any" and (not parsed.panel_ids and not parsed.pane_ids or not parsed.event_selector): + raise ValueError("panels await-any requires at least one --panel or --pane and --event.") + if parsed.command == "panes watch" and not parsed.pane_ids: + raise ValueError("panes watch requires --pane.") + if parsed.command == "panes status" and not parsed.pane_id: + raise ValueError("panes status requires --pane.") return parsed @@ -497,6 +519,15 @@ def parse_local_boolean_flag(parsed: ParsedArgs, flag: str) -> None: if flag == "--jsonl": parsed.jsonl = True return + if flag == "--include-future-panels": + parsed.include_future_panels = True + return + if flag == "--start-agent": + parsed.start_agent = True + return + if flag == "--wait-active": + parsed.wait_active = True + return raise ValueError(f"Unknown option for {parsed.command}: {flag}") @@ -509,9 +540,11 @@ def parse_local_value_flag(parsed: ParsedArgs, flag: str, value: str) -> None: return if flag == "--pane": parsed.pane_id = value + parsed.pane_ids.append(value) return if flag == "--panel": parsed.panel_id = value + parsed.panel_ids.append(value) return if flag == "--path": parsed.repo_path = value @@ -623,6 +656,14 @@ def parse_local_value_flag(parsed: ParsedArgs, flag: str, value: str) -> None: if flag == "--since": parsed.since = value return + if flag == "--changed-since": + parsed.changed_since = value + return + if flag == "--handle-known-interstitials": + if value != "safe": + raise ValueError("--handle-known-interstitials must be safe.") + parsed.handle_known_interstitials = value + return if flag == "--heartbeat-ms": try: heartbeat_ms = float(value) @@ -645,6 +686,8 @@ def is_runpane_local_command(command: str) -> bool: "panes archive", "panes pin", "panes unpin", + "panes status", + "panes watch", "panels create", "panels list", "panels output", @@ -656,6 +699,7 @@ def is_runpane_local_command(command: str) -> bool: "panels events", "panels watch", "panels await", + "panels await-any", "agents doctor", } diff --git a/packages/runpane-py/src/runpane/generated_contract.py b/packages/runpane-py/src/runpane/generated_contract.py index d527a665..818ec44b 100644 --- a/packages/runpane-py/src/runpane/generated_contract.py +++ b/packages/runpane-py/src/runpane/generated_contract.py @@ -1,4 +1,4 @@ # Generated by scripts/generate-runpane-contract.js. Do not edit by hand. import json -RUNPANE_CONTRACT = json.loads("{\n \"$schema\": \"./schema.json\",\n \"schemaVersion\": 1,\n \"name\": \"runpane\",\n \"description\": \"Thin installer and local control CLI for Pane\",\n \"packageInstallPolicy\": [\n \"The packages must not download, install, or configure Pane during package installation.\",\n \"Work starts only when a user runs `runpane ...`.\"\n ],\n \"compatibility\": {\n \"node\": \">=18.17.0\",\n \"python\": \">=3.8\"\n },\n \"terminology\": {\n \"repo\": \"Saved Pane repository/project record\",\n \"pane\": \"User-visible Pane session\",\n \"tool\": \"Terminal-backed tab\",\n \"agent\": \"Built-in agent command template\"\n },\n \"defaults\": {\n \"target\": \"client\",\n \"paneVersion\": \"latest\",\n \"channel\": \"stable\",\n \"format\": \"auto\",\n \"dryRun\": false,\n \"yes\": false,\n \"verbose\": false\n },\n \"enums\": {\n \"installTargets\": [\n \"client\",\n \"daemon\"\n ],\n \"artifactFormats\": [\n \"auto\",\n \"appimage\",\n \"deb\",\n \"dmg\",\n \"zip\",\n \"exe\"\n ],\n \"channels\": [\n \"stable\",\n \"nightly\"\n ],\n \"agents\": [\n \"codex\",\n \"claude\"\n ],\n \"eventSelectors\": [\n \"panel-created\",\n \"terminal-ready\",\n \"prompt-staged\",\n \"prompt-submitted\",\n \"agent-active\",\n \"agent-idle\",\n \"input-required\",\n \"blocked\",\n \"unblocked\",\n \"panel-exited\",\n \"panel-archived\"\n ]\n },\n \"eventSelectorMap\": {\n \"panel-created\": \"panel_created\",\n \"terminal-ready\": \"terminal_ready\",\n \"prompt-staged\": \"prompt_staged\",\n \"prompt-submitted\": \"prompt_submitted\",\n \"agent-active\": \"agent_active\",\n \"agent-idle\": \"agent_idle\",\n \"input-required\": \"input_required\",\n \"blocked\": \"blocked\",\n \"unblocked\": \"unblocked\",\n \"panel-exited\": \"panel_exited\",\n \"panel-archived\": \"panel_archived\"\n },\n \"agentTemplates\": {\n \"codex\": {\n \"title\": \"Codex\",\n \"command\": \"codex --yolo\",\n \"description\": \"Open a Codex terminal tab and allow the initial input to drive the agent.\"\n },\n \"claude\": {\n \"title\": \"Claude Code\",\n \"command\": \"claude --dangerously-skip-permissions\",\n \"description\": \"Open a Claude Code terminal tab and allow the initial input to drive the agent.\"\n }\n },\n \"commands\": [\n {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"usage\": [\n \"runpane help [command]\"\n ]\n },\n {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"usage\": [\n \"runpane setup\"\n ],\n \"interactiveEntrypoint\": true\n },\n {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"usage\": [\n \"runpane install [client|daemon] [options]\"\n ],\n \"defaultTarget\": \"client\",\n \"targets\": [\n \"client\",\n \"daemon\"\n ],\n \"unknownDaemonFlagsForwarded\": true\n },\n {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"usage\": [\n \"runpane update [options]\"\n ],\n \"target\": \"client\"\n },\n {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"usage\": [\n \"runpane version\",\n \"runpane --version\"\n ]\n },\n {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"usage\": [\n \"runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"usage\": [\n \"runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"usage\": [\n \"runpane agent-context [--json]\",\n \"runpane agent-context --command <command> [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"usage\": [\n \"runpane repos list [--json] [--pane-dir <path>]\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"usage\": [\n \"runpane repos add --path <path> [--name <name>] [--json] [--yes]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions in a saved repository.\",\n \"usage\": [\n \"runpane panes list [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"usage\": [\n \"runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [options]\",\n \"runpane panes create --from-json <path|-> [--yes] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"usage\": [\n \"runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"usage\": [\n \"runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready] --yes [--json]\",\n \"runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--focus|--no-focus] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"usage\": [\n \"runpane panels list --pane <pane-id> [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"usage\": [\n \"runpane panels output --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"usage\": [\n \"runpane panels screen --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"usage\": [\n \"runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"usage\": [\n \"runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"usage\": [\n \"runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"usage\": [\n \"runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ]\n },\n {\n \"name\": \"panels events\",\n \"summary\": \"Replay retained semantic panel events after a cursor.\",\n \"usage\": [\n \"runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]\"\n ],\n \"exitCodes\": {\n \"0\": \"success\",\n \"1\": \"transport or other error\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"panelEventsResult\"\n ]\n },\n {\n \"name\": \"panels watch\",\n \"summary\": \"Stream semantic panel events as compact JSONL.\",\n \"usage\": [\n \"runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]\"\n ],\n \"outputMode\": \"jsonl\",\n \"exitCodes\": {\n \"0\": \"clean stop\",\n \"1\": \"transport or other error\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"semanticEvent\",\n \"cursorExpiredError\"\n ]\n },\n {\n \"name\": \"panels await\",\n \"summary\": \"Wait for the first matching semantic panel event.\",\n \"usage\": [\n \"runpane panels await --panel <panel-id> --event <selector> [--since <cursor>] [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]\"\n ],\n \"exitCodes\": {\n \"0\": \"matched\",\n \"1\": \"transport or other error\",\n \"2\": \"timed out\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"panelAwaitResult\",\n \"cursorExpiredError\"\n ]\n }\n ],\n \"flags\": {\n \"wrapper\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"description\": \"Pane release to install or inspect.\"\n },\n {\n \"name\": \"--download-dir\",\n \"value\": \"<path>\",\n \"description\": \"Directory for downloaded Pane release artifacts.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"description\": \"Use or inspect an existing Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"description\": \"Pane release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"description\": \"Print the plan without downloading or installing.\"\n },\n {\n \"name\": \"--yes\",\n \"aliases\": [\n \"-y\"\n ],\n \"description\": \"Skip interactive prompts where possible.\"\n },\n {\n \"name\": \"--verbose\",\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"remoteValue\": [\n {\n \"name\": \"--label\",\n \"value\": \"<name>\"\n },\n {\n \"name\": \"--prefer-tunnel\",\n \"value\": \"<tailscale|ssh|manual|auto>\"\n },\n {\n \"name\": \"--channel\",\n \"value\": \"<stable|nightly>\"\n },\n {\n \"name\": \"--base-url\",\n \"value\": \"<url>\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\"\n },\n {\n \"name\": \"--listen-port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--repo-ref\",\n \"value\": \"<ref>\"\n }\n ],\n \"remoteBoolean\": [\n {\n \"name\": \"--auto-listen-port\"\n },\n {\n \"name\": \"--interactive-tailscale-setup\"\n },\n {\n \"name\": \"--no-install-service\"\n },\n {\n \"name\": \"--no-tailscale-serve\"\n },\n {\n \"name\": \"--print-only\"\n }\n ],\n \"localValue\": [\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"description\": \"Connect to a Pane daemon using this Pane data directory.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"description\": \"Pane/session id to inspect.\"\n },\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"description\": \"Tool panel id to inspect or control.\"\n },\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"description\": \"Name for the registered repository or created pane/session.\"\n },\n {\n \"name\": \"--worktree-name\",\n \"value\": \"<name>\",\n \"description\": \"Worktree name to request. Defaults to --name.\"\n },\n {\n \"name\": \"--base-branch\",\n \"value\": \"<branch>\",\n \"description\": \"Base branch for the created worktree.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"description\": \"Terminal tab title. Defaults to the selected agent title or Terminal.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"aliases\": [\n \"--prompt\"\n ],\n \"description\": \"Text to send to the terminal after the command is ready. --prompt is an alias.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"description\": \"Read a full panes.create request JSON payload from a file or stdin.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Maximum time to wait for each pane creation job.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Readiness wait timeout for panes create --wait-ready.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"description\": \"Accepted for forward compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"description\": \"Maximum recent output lines or records to read.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"description\": \"Panel wait condition.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"description\": \"Text to wait for with panels wait --for text.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Polling interval for panels wait.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"description\": \"Semantic event selector in kebab-case.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"description\": \"Replay events strictly after this cursor.\"\n },\n {\n \"name\": \"--heartbeat-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Periodic event-log and state reconciliation interval.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"description\": \"Text bytes to send to a terminal panel.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read panel input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"description\": \"Identify whether a local-control mutation is user-initiated or agent/orchestrator-initiated.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"description\": \"Composer submit key sequence strategy for panels submit-composer.\"\n }\n ],\n \"localBoolean\": [\n {\n \"name\": \"--json\",\n \"description\": \"Print machine-readable JSON output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"description\": \"Wait for created terminal panels to be ready before returning.\"\n },\n {\n \"name\": \"--no-focus\",\n \"description\": \"Create the pane or panel in the background without changing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"description\": \"Explicitly focus the created pane or panel.\"\n },\n {\n \"name\": \"--pinned\",\n \"description\": \"Create the pane already pinned (the UI's favorite/pin star).\"\n },\n {\n \"name\": \"--force\",\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n },\n {\n \"name\": \"--jsonl\",\n \"description\": \"Print one compact JSON object per line (panels watch always uses JSONL).\"\n }\n ]\n },\n \"help\": {\n \"npm\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z> Pane release to install\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path> Use an existing Pane executable\",\n \" --dry-run Print the plan without downloading\",\n \" --yes Skip interactive prompts where possible\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [options]\",\n \"\",\n \"Updates Pane using the same artifact selection as \\\"runpane install client\\\".\",\n \"\",\n \"Options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json Print a machine-readable environment report\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --pane-path <path> Inspect a specific Pane executable\",\n \" --format <format> Release artifact format to inspect\",\n \" --verbose Print extra diagnostics\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json Print machine-readable output\",\n \" --pane-dir <path> Connect to a specific Pane data directory\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path> Existing git repository path\",\n \" --name <name> Saved repository name; defaults to the directory name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without adding the repo\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to archive\",\n \" --source <user|agent> Mark mutation source\",\n \" --force Archive even if the pane has uncommitted, untracked, or unpushed changes\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to pin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without pinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to unpin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without unpinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads bounded recent terminal output from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Tool panel id\",\n \" --limit <count> Maximum recent output lines or records to read\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text bytes to send\",\n \" --input-file <path|-> Read input from a file or stdin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command> Print the full definition for one runpane command\",\n \" --json Print machine-readable output\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels events\": [\n \"Usage:\",\n \" runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]\",\n \"\",\n \"Replays retained semantic events strictly after a cursor.\",\n \"\",\n \"Exit codes: 0 success, 3 cursor expired, 1 transport/error.\"\n ],\n \"panels watch\": [\n \"Usage:\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]\",\n \"\",\n \"Streams one compact semantic event JSON object per stdout line.\",\n \"\",\n \"Exit codes: 0 clean stop, 3 cursor expired, 1 transport/error.\"\n ],\n \"panels await\": [\n \"Usage:\",\n \" runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]\",\n \"\",\n \"Waits for a matching semantic event and periodically reconciles state.\",\n \"\",\n \"Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport/error.\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n },\n \"pip\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" pipx run runpane install client\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \" uvx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [--version <latest|vX.Y.Z>] [--dry-run] [--yes]\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\",\n \" --pane-path <path>\",\n \" --format <format>\",\n \" --verbose\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path>\",\n \" --name <name>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --source <user|agent>\",\n \" --force\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads recent terminal output records from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --limit <count>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --text <text>\",\n \" --input-file <path|->\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command>\",\n \" --json\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels events\": [\n \"Usage:\",\n \" python -m runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]\",\n \"\",\n \"Replays retained semantic events strictly after a cursor.\"\n ],\n \"panels watch\": [\n \"Usage:\",\n \" python -m runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]\",\n \"\",\n \"Streams one compact semantic event JSON object per stdout line.\"\n ],\n \"panels await\": [\n \"Usage:\",\n \" python -m runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]\",\n \"\",\n \"Waits for a matching semantic event and periodically reconciles state.\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" python -m runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n }\n },\n \"docs\": {\n \"maintainerRules\": [\n \"Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\",\n \"Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\",\n \"The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\",\n \"Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\",\n \"Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\",\n \"`pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\",\n \"Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\"\n ],\n \"recommendedQuickStarts\": [\n \"npx --yes runpane@latest\",\n \"npm i -g runpane && runpane setup\",\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"npmCommands\": [\n \"npx --yes runpane@latest\",\n \"npx --yes runpane@latest setup\",\n \"npx --yes runpane@latest install client\",\n \"npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \"pnpm dlx runpane@latest\",\n \"pnpm dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"npm i -g runpane && runpane\",\n \"npm i -g runpane && runpane setup\",\n \"npm i -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"pnpm add -g runpane && runpane\",\n \"pnpm add -g runpane && runpane setup\",\n \"pnpm add -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"yarn dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"bunx runpane@latest install daemon --label \\\"My Server\\\"\"\n ],\n \"pythonCommands\": [\n \"pipx run runpane\",\n \"pipx run runpane setup\",\n \"python -m pip install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"python -m runpane setup\",\n \"runpane install daemon --label \\\"My Server\\\"\",\n \"pipx install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"uvx runpane@latest\",\n \"uvx runpane@latest setup\",\n \"uvx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"python -m runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"packageManagerNotes\": [\n \"Use `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\",\n \"Do not document `pnpm install runpane` as the public CLI install path.\"\n ],\n \"commandUsages\": [\n \"runpane\",\n \"runpane setup\",\n \"runpane install\",\n \"runpane install client\",\n \"runpane install daemon\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane doctor --json\",\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\",\n \"runpane repos list --json\",\n \"runpane repos add --path /path/to/repo --name Pane --yes --json\",\n \"runpane panes list --repo active --json\",\n \"runpane panes create --repo active --name issue-252 --agent <agent> --prompt \\\"Kick off the discussion skill for issue 252\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panels list --pane <pane-id> --json\",\n \"runpane panels output --panel <panel-id> --limit 200 --json\",\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes --json\",\n \"runpane help\",\n \"runpane <command> --help\"\n ],\n \"commandDescriptions\": [\n \"`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. In non-interactive shells or CI, both forms must print help, common commands, and agent discovery hints, then exit successfully instead of waiting for input.\",\n \"`runpane install` is an alias for `runpane install client`.\",\n \"`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\",\n \"`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\",\n \"The wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\",\n \"`runpane update` uses the same release resolution and installer path as `install client`.\",\n \"`runpane version` prints only wrapper package metadata and does not contact, launch, or focus the Pane app or daemon.\",\n \"`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, daemon reachability, and remote-daemon hints. Add `--json` for a machine-readable report that agents should run before mutating Pane state. Installed app version detection must be best-effort and must not launch, focus, or configure Pane; macOS wrappers read app bundle metadata instead of executing `Pane --version`.\",\n \"`runpane agent-context` prints a brief, token-efficient command schema for coding agents without connecting to the Pane daemon.\",\n \"`runpane agent-context --command \\\"panes create\\\"` prints the detailed definition for one command. Add `--json` for machine-readable output.\",\n \"`runpane repos list` connects to the running local Pane daemon and prints saved repository records.\",\n \"`runpane repos add` registers an existing git repository with the running local Pane daemon. It does not create directories or initialize git repositories by default.\",\n \"`runpane panes list` lists Pane sessions, optionally scoped to one saved repository.\",\n \"`runpane panes create` connects to the running local Pane daemon, resolves the requested saved base repository, creates user-visible Pane sessions backed by Pane-managed worktrees/branches, opens terminal-backed tool tabs, and optionally sends initial input to the started tool. Built-in agent panes and `--source agent` default to background/no-focus unless `--focus` is passed.\",\n \"For `panes create --wait-ready`, `initialInput.verifiedSubmitted: true` is reported only after argument attachment or composer-clear plus activity evidence. Routing input does not by itself verify submission.\",\n \"`runpane panes archive` archives a Pane exactly like the UI Archive action, including removal of its Pane-managed git worktree, and refuses (unless `--force`) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes. It waits for worktree removal to finish before returning and reports the outcome in `worktreeCleanup`.\",\n \"`runpane panels list` lists tool panels inside one Pane session.\",\n \"`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.\",\n \"`runpane panels input` sends exact input bytes to one terminal panel. Prefer `--input-file` for newlines, Ctrl-C, quotes, or shell-sensitive text.\",\n \"`runpane panes create --prompt` is an alias for `--initial-input`; request JSON and daemon payloads should use the canonical `initialInput` field.\",\n \"If composer submission cannot be verified without risking a duplicate, the create item is unsuccessful with `initialInput.staged`, `initialInput.attempts`, `initialInput.blocked.kind: submission_unverified`, and an actionable `nextCommand`. The CLI-facing `--prompt` alias maps to this canonical `initialInput` result.\",\n \"When running from WSL while Pane is installed on Windows, the Linux wrapper may look for a missing `/tmp/pane-daemon.../daemon.sock` or resolve to a Windows shim such as Volta. In that case invoke the Windows wrapper through PowerShell from a Windows cwd, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\"\n ],\n \"wrapperFlagNote\": \"The top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\",\n \"localControlFlagNote\": \"`runpane doctor --json`, `runpane repos list`, `runpane panes list`, `runpane panes create`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues.\",\n \"daemonFlagNote\": \"Unknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\",\n \"downloadAttribution\": [\n \"The npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\",\n \"The PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\",\n \"Wrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\",\n \"If the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\",\n \"Wrappers also emit best-effort lifecycle telemetry to `https://runpane.com/api/runpane/telemetry` for command start/success/failure, download request/success/failure, and GitHub fallback usage.\",\n \"Wrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so distinct wrapper users can be counted with `count(DISTINCT properties.install_id)`.\",\n \"Wrapper telemetry must be disabled in CI and when `RUNPANE_TELEMETRY_DISABLED` is set, and must not include raw paths, labels, prompts, raw error text, or environment values.\"\n ],\n \"publishingCredentials\": [\n \"Local implementation, build, and dry-run validation do not need npm or PyPI API tokens.\",\n \"Release publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\",\n \"Fallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\"\n ]\n },\n \"testFixtures\": {\n \"parserSamples\": [\n [\n \"setup\"\n ],\n [\n \"install\"\n ],\n [\n \"install\",\n \"client\",\n \"--version\",\n \"v2.2.8\",\n \"--format\",\n \"dmg\",\n \"--download-dir\",\n \"/tmp/pane-downloads\",\n \"--dry-run\",\n \"--yes\"\n ],\n [\n \"install\",\n \"daemon\",\n \"--label\",\n \"VM\",\n \"--prefer-tunnel\",\n \"ssh\",\n \"--channel\",\n \"nightly\",\n \"--base-url\",\n \"https://example.test\",\n \"--pane-dir\",\n \"/tmp/pane\",\n \"--listen-port\",\n \"4555\",\n \"--auto-listen-port\",\n \"--print-only\",\n \"--repo-ref\",\n \"main\",\n \"--unknown-future-flag\",\n \"future-value\",\n \"--dry-run\",\n \"--verbose\"\n ],\n [\n \"update\",\n \"--version\",\n \"latest\",\n \"--format\",\n \"appimage\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--dry-run\"\n ],\n [\n \"doctor\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--format\",\n \"zip\",\n \"--verbose\"\n ],\n [\n \"doctor\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"agent-context\"\n ],\n [\n \"agent-context\",\n \"--command\",\n \"panes create\",\n \"--json\"\n ],\n [\n \"repos\",\n \"list\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"repos\",\n \"add\",\n \"--path\",\n \"/tmp/repo\",\n \"--name\",\n \"Repo\",\n \"--dry-run\",\n \"--yes\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"panes\",\n \"list\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--repo\",\n \"active\",\n \"--name\",\n \"issue-252\",\n \"--agent\",\n \"codex\",\n \"--prompt\",\n \"Kick off discussion\",\n \"--source\",\n \"agent\",\n \"--pinned\",\n \"--dry-run\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"archive\",\n \"--pane\",\n \"session-1\",\n \"--force\",\n \"--source\",\n \"agent\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"pin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"unpin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"list\",\n \"--pane\",\n \"session-1\",\n \"--json\"\n ],\n [\n \"panels\",\n \"output\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"200\",\n \"--json\"\n ],\n [\n \"panels\",\n \"input\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"Continue\\\\n\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"--version\"\n ],\n [\n \"agents\",\n \"doctor\",\n \"--agent\",\n \"codex\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--source\",\n \"agent\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"45000\",\n \"--concurrency\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"screen\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"80\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"wait\",\n \"--panel\",\n \"panel-1\",\n \"--for\",\n \"text\",\n \"--contains\",\n \"ready\",\n \"--timeout-ms\",\n \"30000\",\n \"--interval-ms\",\n \"500\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--source\",\n \"agent\",\n \"--no-focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"15000\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--focus\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit-composer\",\n \"--panel\",\n \"panel-1\",\n \"--strategy\",\n \"auto\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"events\",\n \"--panel\",\n \"panel-1\",\n \"--event\",\n \"agent-idle\",\n \"--since\",\n \"epoch:1\",\n \"--json\"\n ],\n [\n \"panels\",\n \"watch\",\n \"--panel\",\n \"panel-1\",\n \"--event\",\n \"blocked\",\n \"--since\",\n \"epoch:2\",\n \"--heartbeat-ms\",\n \"1000\",\n \"--jsonl\"\n ],\n [\n \"panels\",\n \"await\",\n \"--panel\",\n \"panel-1\",\n \"--event\",\n \"agent-idle\",\n \"--timeout-ms\",\n \"5000\",\n \"--heartbeat-ms\",\n \"500\",\n \"--json\"\n ]\n ],\n \"topLevelHelpIncludes\": [\n \"runpane setup\",\n \"runpane install\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane agent-context\",\n \"runpane repos list\",\n \"runpane repos add\",\n \"runpane panes list\",\n \"runpane panes create\",\n \"runpane panes archive\",\n \"runpane panels create\",\n \"runpane panels list\",\n \"runpane panels output\",\n \"runpane panels input\",\n \"runpane agents doctor\",\n \"runpane panels screen\",\n \"runpane panels submit\",\n \"runpane panels submit-composer\",\n \"runpane panels wait\",\n \"runpane panels events\",\n \"runpane panels watch\",\n \"runpane panels await\",\n \"runpane doctor --json\",\n \"runpane agent-context --json\",\n \"Agent discovery:\",\n \"Common commands:\"\n ],\n \"npmHelpIncludes\": [\n \"pnpm dlx runpane@latest\",\n \"npx --yes runpane@latest\"\n ],\n \"pipHelpIncludes\": [\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"installHelpIncludes\": [\n \"--version <latest|vX.Y.Z>\",\n \"--format <auto|appimage|deb|dmg|zip|exe>\",\n \"--download-dir <path>\",\n \"--pane-path <path>\",\n \"--label <name>\",\n \"--prefer-tunnel <tailscale|ssh|manual|auto>\",\n \"--repo-ref <ref>\"\n ]\n },\n \"jsonSchemas\": {\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"doctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"source\",\n \"wrapper\",\n \"release\",\n \"installedPane\",\n \"daemon\",\n \"remoteSetup\",\n \"nextCommands\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"npm\",\n \"pip\"\n ]\n },\n \"wrapper\": {\n \"type\": \"object\",\n \"required\": [\n \"runtime\",\n \"version\",\n \"paneDir\",\n \"endpoint\"\n ],\n \"properties\": {\n \"runtime\": {\n \"enum\": [\n \"node\",\n \"python\"\n ]\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"paneDir\": {\n \"type\": \"string\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"platform\": {\n \"type\": \"object\",\n \"properties\": {\n \"os\": {\n \"type\": \"string\"\n },\n \"arch\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"release\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"installedPane\": {\n \"type\": \"object\",\n \"required\": [\n \"found\"\n ],\n \"properties\": {\n \"found\": {\n \"type\": \"boolean\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"daemon\": {\n \"type\": \"object\",\n \"required\": [\n \"reachable\",\n \"endpoint\"\n ],\n \"properties\": {\n \"reachable\": {\n \"type\": \"boolean\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"result\": {\n \"type\": \"object\"\n },\n \"error\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"remoteSetup\": {\n \"type\": \"object\",\n \"required\": [\n \"ready\",\n \"displayAvailable\",\n \"headlessEnvironmentApplied\",\n \"diagnostics\"\n ],\n \"properties\": {\n \"ready\": {\n \"type\": \"boolean\"\n },\n \"displayAvailable\": {\n \"type\": \"boolean\"\n },\n \"headlessEnvironmentApplied\": {\n \"type\": \"boolean\"\n },\n \"diagnostics\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"severity\",\n \"message\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"PANE_APPIMAGE_FUSE_MISSING\",\n \"PANE_ELECTRON_SANDBOX_ROOT\",\n \"PANE_ELECTRON_SANDBOX_UNAVAILABLE\",\n \"PANE_USER_SERVICE_UNAVAILABLE\"\n ]\n },\n \"severity\": {\n \"enum\": [\n \"warning\",\n \"error\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"recoveryCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommands\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repos\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repos\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"name\",\n \"path\",\n \"active\",\n \"sessionCount\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n },\n \"sessionCount\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"path\"\n ],\n \"properties\": {\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"created\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"created\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"preview\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"path\",\n \"alreadyExists\",\n \"wouldCreate\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"alreadyExists\": {\n \"type\": \"boolean\"\n },\n \"wouldCreate\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"repo\",\n \"panes\"\n ],\n \"properties\": {\n \"repo\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"const\": true\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panes\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"tool\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"worktreeName\": {\n \"type\": \"string\"\n },\n \"baseBranch\": {\n \"type\": \"string\"\n },\n \"sessionPrompt\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"agent\"\n ],\n \"properties\": {\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"command\"\n ],\n \"properties\": {\n \"command\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"timeoutMs\": {\n \"type\": \"number\"\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n },\n \"concurrency\": {\n \"type\": \"number\"\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repo\",\n \"items\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"name\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"sessionId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"type\": \"object\",\n \"required\": [\n \"delivered\",\n \"submitted\",\n \"inputBytes\"\n ],\n \"properties\": {\n \"delivered\": {\n \"type\": \"boolean\"\n },\n \"submitted\": {\n \"type\": \"boolean\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\",\n \"argument\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\",\n \"argument\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"staged\": {\n \"type\": \"boolean\"\n },\n \"attempts\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panes\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"panes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"paneId\",\n \"name\",\n \"status\",\n \"worktreePath\",\n \"repoId\",\n \"panelCount\",\n \"pinned\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"repoId\": {\n \"type\": \"number\"\n },\n \"repoName\": {\n \"type\": \"string\"\n },\n \"panelCount\": {\n \"type\": \"number\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"force\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panePinRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"panePinResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"const\": true\n },\n \"favoritePinnedAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveResult\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"archived\",\n \"forced\",\n \"worktreeCleanup\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"const\": true\n },\n \"forced\": {\n \"type\": \"boolean\"\n },\n \"worktreeCleanup\": {\n \"enum\": [\n \"completed\",\n \"failed\",\n \"timeout\",\n \"not-applicable\"\n ]\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"blocked\",\n \"nextCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"message\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"uncommitted-changes\",\n \"unpushed-commits\",\n \"uncommitted-and-unpushed\",\n \"status-unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panelListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panels\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panels\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"panelId\",\n \"paneId\",\n \"type\",\n \"title\",\n \"active\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"type\": \"string\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"position\": {\n \"type\": \"number\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActiveAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelOutputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"limit\",\n \"returnedCount\",\n \"hasMore\",\n \"outputs\",\n \"text\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"outputs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"type\",\n \"data\",\n \"timestamp\"\n ],\n \"properties\": {\n \"type\": {\n \"type\": \"string\"\n },\n \"data\": {},\n \"timestamp\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextBriefResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"summary\",\n \"rules\",\n \"tools\",\n \"detailCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"brief\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"rules\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"tools\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"arguments\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"detailCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextCommandResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"command\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"command\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"command\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"details\",\n \"arguments\",\n \"examples\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"details\": {\n \"type\": \"string\"\n },\n \"requiresPaneDaemon\": {\n \"type\": \"boolean\"\n },\n \"mutates\": {\n \"type\": \"boolean\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"examples\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"jsonSchemas\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"notes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"panelScreenResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"source\",\n \"limit\",\n \"returnedLineCount\",\n \"hasMore\",\n \"text\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedLineCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"$ref\": \"#/jsonSchemas/paneCreateResult/properties/items/items/oneOf/0/properties/initialInput\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"enter\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"enter\": {\n \"const\": \"cr\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"semanticEvent\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"cursor\",\n \"type\",\n \"at\",\n \"panelId\",\n \"state\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"cursor\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"enum\": [\n \"panel_created\",\n \"terminal_ready\",\n \"prompt_staged\",\n \"prompt_submitted\",\n \"agent_active\",\n \"agent_idle\",\n \"input_required\",\n \"blocked\",\n \"unblocked\",\n \"panel_exited\",\n \"panel_archived\"\n ]\n },\n \"at\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"state\": {\n \"$ref\": \"#/jsonSchemas/panelWaitResult/properties/state\"\n },\n \"resolvedBy\": {\n \"enum\": [\n \"event\",\n \"reconciliation\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"cursorExpiredError\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"earliestCursor\",\n \"reconcileCommand\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"cursor_expired\"\n ]\n },\n \"earliestCursor\": {\n \"type\": \"string\"\n },\n \"reconcileCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelEventsResult\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"events\",\n \"cursor\"\n ],\n \"properties\": {\n \"ok\": {\n \"enum\": [\n true\n ]\n },\n \"events\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/jsonSchemas/semanticEvent\"\n }\n },\n \"cursor\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"enum\": [\n false\n ]\n },\n \"error\": {\n \"$ref\": \"#/jsonSchemas/cursorExpiredError\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panelAwaitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"timedOut\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"matchedEvent\": {\n \"enum\": [\n \"panel_created\",\n \"terminal_ready\",\n \"prompt_staged\",\n \"prompt_submitted\",\n \"agent_active\",\n \"agent_idle\",\n \"input_required\",\n \"blocked\",\n \"unblocked\",\n \"panel_exited\",\n \"panel_archived\"\n ]\n },\n \"resolvedBy\": {\n \"enum\": [\n \"event\",\n \"reconciliation\"\n ]\n },\n \"event\": {\n \"$ref\": \"#/jsonSchemas/semanticEvent\"\n },\n \"state\": {\n \"$ref\": \"#/jsonSchemas/panelWaitResult/properties/state\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelWaitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\",\n \"screen\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"screen\": {\n \"type\": \"object\",\n \"required\": [\n \"source\",\n \"text\",\n \"hasMore\"\n ],\n \"properties\": {\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentDoctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"agent\",\n \"command\",\n \"available\",\n \"checks\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"environment\": {\n \"enum\": [\n \"wsl\",\n \"windows\",\n \"linux\",\n \"macos\"\n ]\n },\n \"available\": {\n \"type\": \"boolean\"\n },\n \"executablePath\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"checks\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"ok\",\n \"message\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"tool\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"const\": \"terminal\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panelId\",\n \"title\",\n \"active\",\n \"focused\",\n \"tool\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"strategy\": {\n \"enum\": [\n \"auto\",\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"strategy\",\n \"sequenceName\",\n \"verifiedSubmitted\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"agentContext\": {\n \"brief\": {\n \"title\": \"Pane agent context\",\n \"summary\": \"Pane lets a developer manage saved base repositories, user-visible Panes (Pane sessions) for feature/PR work, and terminal-backed panel tabs. A Pane is a visible workspace that normally maps to one Pane-managed git worktree and branch; a panel is a terminal tab inside one Pane and shares that Pane's worktree; an agent is a CLI process running inside a panel.\",\n \"rules\": [\n \"Start with `runpane doctor --json` to understand wrapper, platform, daemon reachability, and the next safe commands before mutating Pane state.\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"Happy path for any user request to use Pane/RunPane: run `runpane doctor --json`, read `runpane agent-context --json`, resolve or add the saved base repo, create the requested visible Pane with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --pinned --no-focus --wait-ready --yes --json` when it should stay visible (or omit `--pinned`, or use `--tool-command <command>` instead of `--agent <agent>`), then validate with `panels wait` or `panels screen`.\",\n \"Treat Pane as the user's visible cockpit for watching/co-driving work. Do not create Panes or panels for private delegation unless the user asked for visible Pane orchestration or the result should appear in the Pane app.\",\n \"Register the saved base repository once with `repos add`; do not register a pre-created worktree as a separate repo unless the user explicitly asks.\",\n \"Use `panes create` for separate visible Panes (Pane sessions) for feature/PR work. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` for reviewer/helper/clean-context tabs that should stay inside an existing Pane and share that Pane's worktree.\",\n \"For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"Skill routing: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for those behaviors, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the workflow map at https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png. For where the local cache and Pane Chat bootstrap live in Pane, reference https://github.com/dcouple/Pane/pull/291. Use GitHub reads against https://github.com/dcouple/skills/tree/main/parsa only to inspect or refresh referenced skill files; do not clone or install it unless the user asks.\",\n \"When an agent creates Panes or panels, pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when declaring a Pane that should remain in the UI's favorite/pin set.\",\n \"Use `runpane agent-context --json` for the full agent-facing CLI context, or `runpane agent-context --command <command> --json` for one detailed command definition.\",\n \"For `agent-context --command`, use canonical spaced names like `panes create`; copied forms like `panes.create` or `runpane panes create` are accepted too.\",\n \"Use `runpane repos list --json` to find the saved repository when unsure after doctor shows the daemon is reachable.\",\n \"If WSL cannot reach Pane or `runpane` resolves to a broken Windows shim, the user may be running Windows Pane; try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`.\",\n \"If the repository exists on disk but is not saved in Pane, use `runpane repos add --path <repo> --yes --json` before creating panes.\",\n \"Use `runpane agents doctor --agent <codex|claude> --repo <selector> --json` when agent availability differs across host, Windows, WSL, or repo environments.\",\n \"Use `runpane panes create --wait-ready` to create Panes and validate initial terminal readiness in one call.\",\n \"Use `runpane panels screen` for compact current state, `panels wait` for readiness/text checks, `panels submit` for ordinary Enter-submitted input, and `panels submit-composer --strategy auto` for agent composers.\",\n \"Use `runpane panels input` only when exact bytes are required, such as Ctrl-C or handcrafted terminal input.\",\n \"After creating Panes or sending terminal input, validate with `panels wait` or bounded `panels screen` before reporting success.\"\n ],\n \"detailCommand\": \"runpane agent-context --command <command> [--json]\",\n \"tools\": [\n {\n \"name\": \"doctor\",\n \"summary\": \"Report wrapper, platform, installed Pane, and daemon reachability before an agent mutates Pane state.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\",\n \"--pane-path <path>\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"arguments\": [\n \"--command <command>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Check whether Codex or Claude is available in the repo environment Pane will use.\",\n \"arguments\": [\n \"--agent <codex|claude>\",\n \"--repo <selector>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"arguments\": [\n \"--path <path>\",\n \"--name <name>\",\n \"--yes\",\n \"--json\",\n \"--dry-run\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--name <name>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--prompt <text>\",\n \"--initial-input-file <path|->\",\n \"--from-json <path|->\",\n \"--source <user|agent>\",\n \"--pinned\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms <ms>\",\n \"--concurrency <count>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--source <user|agent>\",\n \"--force\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--source <user|agent>\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text plus terminal Enter to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer with the correct key sequence, including Ctrl+Enter for Codex.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--strategy <auto|codex-ctrl-enter|enter>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for terminal initialized, ready, idle, or text state with compact output.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--for <condition>\",\n \"--contains <text>\",\n \"--timeout-ms <ms>\",\n \"--interval-ms <ms>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels events\",\n \"summary\": \"Replay semantic panel events after a cursor.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--event <selector>\",\n \"--since <cursor>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels watch\",\n \"summary\": \"Stream semantic panel events as JSONL.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--event <selector>\",\n \"--since <cursor>\",\n \"--heartbeat-ms <ms>\",\n \"--jsonl\"\n ]\n },\n {\n \"name\": \"panels await\",\n \"summary\": \"Wait for a matching semantic panel event.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--event <selector>\",\n \"--since <cursor>\",\n \"--timeout-ms <ms>\",\n \"--heartbeat-ms <ms>\",\n \"--json\"\n ]\n }\n ]\n },\n \"commands\": {\n \"help\": {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"details\": \"Use this when you need human-readable usage text for a command.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Optional command topic such as \\\"panes create\\\".\"\n }\n ],\n \"examples\": [\n \"runpane help\",\n \"runpane help panes create\"\n ],\n \"notes\": [\n \"Help text is generated from the runpane contract.\"\n ]\n },\n \"setup\": {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"details\": \"Use this for a human-driven Pane setup flow, not for unattended agent orchestration.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [],\n \"examples\": [\n \"runpane setup\"\n ],\n \"notes\": [\n \"In non-interactive shells, setup prints help and exits successfully.\"\n ]\n },\n \"install\": {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"details\": \"Installs or launches Pane release artifacts at command runtime. `install daemon` forwards remote setup flags to Pane.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"target\",\n \"value\": \"<client|daemon>\",\n \"required\": false,\n \"description\": \"Install the desktop client or configure a remote daemon host.\"\n },\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to install.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip interactive prompts where possible.\"\n }\n ],\n \"examples\": [\n \"runpane install client\",\n \"runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"notes\": [\n \"Package install itself is inert; work begins only when runpane is executed.\"\n ]\n },\n \"update\": {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"details\": \"Resolves and installs the selected Pane client release.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to update to.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Print the update plan without downloading.\"\n }\n ],\n \"examples\": [\n \"runpane update\",\n \"runpane update --version latest\"\n ],\n \"notes\": [\n \"Equivalent artifact selection to `runpane install client`.\"\n ]\n },\n \"version\": {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"details\": \"Use this to confirm which wrapper is available. App, daemon, and release diagnostics belong in doctor.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Ignored for version; retained only for parser compatibility.\"\n }\n ],\n \"examples\": [\n \"runpane version\",\n \"runpane --version\"\n ],\n \"notes\": []\n },\n \"doctor\": {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"details\": \"Use this first when an agent needs to understand whether Pane is installed, which wrapper/runtime is running, what daemon endpoint is expected, and whether the running Pane app is reachable. JSON mode should return a report even when the daemon is unreachable.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print a machine-readable environment report.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Inspect a specific Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<format>\",\n \"required\": false,\n \"description\": \"Release artifact format to inspect.\"\n },\n {\n \"name\": \"--verbose\",\n \"required\": false,\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"examples\": [\n \"runpane doctor --json\",\n \"runpane doctor\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ],\n \"notes\": [\n \"Agents should run `runpane doctor --json` before mutating Pane state.\",\n \"The JSON report includes daemon reachability as data; an unreachable daemon is not a reason to skip the rest of the report.\",\n \"Use `runpane agent-context --json` after doctor when full CLI context is needed.\"\n ]\n },\n \"agent-context\": {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"details\": \"Use this first when an agent needs to discover Pane primitives. It is local/offline and does not require a running Pane app.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Print the detailed definition for one command, for example \\\"panes create\\\".\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ],\n \"notes\": [\n \"Default output is brief so AGENTS.md can point here without bloating context.\",\n \"`--command` accepts canonical spaced names and common copied forms, including `panes.create` and `runpane panes create`.\"\n ]\n },\n \"repos list\": {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"details\": \"Use this to find the right Pane-managed repository before creating panes. If the repo is missing, use `repos add` first.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable repository records.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos list --json\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ],\n \"notes\": [\n \"Requires a running Pane app or daemon for the selected Pane data directory.\",\n \"If this fails from WSL with a missing `/tmp/pane-daemon.../daemon.sock`, `volta: command not found`, or a Windows shim error, the user may be running Windows Pane. Retry via `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\",\n \"When using PowerShell from WSL, set a Windows cwd such as `$env:TEMP` or `$env:USERPROFILE` before running runpane to avoid UNC cwd issues.\"\n ]\n },\n \"repos add\": {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"details\": \"Use this when the repository exists on disk but is not saved in Pane yet. It validates the path as a git repository.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"required\": true,\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": false,\n \"description\": \"Saved repository name; defaults to the directory name.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without adding the repo.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos add --path /path/to/repo --yes --json\"\n ],\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ],\n \"notes\": [\n \"It does not create directories or initialize git repositories by default.\",\n \"For a Windows Pane app managing WSL repositories, prefer a saved WSL repo from `repos list` when possible. Adding a brand-new WSL repo from Windows may require WSL-aware path handling.\"\n ]\n },\n \"panes list\": {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"details\": \"Use this after selecting a repository to find existing Pane sessions and their ids before inspecting panels.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panes list --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ],\n \"notes\": [\n \"Without --repo, this lists sessions across saved Pane repositories.\"\n ]\n },\n \"panes create\": {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"details\": \"Use this when the user wants separate visible Panes (Pane sessions) for feature or PR work. Select the saved base repository, choose a built-in agent or custom terminal command, and optionally send initial input after the tool starts. Pane creates and owns a git worktree/branch for each new Pane; do not pre-create a git worktree and register it as a separate repo unless the user explicitly asks. For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": true,\n \"description\": \"Pane/session name.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--prompt\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Alias for --initial-input; sends text after the command is ready.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read a full panes.create request JSON payload.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background/no-focus creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the pane in the background without stealing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created pane.\"\n },\n {\n \"name\": \"--pinned\",\n \"required\": false,\n \"description\": \"Create the pane already pinned (the Pane UI's favorite/pin star); does not imply focus.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait for each created terminal panel to be ready before returning.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness timeout per pane; defaults to 30000.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Accepted for compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n }\n ],\n \"examples\": [\n \"runpane panes create --repo active --name issue-257 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes create --repo active --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ],\n \"notes\": [\n \"At least one of --agent or --tool-command is required unless --from-json is used.\",\n \"`panes create` is for user-visible Pane orchestration, not the agent's default private delegation mechanism.\",\n \"Register the saved base repository once. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` instead when a reviewer/helper should share an existing Pane's worktree.\",\n \"Agent-created Panes should pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when the Pane should remain in the UI's favorite/pin set.\",\n \"The built-in agent templates come from the runpane contract; custom terminal commands can pass agent-specific flags when requested by the user.\",\n \"Use --initial-input-file for multi-line prompts or shell-sensitive initial input.\",\n \"With --wait-ready, verifiedSubmitted is earned from delivery evidence; routing initial input alone does not imply verified submission.\",\n \"If initialInput.blocked.kind is submission_unverified, do not submit again automatically. Inspect initialInput.staged and attempts, then run nextCommand to resolve the ambiguous composer state.\",\n \"When the JSON result includes nextCommand, run it to validate that the terminal produced output before reporting success.\",\n \"Multi-pane requests are created sequentially today. The --concurrency flag is accepted for compatibility, but agents should not rely on parallel creation.\",\n \"For POSIX or WSL command chaining, use a custom terminal command like `bash -lc 'cmd1 && cmd2 && cmd3'`.\",\n \"For Windows PowerShell command chaining, use a custom terminal command like `powershell -NoProfile -Command \\\"cmd1; if ($LASTEXITCODE) { exit $LASTEXITCODE }; cmd2\\\"`.\",\n \"From WSL with Windows Pane, invoke through PowerShell and select the saved WSL repo by name or id, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane panes create --repo \\\"WSL Pane\\\" --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json'`.\",\n \"Use --wait-ready when an agent needs to verify that an agent terminal started instead of only creating a pane.\",\n \"If readiness returns blocked, inspect blocked.suggestedCommand rather than guessing which prompt to answer.\"\n ]\n },\n \"panes archive\": {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"details\": \"Use this to close out a Pane once its PR has merged. Refuses to archive (unless --force) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes, so an agent cannot silently discard work. A merged and pushed branch has no unpushed commits, so it archives cleanly. Waits for worktree removal to finish before returning.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to archive.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; does not change archive safety behavior.\"\n },\n {\n \"name\": \"--force\",\n \"required\": false,\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panes archive --pane <pane-id> --force --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ],\n \"notes\": [\n \"If the result has ok:false with a blocked field, the pane was NOT archived; inspect blocked.code and rerun with --force if discarding the flagged work is intentional.\",\n \"A successful archive waits for the Pane-managed worktree to be removed before returning; check worktreeCleanup in the result for the final outcome.\",\n \"Archiving a main-repo Pane (no Pane-managed worktree) always succeeds immediately since nothing is deleted from disk.\"\n ]\n },\n \"panes pin\": {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to true without reading and toggling it. Repeating the command is idempotent and pinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to pin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes pin --pane <pane-id> --yes --json\",\n \"runpane panes pin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-pinned Pane pinned.\",\n \"Pinning does not focus the Pane.\"\n ]\n },\n \"panes unpin\": {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to false without reading and toggling it. Repeating the command is idempotent and unpinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to unpin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes unpin --pane <pane-id> --yes --json\",\n \"runpane panes unpin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-unpinned Pane unpinned.\",\n \"Unpinning does not focus the Pane.\"\n ]\n },\n \"panels list\": {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"details\": \"Use this to find the terminal panel id to inspect or control after creating or selecting a Pane session.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels list --pane <pane-id> --json\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ],\n \"notes\": [\n \"The ids returned here are stable inputs for `panels output` and `panels input`.\"\n ]\n },\n \"panels output\": {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"details\": \"Use this to inspect recent terminal output from a terminal-backed panel without loading the full history by default. Live terminal scrollback is returned as bounded recent lines; persisted output fallback is returned as bounded records. Terminal control noise is stripped before returning text.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Tool panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum recent output lines or records to read. Defaults to 200.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels output --panel <panel-id> --limit 200 --json\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ],\n \"notes\": [\n \"Default output is bounded to the latest 200 lines or records. Use a larger --limit only when hasMore is true and more history is needed.\",\n \"Use --json when an agent needs timestamps, record types, returnedCount, or hasMore.\",\n \"The output is intended to be agent-readable, but terminal prompts and shell echoes may still appear; use the newest relevant lines before concluding success.\",\n \"Prefer `panels screen` for a smaller current-state read before increasing output limits.\"\n ]\n },\n \"panels input\": {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"details\": \"Use this to answer prompts or continue an agent inside an existing Pane terminal panel. Input is byte-oriented; choose --input-file for exact control over newlines and control characters.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text bytes to send.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read input from a file or stdin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"printf '\\\\003' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"runpane panels input --panel <panel-id> --text \\\"simple text\\\" --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ],\n \"notes\": [\n \"Input is sent exactly as provided. Include a real newline byte when the terminal should receive Enter; across shells, `--input-file` is safer than `--text \\\"...\\\\n\\\"`.\",\n \"Use `--input-file -` or a temp file for multi-line input, quotes, Ctrl-C, or shell-sensitive text.\",\n \"If interrupting a running process, send Ctrl-C first, validate/read output, then send the next command in a separate `panels input` call so bytes are not dropped.\",\n \"After sending input, validate with `runpane panels output --panel <panel-id> --json` before reporting success.\",\n \"Runpane records action metadata and errors for observability, but should not log full input text by default.\",\n \"For ordinary text plus Enter, prefer `panels submit` so the terminal receives a CR Enter byte.\"\n ]\n },\n \"agents doctor\": {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"details\": \"Use this before creating Codex or Claude panes when PATH may differ between macOS/Linux/Windows/WSL or between the wrapper shell and Pane. The check runs through Pane project context, not the wrapper process.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": true,\n \"description\": \"Built-in agent command to diagnose.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Repository selector; defaults to the active Pane repo.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane agents doctor --agent <agent> --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ],\n \"notes\": [\n \"For WSL repos, install the agent inside the WSL distro Pane uses, not only on Windows.\",\n \"This diagnoses built-in agent templates only; custom commands should be validated by creating a pane and reading its screen.\"\n ]\n },\n \"panels screen\": {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"details\": \"Use this for token-safe current terminal state. It prefers active alternate-screen/TUI output, then live scrollback, then persisted output. Default output is bounded to 80 lines.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum lines to return; defaults to 80.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels screen --panel <panel-id> --limit 80 --json\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ],\n \"notes\": [\n \"Use this before `panels output` when an agent only needs the latest visible/current state.\",\n \"If hasMore is true and context is missing, rerun with a larger --limit or use `panels output`.\"\n ]\n },\n \"panels submit\": {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"details\": \"Use this for ordinary interactive submissions. The daemon normalizes a final LF or CRLF to CR, or appends CR if no newline is present. Exact byte workflows remain on `panels input`.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text to submit before Enter.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read text from a file or stdin before Enter.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit --panel <panel-id> --text \\\"2\\\" --yes --json\",\n \"printf \\\"echo hello\\\" | runpane panels submit --panel <panel-id> --input-file - --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ],\n \"notes\": [\n \"The response includes nextCommand for validation. Run it before reporting that the input worked.\",\n \"Use `panels input` for Ctrl-C, escape sequences, or any workflow requiring exact bytes.\"\n ]\n },\n \"panels wait\": {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"details\": \"Use this to validate asynchronous terminal behavior without pulling large scrollback. It returns brief readiness state, blocker hints, a compact screen, and a next command.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"required\": false,\n \"description\": \"Condition to wait for. Defaults to ready for CLI panels and idle otherwise.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text required for --for text; implies --for text when --for is omitted.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Wait timeout; defaults to 30000.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Polling interval; defaults to 500.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json\",\n \"runpane panels wait --panel <panel-id> --contains \\\"Ready\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ],\n \"notes\": [\n \"If blocked is present, do not assume success. Use blocked.suggestedCommand or inspect `panels screen`.\",\n \"The default timeout and screen are intentionally small for agent context safety.\"\n ]\n },\n \"panels events\": {\n \"name\": \"panels events\",\n \"summary\": \"Replay retained semantic panel events after a cursor.\",\n \"details\": \"Request/response pull surface for the bounded semantic event ring.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": false,\n \"description\": \"Optional panel filter.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional semantic event selector.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Exclusive replay cursor.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels events --since <cursor> --json\"\n ],\n \"jsonSchemas\": [\n \"panelEventsResult\"\n ],\n \"notes\": [\n \"Exit code 3 means cursor_expired; reconcile from the command in the error.\"\n ]\n },\n \"panels watch\": {\n \"name\": \"panels watch\",\n \"summary\": \"Stream semantic panel events as JSONL.\",\n \"details\": \"Uses replay then live delivery on one retained daemon connection with periodic reconciliation.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": false,\n \"description\": \"Optional panel filter.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional semantic event selector.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Exclusive replay cursor.\"\n },\n {\n \"name\": \"--heartbeat-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Reconciliation interval.\"\n },\n {\n \"name\": \"--jsonl\",\n \"required\": false,\n \"description\": \"Describes the always-JSONL output.\"\n }\n ],\n \"examples\": [\n \"runpane panels watch --panel <panel-id> --jsonl\"\n ],\n \"jsonSchemas\": [\n \"semanticEvent\",\n \"cursorExpiredError\"\n ],\n \"notes\": [\n \"Stdout contains compact JSON records only. Exit codes: 0 clean stop, 1 transport error, 3 cursor expired.\"\n ]\n },\n \"panels await\": {\n \"name\": \"panels await\",\n \"summary\": \"Wait for the first matching semantic event.\",\n \"details\": \"Exit-on-first-match sugar over watch with periodic state reconciliation.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Panel to await.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Semantic event selector.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Exclusive replay cursor.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Wait timeout.\"\n },\n {\n \"name\": \"--heartbeat-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Reconciliation interval.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels await --panel <panel-id> --event agent-idle --json\"\n ],\n \"jsonSchemas\": [\n \"panelAwaitResult\",\n \"cursorExpiredError\"\n ],\n \"notes\": [\n \"Exit codes: 0 matched, 1 transport error, 2 timed out, 3 cursor expired.\"\n ]\n },\n \"panels create\": {\n \"name\": \"panels create\",\n \"summary\": \"Create a reviewer/helper terminal tab inside an existing Pane.\",\n \"details\": \"Use this to add reviewer, helper, or clean-context tabs to an existing Pane. The new panel shares the existing Pane's worktree and branch; it does not create a separate Pane session. Use `panes create` instead when the user wants a separate visible Pane (Pane session) for feature/PR work. Agent/orchestrator-created panels should pass --source agent or --no-focus so the user stays in the implementation tab. The daemon initializes the terminal immediately and can wait for CLI readiness.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Existing Pane session id to add the panel to.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent command template to launch.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to launch instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"required\": false,\n \"description\": \"Panel title override.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Initial input to send after the tool starts.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the panel in the background without changing active panel.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created panel.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait until the terminal tool is ready.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness wait timeout.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panels create --pane <pane-id> --tool-command <command> --title <title> --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ],\n \"notes\": [\n \"Use this for same-pane reviewer loops after PR creation/testing.\",\n \"Panels share the existing Pane's worktree and branch.\",\n \"`panels create` is for visible helper/reviewer tabs, not the agent's default private delegation mechanism.\",\n \"For agent-created panels, prefer --source agent or --no-focus to avoid stealing focus from the user or implementation tab.\"\n ]\n },\n \"panels submit-composer\": {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"details\": \"Use this after sending text into an agent composer when plain Enter is not the right submit action. Agents should use --strategy auto; Pane owns per-agent submit sequences internally and verifies visible composer state when possible.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"required\": false,\n \"description\": \"Composer submit key sequence strategy.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit-composer --panel <codex-panel-id> --strategy auto --yes --json\",\n \"runpane panels submit-composer --panel <panel-id> --strategy enter --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ],\n \"notes\": [\n \"Use `panels input` or `panels submit` to write prompt text first; this command only submits the current composer.\",\n \"Use --strategy auto for agent workflows; explicit strategies are diagnostic escape hatches.\",\n \"The JSON result includes sequenceName and verifiedSubmitted. If ok is false, follow blocked.suggestedCommand instead of assuming submission happened.\"\n ]\n }\n },\n \"managedBlock\": [\n \"## Pane\",\n \"\",\n \"The developer is using Pane for this repository. Pane can manage saved repositories and create user-visible Panes with terminal-backed tools for planning, discussion, implementation, and review work.\",\n \"\",\n \"This managed guidance was created by [runpane.com](https://runpane.com) for the [Pane repository](https://github.com/dcouple/Pane). Do not delete or overwrite this block; the developer uses it for their workflow and it should remain committed to the main repository unless they explicitly ask to remove it.\",\n \"\",\n \"Pane mental model: a repository is the saved base repo; a Pane is a user-visible feature/PR workspace (Pane session) that normally maps to one Pane-managed git worktree and branch; a panel/tab is a terminal inside one Pane and shares that Pane's worktree; an agent is the CLI process running in a panel.\",\n \"\",\n \"Default happy path when the user asks you to use Pane or RunPane: run `runpane doctor --json`; read `runpane agent-context --json`; resolve the saved base repository with `runpane repos list --json` or add it once with `runpane repos add --path <repo> --yes --json`; create one visible Pane (Pane session) for the requested feature/PR with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json` or the equivalent `--tool-command <command>` form; then validate with `runpane panels wait` or `runpane panels screen` before reporting progress.\",\n \"\",\n \"Use Pane when the user wants visible Panes or co-drivable parallel feature/PR workspaces. Do not use Pane as your default private delegation mechanism; for private background decomposition, use your normal subagent/worktree workflow.\",\n \"\",\n \"Register the main/base repository once. Do not register pre-created git worktrees as separate Pane repositories unless the user explicitly asks.\",\n \"\",\n \"Use `runpane panes create` for separate visible Panes (Pane sessions) for feature/PR work. Use `runpane panels create` for reviewer/helper tabs inside an existing Pane that should share that Pane's worktree.\",\n \"\",\n \"Typical workflow: register the saved base repository once; create one Pane (Pane session) per feature/PR; use panels/tabs inside that Pane for helper or reviewer agents that should share the worktree; archive the Pane after the PR is done to remove it from active Panes and clean up its managed worktree when applicable.\",\n \"\",\n \"Skill routing reference: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for the behavior those words imply, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the [workflow map](https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png).\",\n \"Use those peer references together to choose the phase: discuss/investigate until the work is clear enough to delegate, then ticket/plan/implement/review/PR-test/teach-back as appropriate. The orchestrator and workflow map may point to different skills; reconcile them with the user's request instead of hardcoding a skill list or treating one reference as subordinate.\",\n \"For the Pane implementation source of truth for where the skill cache, cached workflow assets, and Pane Chat bootstrap live, reference [PR #291](https://github.com/dcouple/Pane/pull/291): `main/src/services/skillCacheManager.ts` owns `<PANE_DIR>/skills/`, `.sources/dcouple-skills`, and `pane-chat/runpane-orchestrator.md`; `main/src/services/paneChatManager.ts` owns the tiny bootstrap prompt that tells the selected Pane Chat agent to read that guide.\",\n \"Use GitHub reads against the [Parsa skills folder](https://github.com/dcouple/skills/tree/main/parsa) only to inspect or refresh referenced skill files; do not clone/install the repo unless the user asks.\",\n \"Do not hardcode a specific assistant brand in workflow guidance. Use the Pane agent or custom tool command the user selected, and use `runpane agents doctor --agent <agent> --repo <selector> --json` only when checking a built-in agent template.\",\n \"\",\n \"Start with `runpane doctor --json` before taking Pane actions. Use it to understand wrapper/runtime details, daemon reachability, and the next safe commands.\",\n \"\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22: `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"\",\n \"Use `runpane agent-context --json` for full Pane CLI context. Use `runpane agent-context --command \\\"panels wait\\\" --json` or another command name for detailed schema only when needed.\",\n \"\",\n \"Default to context-safe validation: after creating Panes or sending terminal input, run `runpane panels wait` or `runpane panels screen` before reporting success. Prefer `runpane panels submit` for normal text plus Enter; use `runpane panels input` only for exact bytes such as Ctrl-C or escape sequences.\",\n \"\",\n \"Common commands:\",\n \"- `runpane doctor --json`\",\n \"- `runpane agent-context --json`\",\n \"- `runpane repos list --json`\",\n \"- `runpane repos add --path <repo> --yes --json`\",\n \"- `runpane agents doctor --agent <agent> --repo active --json`\",\n \"- `runpane panes create --repo active --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels list --pane <pane-id> --json`\",\n \"- `runpane panels screen --panel <panel-id> --limit 80 --json`\",\n \"- `runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json`\",\n \"- `runpane panels submit --panel <panel-id> --text \\\"<answer>\\\" --yes --json`\",\n \"- `runpane panels input --panel <panel-id> --input-file <path|-> --yes --json`\",\n \"\",\n \"WSL note: if `runpane doctor --json` cannot find `/tmp/pane-daemon.../daemon.sock` or `runpane` resolves to a broken Windows shim, Pane may be running on Windows. Try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`, then create Panes through the same PowerShell form using the saved WSL repo name or id. Use `runpane agents doctor --agent <agent> --repo <selector> --json` to diagnose the repo environment Pane will actually use.\"\n ]\n }\n}") +RUNPANE_CONTRACT = json.loads("{\n \"$schema\": \"./schema.json\",\n \"schemaVersion\": 1,\n \"name\": \"runpane\",\n \"description\": \"Thin installer and local control CLI for Pane\",\n \"packageInstallPolicy\": [\n \"The packages must not download, install, or configure Pane during package installation.\",\n \"Work starts only when a user runs `runpane ...`.\"\n ],\n \"compatibility\": {\n \"node\": \">=18.17.0\",\n \"python\": \">=3.8\"\n },\n \"terminology\": {\n \"repo\": \"Saved Pane repository/project record\",\n \"pane\": \"User-visible Pane session\",\n \"tool\": \"Terminal-backed tab\",\n \"agent\": \"Built-in agent command template\"\n },\n \"defaults\": {\n \"target\": \"client\",\n \"paneVersion\": \"latest\",\n \"channel\": \"stable\",\n \"format\": \"auto\",\n \"dryRun\": false,\n \"yes\": false,\n \"verbose\": false\n },\n \"enums\": {\n \"installTargets\": [\n \"client\",\n \"daemon\"\n ],\n \"artifactFormats\": [\n \"auto\",\n \"appimage\",\n \"deb\",\n \"dmg\",\n \"zip\",\n \"exe\"\n ],\n \"channels\": [\n \"stable\",\n \"nightly\"\n ],\n \"agents\": [\n \"codex\",\n \"claude\"\n ],\n \"eventSelectors\": [\n \"panel-created\",\n \"terminal-ready\",\n \"prompt-staged\",\n \"prompt-submitted\",\n \"agent-active\",\n \"agent-idle\",\n \"input-required\",\n \"blocked\",\n \"unblocked\",\n \"panel-exited\",\n \"panel-archived\"\n ]\n },\n \"eventSelectorMap\": {\n \"panel-created\": \"panel_created\",\n \"terminal-ready\": \"terminal_ready\",\n \"prompt-staged\": \"prompt_staged\",\n \"prompt-submitted\": \"prompt_submitted\",\n \"agent-active\": \"agent_active\",\n \"agent-idle\": \"agent_idle\",\n \"input-required\": \"input_required\",\n \"blocked\": \"blocked\",\n \"unblocked\": \"unblocked\",\n \"panel-exited\": \"panel_exited\",\n \"panel-archived\": \"panel_archived\"\n },\n \"agentTemplates\": {\n \"codex\": {\n \"title\": \"Codex\",\n \"command\": \"codex --yolo\",\n \"description\": \"Open a Codex terminal tab and allow the initial input to drive the agent.\"\n },\n \"claude\": {\n \"title\": \"Claude Code\",\n \"command\": \"claude --dangerously-skip-permissions\",\n \"description\": \"Open a Claude Code terminal tab and allow the initial input to drive the agent.\"\n }\n },\n \"commands\": [\n {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"usage\": [\n \"runpane help [command]\"\n ]\n },\n {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"usage\": [\n \"runpane setup\"\n ],\n \"interactiveEntrypoint\": true\n },\n {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"usage\": [\n \"runpane install [client|daemon] [options]\"\n ],\n \"defaultTarget\": \"client\",\n \"targets\": [\n \"client\",\n \"daemon\"\n ],\n \"unknownDaemonFlagsForwarded\": true\n },\n {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"usage\": [\n \"runpane update [options]\"\n ],\n \"target\": \"client\"\n },\n {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"usage\": [\n \"runpane version\",\n \"runpane --version\"\n ]\n },\n {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"usage\": [\n \"runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"usage\": [\n \"runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"usage\": [\n \"runpane agent-context [--json]\",\n \"runpane agent-context --command <command> [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"usage\": [\n \"runpane repos list [--json] [--pane-dir <path>]\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"usage\": [\n \"runpane repos add --path <path> [--name <name>] [--json] [--yes]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions in a saved repository.\",\n \"usage\": [\n \"runpane panes list [--repo <selector>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"usage\": [\n \"runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [options]\",\n \"runpane panes create --from-json <path|-> [--yes] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"usage\": [\n \"runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star and repeated requests are idempotent.\",\n \"usage\": [\n \"runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ]\n },\n {\n \"name\": \"panes status\",\n \"summary\": \"Read a compact semantic-state snapshot for a Pane.\",\n \"usage\": [\n \"runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelStateSummary\"\n ]\n },\n {\n \"name\": \"panes watch\",\n \"summary\": \"Stream semantic events for panels in a Pane.\",\n \"usage\": [\n \"runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]\"\n ],\n \"outputMode\": \"jsonl\",\n \"exitCodes\": {\n \"0\": \"clean stop\",\n \"1\": \"transport or other error\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"semanticEvent\",\n \"cursorExpiredError\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"usage\": [\n \"runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready] --yes [--json]\",\n \"runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--focus|--no-focus] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"usage\": [\n \"runpane panels list --pane <pane-id> [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"usage\": [\n \"runpane panels output --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"usage\": [\n \"runpane panels screen --panel <panel-id> [--limit <count>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"usage\": [\n \"runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"usage\": [\n \"runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"usage\": [\n \"runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"usage\": [\n \"runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ]\n },\n {\n \"name\": \"panels events\",\n \"summary\": \"Replay retained semantic panel events after a cursor.\",\n \"usage\": [\n \"runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]\"\n ],\n \"exitCodes\": {\n \"0\": \"success\",\n \"1\": \"transport or other error\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"panelEventsResult\"\n ]\n },\n {\n \"name\": \"panels watch\",\n \"summary\": \"Stream semantic panel events as compact JSONL.\",\n \"usage\": [\n \"runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]\"\n ],\n \"outputMode\": \"jsonl\",\n \"exitCodes\": {\n \"0\": \"clean stop\",\n \"1\": \"transport or other error\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"semanticEvent\",\n \"cursorExpiredError\"\n ]\n },\n {\n \"name\": \"panels await\",\n \"summary\": \"Wait for the first matching semantic panel event.\",\n \"usage\": [\n \"runpane panels await --panel <panel-id> --event <selector> [--since <cursor>] [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]\"\n ],\n \"exitCodes\": {\n \"0\": \"matched\",\n \"1\": \"transport or other error\",\n \"2\": \"timed out\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"panelAwaitResult\",\n \"cursorExpiredError\"\n ]\n },\n {\n \"name\": \"panels await-any\",\n \"summary\": \"Wait for the first matching event across panels or Panes.\",\n \"usage\": [\n \"runpane panels await-any (--panel <panel-id>|--pane <pane-id>)... --event <selector> [--timeout-ms <ms>] [--json]\"\n ],\n \"exitCodes\": {\n \"0\": \"matched\",\n \"1\": \"transport or other error\",\n \"2\": \"timed out\",\n \"3\": \"cursor expired\"\n },\n \"jsonSchemas\": [\n \"panelAwaitResult\",\n \"cursorExpiredError\"\n ]\n }\n ],\n \"flags\": {\n \"wrapper\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"description\": \"Pane release to install or inspect.\"\n },\n {\n \"name\": \"--download-dir\",\n \"value\": \"<path>\",\n \"description\": \"Directory for downloaded Pane release artifacts.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"description\": \"Use or inspect an existing Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"description\": \"Pane release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"description\": \"Print the plan without downloading or installing.\"\n },\n {\n \"name\": \"--yes\",\n \"aliases\": [\n \"-y\"\n ],\n \"description\": \"Skip interactive prompts where possible.\"\n },\n {\n \"name\": \"--verbose\",\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"remoteValue\": [\n {\n \"name\": \"--label\",\n \"value\": \"<name>\"\n },\n {\n \"name\": \"--prefer-tunnel\",\n \"value\": \"<tailscale|ssh|manual|auto>\"\n },\n {\n \"name\": \"--channel\",\n \"value\": \"<stable|nightly>\"\n },\n {\n \"name\": \"--base-url\",\n \"value\": \"<url>\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\"\n },\n {\n \"name\": \"--listen-port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--repo-ref\",\n \"value\": \"<ref>\"\n }\n ],\n \"remoteBoolean\": [\n {\n \"name\": \"--auto-listen-port\"\n },\n {\n \"name\": \"--interactive-tailscale-setup\"\n },\n {\n \"name\": \"--no-install-service\"\n },\n {\n \"name\": \"--no-tailscale-serve\"\n },\n {\n \"name\": \"--print-only\"\n }\n ],\n \"localValue\": [\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"description\": \"Connect to a Pane daemon using this Pane data directory.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"repeatable\": true,\n \"description\": \"Pane/session id to inspect.\"\n },\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"repeatable\": true,\n \"description\": \"Tool panel id to inspect or control.\"\n },\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"description\": \"Name for the registered repository or created pane/session.\"\n },\n {\n \"name\": \"--worktree-name\",\n \"value\": \"<name>\",\n \"description\": \"Worktree name to request. Defaults to --name.\"\n },\n {\n \"name\": \"--base-branch\",\n \"value\": \"<branch>\",\n \"description\": \"Base branch for the created worktree.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"description\": \"Terminal tab title. Defaults to the selected agent title or Terminal.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"aliases\": [\n \"--prompt\"\n ],\n \"description\": \"Text to send to the terminal after the command is ready. --prompt is an alias.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"description\": \"Read a full panes.create request JSON payload from a file or stdin.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Maximum time to wait for each pane creation job.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Readiness wait timeout for panes create --wait-ready.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"description\": \"Accepted for forward compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"description\": \"Maximum recent output lines or records to read.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"description\": \"Panel wait condition.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"description\": \"Text to wait for with panels wait --for text.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Polling interval for panels wait.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"description\": \"Semantic event selector in kebab-case.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"description\": \"Replay events strictly after this cursor.\"\n },\n {\n \"name\": \"--changed-since\",\n \"value\": \"<cursor>\",\n \"description\": \"Return panels with semantic transitions after this lifecycle cursor.\"\n },\n {\n \"name\": \"--heartbeat-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Periodic event-log and state reconciliation interval.\"\n },\n {\n \"name\": \"--handle-known-interstitials\",\n \"value\": \"<safe>\",\n \"description\": \"Opt in to the explicit reversible interstitial allowlist.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"description\": \"Text bytes to send to a terminal panel.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read panel input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"description\": \"Identify whether a local-control mutation is user-initiated or agent/orchestrator-initiated.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"description\": \"Composer submit key sequence strategy for panels submit-composer.\"\n }\n ],\n \"localBoolean\": [\n {\n \"name\": \"--json\",\n \"description\": \"Print machine-readable JSON output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"description\": \"Wait for created terminal panels to be ready before returning.\"\n },\n {\n \"name\": \"--no-focus\",\n \"description\": \"Create the pane or panel in the background without changing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"description\": \"Explicitly focus the created pane or panel.\"\n },\n {\n \"name\": \"--pinned\",\n \"description\": \"Create the pane already pinned (the UI's favorite/pin star).\"\n },\n {\n \"name\": \"--force\",\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n },\n {\n \"name\": \"--jsonl\",\n \"description\": \"Print one compact JSON object per line (panels watch always uses JSONL).\"\n },\n {\n \"name\": \"--include-future-panels\",\n \"description\": \"Keep watching panels created in the selected Pane after the stream starts.\"\n },\n {\n \"name\": \"--start-agent\",\n \"description\": \"Require verified initial prompt submission and active agent work.\"\n },\n {\n \"name\": \"--wait-active\",\n \"description\": \"Wait for agent activity after initial prompt delivery.\"\n }\n ]\n },\n \"help\": {\n \"npm\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z> Pane release to install\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path> Use an existing Pane executable\",\n \" --dry-run Print the plan without downloading\",\n \" --yes Skip interactive prompts where possible\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [options]\",\n \"\",\n \"Updates Pane using the same artifact selection as \\\"runpane install client\\\".\",\n \"\",\n \"Options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json Print a machine-readable environment report\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --pane-path <path> Inspect a specific Pane executable\",\n \" --format <format> Release artifact format to inspect\",\n \" --verbose Print extra diagnostics\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json Print machine-readable output\",\n \" --pane-dir <path> Connect to a specific Pane data directory\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path> Existing git repository path\",\n \" --name <name> Saved repository name; defaults to the directory name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without adding the repo\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to archive\",\n \" --source <user|agent> Mark mutation source\",\n \" --force Archive even if the pane has uncommitted, untracked, or unpushed changes\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to pin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without pinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id to unpin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without unpinning the pane\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Pane/session id\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads bounded recent terminal output from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Tool panel id\",\n \" --limit <count> Maximum recent output lines or records to read\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text bytes to send\",\n \" --input-file <path|-> Read input from a file or stdin\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command> Print the full definition for one runpane command\",\n \" --json Print machine-readable output\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels events\": [\n \"Usage:\",\n \" runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]\",\n \"\",\n \"Replays retained semantic events strictly after a cursor.\",\n \"\",\n \"Exit codes: 0 success, 3 cursor expired, 1 transport/error.\"\n ],\n \"panes status\": [\n \"Usage:\",\n \" runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]\",\n \"\",\n \"Returns compact per-panel semantic state and a race-free snapshot cursor.\"\n ],\n \"panes watch\": [\n \"Usage:\",\n \" runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]\",\n \"\",\n \"Streams semantic events for a Pane.\"\n ],\n \"panels watch\": [\n \"Usage:\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]\",\n \"\",\n \"Streams one compact semantic event JSON object per stdout line.\",\n \"\",\n \"Exit codes: 0 clean stop, 3 cursor expired, 1 transport/error.\"\n ],\n \"panels await\": [\n \"Usage:\",\n \" runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]\",\n \"\",\n \"Waits for a matching semantic event and periodically reconciles state.\",\n \"\",\n \"Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport/error.\"\n ],\n \"panels await-any\": [\n \"Usage:\",\n \" runpane panels await-any (--panel <id>|--pane <id>)... --event <selector> [--timeout-ms <ms>] [--json]\",\n \"\",\n \"Waits for the first matching event across all selected targets.\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n },\n \"pip\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] [--wait-ready]\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--source user|agent] [--focus|--no-focus] --yes\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" pipx run runpane install client\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \" uvx runpane@latest\",\n \"\",\n \"Common commands:\",\n \" runpane help\",\n \" runpane setup\",\n \" runpane install\",\n \" runpane doctor\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"<command>\\\" --json\",\n \"\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [--version <latest|vX.Y.Z>] [--dry-run] [--yes]\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--json] [--pane-dir <path>] [--pane-path <path>] [--format <format>] [--verbose]\",\n \"\",\n \"Checks wrapper/runtime details, release metadata, installed Pane detection, and Pane daemon reachability.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\",\n \" --pane-path <path>\",\n \" --format <format>\",\n \" --verbose\",\n \"\",\n \"Agent discovery:\",\n \" runpane doctor --json\",\n \" runpane agent-context --json\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path>\",\n \" --name <name>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes\": [\n \"Pane session commands.\",\n \"\",\n \"Usage:\",\n \" runpane panes <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes archive --pane <pane-id> [--force] [--source user|agent] --yes [--json]\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Run \\\"runpane help panes <command>\\\" for command-specific options.\"\n ],\n \"panes list\": [\n \"Usage:\",\n \" runpane panes list [--repo <selector>] [--json]\",\n \"\",\n \"Lists Pane sessions. Pass --repo to limit results to one saved repository.\",\n \"\",\n \"Options:\",\n \" --repo <selector>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates user-visible Panes (Pane sessions) for feature/PR work in a saved repository and opens a terminal-backed tool tab. Pane creates and owns the worktree/branch for each new Pane.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --wait-ready Wait for terminal readiness before returning\",\n \" --ready-timeout-ms <ms> Readiness wait timeout; defaults to 30000\",\n \" --concurrency <count> Accepted; creation is currently serialized\",\n \" --source <user|agent> Mark mutation source; agent implies background creation\",\n \" --no-focus Create in the background without stealing focus\",\n \" --focus Explicitly focus the created pane\",\n \" --pinned Create already pinned (the UI's favorite/pin star)\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes archive\": [\n \"Usage:\",\n \" runpane panes archive --pane <pane-id> [--source user|agent] [--force] --yes [--json]\",\n \"\",\n \"Archives a Pane (session) exactly like the UI Archive action, including removal of its Pane-managed git worktree. Refuses to archive a pane with uncommitted, untracked, or unpushed-to-remote changes unless --force is passed. Waits for worktree removal to finish before returning.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --source <user|agent>\",\n \" --force\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"panes pin\": [\n \"Usage:\",\n \" runpane panes pin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively pins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes unpin\": [\n \"Usage:\",\n \" runpane panes unpin --pane <pane-id> --yes [--dry-run] [--json]\",\n \"\",\n \"Declaratively unpins a Pane using the Pane UI's favorite/pin star. Repeating the command is idempotent and never changes focus. Use --dry-run to validate and preview without mutating.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panels\": [\n \"Terminal-backed panel commands.\",\n \"\",\n \"Usage:\",\n \" runpane panels <command> [options]\",\n \"\",\n \"Commands:\",\n \" runpane panels create --pane <pane-id> --agent <codex|claude> [options]\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \" runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--json]\",\n \" runpane panels events [--panel <panel-id>] [--since <cursor>] [--json]\",\n \" runpane panels watch [--panel <panel-id>] [--event <selector>] [--jsonl]\",\n \" runpane panels await --panel <panel-id> --event <selector> [--json]\",\n \"\",\n \"Run \\\"runpane help panels create\\\" or another command-specific topic for options.\"\n ],\n \"panels list\": [\n \"Usage:\",\n \" runpane panels list --pane <pane-id> [--json]\",\n \"\",\n \"Lists tool panels in a Pane session.\",\n \"\",\n \"Options:\",\n \" --pane <pane-id>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels output\": [\n \"Usage:\",\n \" runpane panels output --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads recent terminal output records from a panel.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --limit <count>\",\n \" --pane-dir <path>\",\n \" --json\"\n ],\n \"panels input\": [\n \"Usage:\",\n \" runpane panels input --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends exact input bytes to a terminal panel. Include a newline in the input when you mean Enter.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id>\",\n \" --text <text>\",\n \" --input-file <path|->\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --yes\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command>\",\n \" --json\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"agents doctor\": [\n \"Usage:\",\n \" runpane agents doctor --agent <codex|claude> [--repo <selector>] [--json]\",\n \"\",\n \"Diagnoses whether Codex or Claude is available in the same repository environment Pane will use.\",\n \"\",\n \"Options:\",\n \" --agent <codex|claude> Built-in agent command to diagnose\",\n \" --repo <selector> active, id, exact path, or saved repository name; defaults to active\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels screen\": [\n \"Usage:\",\n \" runpane panels screen --panel <panel-id> [--limit <count>] [--json]\",\n \"\",\n \"Reads a compact current-screen view from a terminal panel. Prefers alternate-screen/TUI output and falls back to recent scrollback.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --limit <count> Maximum lines to return; defaults to 80\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panels submit\": [\n \"Usage:\",\n \" runpane panels submit --panel <panel-id> (--text <text>|--input-file <path|->) --yes [--json]\",\n \"\",\n \"Sends text to a terminal panel and normalizes the final terminal Enter to CR. Use this for ordinary prompt answers and shell commands.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --text <text> Text to submit before Enter\",\n \" --input-file <path|-> Read text from a file or stdin before Enter\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --yes Skip confirmation for this mutating command\"\n ],\n \"panels wait\": [\n \"Usage:\",\n \" runpane panels wait --panel <panel-id> [--for initialized|ready|idle|text] [--contains <text>] [--timeout-ms <ms>] [--interval-ms <ms>] [--json]\",\n \"\",\n \"Polls a terminal panel until it is initialized, ready, idle, or contains text. Output is intentionally small and includes next-step guidance.\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id\",\n \" --for <condition> initialized, ready, idle, or text; defaults to ready for CLI panels and idle otherwise\",\n \" --contains <text> Text required for --for text; implies --for text when omitted\",\n \" --timeout-ms <milliseconds> Wait timeout; defaults to 30000\",\n \" --interval-ms <milliseconds> Poll interval; defaults to 500\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\"\n ],\n \"panes status\": [\n \"Usage:\",\n \" python -m runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]\",\n \"\",\n \"Returns compact per-panel semantic state and a race-free snapshot cursor.\"\n ],\n \"panes watch\": [\n \"Usage:\",\n \" python -m runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]\",\n \"\",\n \"Streams semantic events for a Pane.\"\n ],\n \"panels events\": [\n \"Usage:\",\n \" python -m runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]\",\n \"\",\n \"Replays retained semantic events strictly after a cursor.\"\n ],\n \"panels watch\": [\n \"Usage:\",\n \" python -m runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]\",\n \"\",\n \"Streams one compact semantic event JSON object per stdout line.\"\n ],\n \"panels await\": [\n \"Usage:\",\n \" python -m runpane panels await --panel <panel-id> --event <selector> [--timeout-ms <ms>] [--heartbeat-ms <ms>] [--json]\",\n \"\",\n \"Waits for a matching semantic event and periodically reconciles state.\"\n ],\n \"panels await-any\": [\n \"Usage:\",\n \" python -m runpane panels await-any (--panel <id>|--pane <id>)... --event <selector> [--timeout-ms <ms>] [--json]\",\n \"\",\n \"Waits for the first matching event across all selected targets.\"\n ],\n \"panels create\": [\n \"Create a terminal-backed tool panel inside an existing Pane session.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels create --pane <pane-id> --agent <codex|claude> [--title <title>] [--initial-input <text>] [--no-focus] [--wait-ready] --yes [--json]\",\n \" python -m runpane panels create --pane <pane-id> --tool-command <command> [--title <title>] [--no-focus] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --pane <pane-id> Existing Pane session to add the panel to.\",\n \" --agent <codex|claude> Built-in agent command template to launch.\",\n \" --tool-command <command> Custom terminal command to launch.\",\n \" --title <title> Panel title override.\",\n \" --initial-input, --prompt Initial input to send after the tool starts.\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin.\",\n \" --no-focus Create the panel in the background.\",\n \" --focus Explicitly focus the created panel.\",\n \" --source <user|agent> Mark the mutation source; agent implies background creation.\",\n \" --wait-ready Wait until the terminal tool is ready.\",\n \" --ready-timeout-ms <ms> Readiness wait timeout.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ],\n \"panels submit-composer\": [\n \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"\",\n \"Usage:\",\n \" python -m runpane panels submit-composer --panel <panel-id> [--strategy auto|codex-ctrl-enter|enter] --yes [--json]\",\n \"\",\n \"Options:\",\n \" --panel <panel-id> Terminal panel id.\",\n \" --strategy <strategy> Defaults to auto; Codex sends Ctrl+Enter and other panels send Enter.\",\n \" --yes Skip confirmation prompts.\",\n \" --json Print JSON output.\"\n ]\n }\n },\n \"docs\": {\n \"maintainerRules\": [\n \"Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\",\n \"Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\",\n \"The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\",\n \"Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\",\n \"Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\",\n \"`pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\",\n \"Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\"\n ],\n \"recommendedQuickStarts\": [\n \"npx --yes runpane@latest\",\n \"npm i -g runpane && runpane setup\",\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"npmCommands\": [\n \"npx --yes runpane@latest\",\n \"npx --yes runpane@latest setup\",\n \"npx --yes runpane@latest install client\",\n \"npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \"pnpm dlx runpane@latest\",\n \"pnpm dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"npm i -g runpane && runpane\",\n \"npm i -g runpane && runpane setup\",\n \"npm i -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"pnpm add -g runpane && runpane\",\n \"pnpm add -g runpane && runpane setup\",\n \"pnpm add -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"yarn dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"bunx runpane@latest install daemon --label \\\"My Server\\\"\"\n ],\n \"pythonCommands\": [\n \"pipx run runpane\",\n \"pipx run runpane setup\",\n \"python -m pip install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"python -m runpane setup\",\n \"runpane install daemon --label \\\"My Server\\\"\",\n \"pipx install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"uvx runpane@latest\",\n \"uvx runpane@latest setup\",\n \"uvx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"python -m runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"packageManagerNotes\": [\n \"Use `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\",\n \"Do not document `pnpm install runpane` as the public CLI install path.\"\n ],\n \"commandUsages\": [\n \"runpane\",\n \"runpane setup\",\n \"runpane install\",\n \"runpane install client\",\n \"runpane install daemon\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane doctor --json\",\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\",\n \"runpane repos list --json\",\n \"runpane repos add --path /path/to/repo --name Pane --yes --json\",\n \"runpane panes list --repo active --json\",\n \"runpane panes create --repo active --name issue-252 --agent <agent> --prompt \\\"Kick off the discussion skill for issue 252\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panels list --pane <pane-id> --json\",\n \"runpane panels output --panel <panel-id> --limit 200 --json\",\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes --json\",\n \"runpane help\",\n \"runpane <command> --help\"\n ],\n \"commandDescriptions\": [\n \"`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. In non-interactive shells or CI, both forms must print help, common commands, and agent discovery hints, then exit successfully instead of waiting for input.\",\n \"`runpane install` is an alias for `runpane install client`.\",\n \"`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\",\n \"`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\",\n \"The wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\",\n \"`runpane update` uses the same release resolution and installer path as `install client`.\",\n \"`runpane version` prints only wrapper package metadata and does not contact, launch, or focus the Pane app or daemon.\",\n \"`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, daemon reachability, and remote-daemon hints. Add `--json` for a machine-readable report that agents should run before mutating Pane state. Installed app version detection must be best-effort and must not launch, focus, or configure Pane; macOS wrappers read app bundle metadata instead of executing `Pane --version`.\",\n \"`runpane agent-context` prints a brief, token-efficient command schema for coding agents without connecting to the Pane daemon.\",\n \"`runpane agent-context --command \\\"panes create\\\"` prints the detailed definition for one command. Add `--json` for machine-readable output.\",\n \"`runpane repos list` connects to the running local Pane daemon and prints saved repository records.\",\n \"`runpane repos add` registers an existing git repository with the running local Pane daemon. It does not create directories or initialize git repositories by default.\",\n \"`runpane panes list` lists Pane sessions, optionally scoped to one saved repository.\",\n \"`runpane panes create` connects to the running local Pane daemon, resolves the requested saved base repository, creates user-visible Pane sessions backed by Pane-managed worktrees/branches, opens terminal-backed tool tabs, and optionally sends initial input to the started tool. Built-in agent panes and `--source agent` default to background/no-focus unless `--focus` is passed.\",\n \"For `panes create --wait-ready`, `initialInput.verifiedSubmitted: true` is reported only after argument attachment or composer-clear plus activity evidence. Routing input does not by itself verify submission.\",\n \"`runpane panes archive` archives a Pane exactly like the UI Archive action, including removal of its Pane-managed git worktree, and refuses (unless `--force`) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes. It waits for worktree removal to finish before returning and reports the outcome in `worktreeCleanup`.\",\n \"`runpane panels list` lists tool panels inside one Pane session.\",\n \"`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.\",\n \"`runpane panels input` sends exact input bytes to one terminal panel. Prefer `--input-file` for newlines, Ctrl-C, quotes, or shell-sensitive text.\",\n \"`runpane panes create --prompt` is an alias for `--initial-input`; request JSON and daemon payloads should use the canonical `initialInput` field.\",\n \"If composer submission cannot be verified without risking a duplicate, the create item is unsuccessful with `initialInput.staged`, `initialInput.attempts`, `initialInput.blocked.kind: submission_unverified`, and an actionable `nextCommand`. The CLI-facing `--prompt` alias maps to this canonical `initialInput` result.\",\n \"When running from WSL while Pane is installed on Windows, the Linux wrapper may look for a missing `/tmp/pane-daemon.../daemon.sock` or resolve to a Windows shim such as Volta. In that case invoke the Windows wrapper through PowerShell from a Windows cwd, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\"\n ],\n \"wrapperFlagNote\": \"The top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\",\n \"localControlFlagNote\": \"`runpane doctor --json`, `runpane repos list`, `runpane panes list`, `runpane panes create`, and `runpane panels ...` commands use or describe the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running. In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`. From WSL, if the user runs Windows Pane, call the Windows wrapper through `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane ...'` so the command can reach the Windows named-pipe daemon and avoid UNC cwd issues.\",\n \"daemonFlagNote\": \"Unknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\",\n \"downloadAttribution\": [\n \"The npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\",\n \"The PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\",\n \"Wrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\",\n \"If the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\",\n \"Wrappers also emit best-effort lifecycle telemetry to `https://runpane.com/api/runpane/telemetry` for command start/success/failure, download request/success/failure, and GitHub fallback usage.\",\n \"Wrapper telemetry uses a persisted anonymous `install_id` in the form `install_<uuid>` and PostHog `distinct_id = install:<install_id>` so distinct wrapper users can be counted with `count(DISTINCT properties.install_id)`.\",\n \"Wrapper telemetry must be disabled in CI and when `RUNPANE_TELEMETRY_DISABLED` is set, and must not include raw paths, labels, prompts, raw error text, or environment values.\"\n ],\n \"publishingCredentials\": [\n \"Local implementation, build, and dry-run validation do not need npm or PyPI API tokens.\",\n \"Release publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\",\n \"Fallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\"\n ]\n },\n \"testFixtures\": {\n \"parserSamples\": [\n [\n \"setup\"\n ],\n [\n \"install\"\n ],\n [\n \"install\",\n \"client\",\n \"--version\",\n \"v2.2.8\",\n \"--format\",\n \"dmg\",\n \"--download-dir\",\n \"/tmp/pane-downloads\",\n \"--dry-run\",\n \"--yes\"\n ],\n [\n \"install\",\n \"daemon\",\n \"--label\",\n \"VM\",\n \"--prefer-tunnel\",\n \"ssh\",\n \"--channel\",\n \"nightly\",\n \"--base-url\",\n \"https://example.test\",\n \"--pane-dir\",\n \"/tmp/pane\",\n \"--listen-port\",\n \"4555\",\n \"--auto-listen-port\",\n \"--print-only\",\n \"--repo-ref\",\n \"main\",\n \"--unknown-future-flag\",\n \"future-value\",\n \"--dry-run\",\n \"--verbose\"\n ],\n [\n \"update\",\n \"--version\",\n \"latest\",\n \"--format\",\n \"appimage\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--dry-run\"\n ],\n [\n \"doctor\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--format\",\n \"zip\",\n \"--verbose\"\n ],\n [\n \"doctor\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"agent-context\"\n ],\n [\n \"agent-context\",\n \"--command\",\n \"panes create\",\n \"--json\"\n ],\n [\n \"repos\",\n \"list\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"repos\",\n \"add\",\n \"--path\",\n \"/tmp/repo\",\n \"--name\",\n \"Repo\",\n \"--dry-run\",\n \"--yes\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"panes\",\n \"list\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--repo\",\n \"active\",\n \"--name\",\n \"issue-252\",\n \"--agent\",\n \"codex\",\n \"--prompt\",\n \"Kick off discussion\",\n \"--source\",\n \"agent\",\n \"--pinned\",\n \"--dry-run\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"archive\",\n \"--pane\",\n \"session-1\",\n \"--force\",\n \"--source\",\n \"agent\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"pin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"unpin\",\n \"--pane\",\n \"session-1\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"list\",\n \"--pane\",\n \"session-1\",\n \"--json\"\n ],\n [\n \"panels\",\n \"output\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"200\",\n \"--json\"\n ],\n [\n \"panels\",\n \"input\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"Continue\\\\n\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"--version\"\n ],\n [\n \"agents\",\n \"doctor\",\n \"--agent\",\n \"codex\",\n \"--repo\",\n \"active\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--source\",\n \"agent\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"45000\",\n \"--concurrency\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"screen\",\n \"--panel\",\n \"panel-1\",\n \"--limit\",\n \"80\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit\",\n \"--panel\",\n \"panel-1\",\n \"--text\",\n \"2\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"wait\",\n \"--panel\",\n \"panel-1\",\n \"--for\",\n \"text\",\n \"--contains\",\n \"ready\",\n \"--timeout-ms\",\n \"30000\",\n \"--interval-ms\",\n \"500\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--source\",\n \"agent\",\n \"--no-focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms\",\n \"15000\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"create\",\n \"--pane\",\n \"pane-1\",\n \"--agent\",\n \"codex\",\n \"--focus\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"submit-composer\",\n \"--panel\",\n \"panel-1\",\n \"--strategy\",\n \"auto\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panels\",\n \"events\",\n \"--panel\",\n \"panel-1\",\n \"--event\",\n \"agent-idle\",\n \"--since\",\n \"epoch:1\",\n \"--json\"\n ],\n [\n \"panels\",\n \"watch\",\n \"--panel\",\n \"panel-1\",\n \"--event\",\n \"blocked\",\n \"--since\",\n \"epoch:2\",\n \"--heartbeat-ms\",\n \"1000\",\n \"--jsonl\"\n ],\n [\n \"panels\",\n \"await\",\n \"--panel\",\n \"panel-1\",\n \"--event\",\n \"agent-idle\",\n \"--timeout-ms\",\n \"5000\",\n \"--heartbeat-ms\",\n \"500\",\n \"--json\"\n ],\n [\n \"panels\",\n \"await-any\",\n \"--panel\",\n \"panel-1\",\n \"--panel\",\n \"panel-2\",\n \"--pane\",\n \"pane-1\",\n \"--pane\",\n \"pane-2\",\n \"--event\",\n \"agent-idle\",\n \"--json\"\n ],\n [\n \"panes\",\n \"status\",\n \"--pane\",\n \"pane-1\",\n \"--changed-since\",\n \"epoch:4\",\n \"--json\"\n ],\n [\n \"panes\",\n \"watch\",\n \"--pane\",\n \"pane-1\",\n \"--include-future-panels\",\n \"--jsonl\"\n ]\n ],\n \"topLevelHelpIncludes\": [\n \"runpane setup\",\n \"runpane install\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane agent-context\",\n \"runpane repos list\",\n \"runpane repos add\",\n \"runpane panes list\",\n \"runpane panes create\",\n \"runpane panes archive\",\n \"runpane panels create\",\n \"runpane panels list\",\n \"runpane panels output\",\n \"runpane panels input\",\n \"runpane agents doctor\",\n \"runpane panels screen\",\n \"runpane panels submit\",\n \"runpane panels submit-composer\",\n \"runpane panels wait\",\n \"runpane panels events\",\n \"runpane panels watch\",\n \"runpane panels await\",\n \"runpane doctor --json\",\n \"runpane agent-context --json\",\n \"Agent discovery:\",\n \"Common commands:\"\n ],\n \"npmHelpIncludes\": [\n \"pnpm dlx runpane@latest\",\n \"npx --yes runpane@latest\"\n ],\n \"pipHelpIncludes\": [\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"installHelpIncludes\": [\n \"--version <latest|vX.Y.Z>\",\n \"--format <auto|appimage|deb|dmg|zip|exe>\",\n \"--download-dir <path>\",\n \"--pane-path <path>\",\n \"--label <name>\",\n \"--prefer-tunnel <tailscale|ssh|manual|auto>\",\n \"--repo-ref <ref>\"\n ]\n },\n \"jsonSchemas\": {\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"doctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"source\",\n \"wrapper\",\n \"release\",\n \"installedPane\",\n \"daemon\",\n \"remoteSetup\",\n \"nextCommands\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"npm\",\n \"pip\"\n ]\n },\n \"wrapper\": {\n \"type\": \"object\",\n \"required\": [\n \"runtime\",\n \"version\",\n \"paneDir\",\n \"endpoint\"\n ],\n \"properties\": {\n \"runtime\": {\n \"enum\": [\n \"node\",\n \"python\"\n ]\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"paneDir\": {\n \"type\": \"string\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"platform\": {\n \"type\": \"object\",\n \"properties\": {\n \"os\": {\n \"type\": \"string\"\n },\n \"arch\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"release\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"error\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": true\n },\n \"installedPane\": {\n \"type\": \"object\",\n \"required\": [\n \"found\"\n ],\n \"properties\": {\n \"found\": {\n \"type\": \"boolean\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"daemon\": {\n \"type\": \"object\",\n \"required\": [\n \"reachable\",\n \"endpoint\"\n ],\n \"properties\": {\n \"reachable\": {\n \"type\": \"boolean\"\n },\n \"endpoint\": {\n \"type\": \"object\",\n \"required\": [\n \"transport\",\n \"path\"\n ],\n \"properties\": {\n \"transport\": {\n \"enum\": [\n \"pipe\",\n \"unix\"\n ]\n },\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"result\": {\n \"type\": \"object\"\n },\n \"error\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"remoteSetup\": {\n \"type\": \"object\",\n \"required\": [\n \"ready\",\n \"displayAvailable\",\n \"headlessEnvironmentApplied\",\n \"diagnostics\"\n ],\n \"properties\": {\n \"ready\": {\n \"type\": \"boolean\"\n },\n \"displayAvailable\": {\n \"type\": \"boolean\"\n },\n \"headlessEnvironmentApplied\": {\n \"type\": \"boolean\"\n },\n \"diagnostics\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"severity\",\n \"message\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"PANE_APPIMAGE_FUSE_MISSING\",\n \"PANE_ELECTRON_SANDBOX_ROOT\",\n \"PANE_ELECTRON_SANDBOX_UNAVAILABLE\",\n \"PANE_USER_SERVICE_UNAVAILABLE\"\n ]\n },\n \"severity\": {\n \"enum\": [\n \"warning\",\n \"error\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"recoveryCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommands\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repos\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repos\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"name\",\n \"path\",\n \"active\",\n \"sessionCount\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n },\n \"sessionCount\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"path\"\n ],\n \"properties\": {\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"created\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"created\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"preview\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"path\",\n \"alreadyExists\",\n \"wouldCreate\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"alreadyExists\": {\n \"type\": \"boolean\"\n },\n \"wouldCreate\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"repo\",\n \"panes\"\n ],\n \"properties\": {\n \"repo\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"const\": true\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panes\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"tool\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"worktreeName\": {\n \"type\": \"string\"\n },\n \"baseBranch\": {\n \"type\": \"string\"\n },\n \"sessionPrompt\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"agent\"\n ],\n \"properties\": {\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"command\"\n ],\n \"properties\": {\n \"command\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"timeoutMs\": {\n \"type\": \"number\"\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n },\n \"concurrency\": {\n \"type\": \"number\"\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n },\n \"startAgent\": {\n \"type\": \"boolean\"\n },\n \"waitActive\": {\n \"type\": \"boolean\"\n },\n \"handleKnownInterstitials\": {\n \"enum\": [\n \"safe\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repo\",\n \"items\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"name\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"sessionId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n },\n \"handledInterstitials\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"response\"\n ],\n \"properties\": {\n \"kind\": {\n \"type\": \"string\"\n },\n \"response\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"type\": \"object\",\n \"required\": [\n \"delivered\",\n \"submitted\",\n \"inputBytes\"\n ],\n \"properties\": {\n \"delivered\": {\n \"type\": \"boolean\"\n },\n \"submitted\": {\n \"type\": \"boolean\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\",\n \"argument\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\",\n \"argument\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"staged\": {\n \"type\": \"boolean\"\n },\n \"attempts\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panes\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"panes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"paneId\",\n \"name\",\n \"status\",\n \"worktreePath\",\n \"repoId\",\n \"panelCount\",\n \"pinned\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"repoId\": {\n \"type\": \"number\"\n },\n \"repoName\": {\n \"type\": \"string\"\n },\n \"panelCount\": {\n \"type\": \"number\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"force\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panePinRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"panePinResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"pinned\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"pinned\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"const\": true\n },\n \"favoritePinnedAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"paneArchiveResult\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"archived\",\n \"forced\",\n \"worktreeCleanup\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"archived\": {\n \"const\": true\n },\n \"forced\": {\n \"type\": \"boolean\"\n },\n \"worktreeCleanup\": {\n \"enum\": [\n \"completed\",\n \"failed\",\n \"timeout\",\n \"not-applicable\"\n ]\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"blocked\",\n \"nextCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"message\",\n \"safetyCheck\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"uncommitted-changes\",\n \"unpushed-commits\",\n \"uncommitted-and-unpushed\",\n \"status-unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"safetyCheck\": {\n \"type\": \"object\",\n \"required\": [\n \"performed\"\n ],\n \"properties\": {\n \"performed\": {\n \"type\": \"boolean\"\n },\n \"hasUncommittedChanges\": {\n \"type\": \"boolean\"\n },\n \"hasUntrackedFiles\": {\n \"type\": \"boolean\"\n },\n \"hasUpstream\": {\n \"type\": \"boolean\"\n },\n \"unpushedCommits\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panelListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panels\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panels\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"panelId\",\n \"paneId\",\n \"type\",\n \"title\",\n \"active\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"type\": \"string\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"position\": {\n \"type\": \"number\"\n },\n \"createdAt\": {\n \"type\": \"string\"\n },\n \"lastActiveAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelOutputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"limit\",\n \"returnedCount\",\n \"hasMore\",\n \"outputs\",\n \"text\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"outputs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"type\",\n \"data\",\n \"timestamp\"\n ],\n \"properties\": {\n \"type\": {\n \"type\": \"string\"\n },\n \"data\": {},\n \"timestamp\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelInputResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextBriefResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"summary\",\n \"rules\",\n \"tools\",\n \"detailCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"brief\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"rules\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"tools\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"arguments\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"detailCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextCommandResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"command\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"command\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"command\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"details\",\n \"arguments\",\n \"examples\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"details\": {\n \"type\": \"string\"\n },\n \"requiresPaneDaemon\": {\n \"type\": \"boolean\"\n },\n \"mutates\": {\n \"type\": \"boolean\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"examples\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"jsonSchemas\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"notes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"panelScreenResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"source\",\n \"limit\",\n \"returnedLineCount\",\n \"hasMore\",\n \"text\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"limit\": {\n \"type\": \"number\"\n },\n \"returnedLineCount\": {\n \"type\": \"number\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"initialInput\": {\n \"$ref\": \"#/jsonSchemas/paneCreateResult/properties/items/items/oneOf/0/properties/initialInput\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\",\n \"input\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"input\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"enter\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"enter\": {\n \"const\": \"cr\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"semanticEvent\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"cursor\",\n \"type\",\n \"at\",\n \"panelId\",\n \"state\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"cursor\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"enum\": [\n \"panel_created\",\n \"terminal_ready\",\n \"prompt_staged\",\n \"prompt_submitted\",\n \"agent_active\",\n \"agent_idle\",\n \"input_required\",\n \"blocked\",\n \"unblocked\",\n \"panel_exited\",\n \"panel_archived\"\n ]\n },\n \"at\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"state\": {\n \"$ref\": \"#/jsonSchemas/panelWaitResult/properties/state\"\n },\n \"resolvedBy\": {\n \"enum\": [\n \"event\",\n \"reconciliation\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"cursorExpiredError\": {\n \"type\": \"object\",\n \"required\": [\n \"code\",\n \"earliestCursor\",\n \"reconcileCommand\"\n ],\n \"properties\": {\n \"code\": {\n \"enum\": [\n \"cursor_expired\"\n ]\n },\n \"earliestCursor\": {\n \"type\": \"string\"\n },\n \"reconcileCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelEventsResult\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"events\",\n \"cursor\"\n ],\n \"properties\": {\n \"ok\": {\n \"enum\": [\n true\n ]\n },\n \"events\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/jsonSchemas/semanticEvent\"\n }\n },\n \"cursor\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"enum\": [\n false\n ]\n },\n \"error\": {\n \"$ref\": \"#/jsonSchemas/cursorExpiredError\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panelAwaitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"timedOut\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"matchedEvent\": {\n \"enum\": [\n \"panel_created\",\n \"terminal_ready\",\n \"prompt_staged\",\n \"prompt_submitted\",\n \"agent_active\",\n \"agent_idle\",\n \"input_required\",\n \"blocked\",\n \"unblocked\",\n \"panel_exited\",\n \"panel_archived\"\n ]\n },\n \"resolvedBy\": {\n \"enum\": [\n \"event\",\n \"reconciliation\"\n ]\n },\n \"event\": {\n \"$ref\": \"#/jsonSchemas/semanticEvent\"\n },\n \"state\": {\n \"$ref\": \"#/jsonSchemas/panelWaitResult/properties/state\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelWaitResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\",\n \"screen\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n },\n \"terminalReady\": {\n \"type\": \"boolean\"\n },\n \"agentActivity\": {\n \"enum\": [\n \"unknown\",\n \"starting\",\n \"active\",\n \"idle\",\n \"exited\"\n ]\n },\n \"inputRequired\": {\n \"type\": \"boolean\"\n },\n \"blocked\": {\n \"type\": \"boolean\",\n \"description\": \"Whether any blocker is detected. This state boolean is distinct from the top-level blocked object, which carries structured blocker details.\"\n },\n \"hasNewOutput\": {\n \"type\": \"boolean\"\n },\n \"outputGeneration\": {\n \"type\": \"number\"\n },\n \"lastMeaningfulEventAt\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"description\": \"Structured blocker details. This object is distinct from state.blocked, which is only a boolean.\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"screen\": {\n \"type\": \"object\",\n \"required\": [\n \"source\",\n \"text\",\n \"hasMore\"\n ],\n \"properties\": {\n \"source\": {\n \"enum\": [\n \"alternateScreen\",\n \"scrollback\",\n \"persistedOutput\",\n \"empty\"\n ]\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"hasMore\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentDoctorResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"agent\",\n \"command\",\n \"available\",\n \"checks\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"environment\": {\n \"enum\": [\n \"wsl\",\n \"windows\",\n \"linux\",\n \"macos\"\n ]\n },\n \"available\": {\n \"type\": \"boolean\"\n },\n \"executablePath\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"checks\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"ok\",\n \"message\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"paneId\",\n \"tool\"\n ],\n \"properties\": {\n \"paneId\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"const\": \"terminal\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"noFocus\": {\n \"type\": \"boolean\"\n },\n \"focus\": {\n \"type\": \"boolean\"\n },\n \"source\": {\n \"enum\": [\n \"user\",\n \"agent\"\n ]\n },\n \"waitReady\": {\n \"type\": \"boolean\"\n },\n \"readyTimeoutMs\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"paneId\",\n \"panelId\",\n \"title\",\n \"active\",\n \"focused\",\n \"tool\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"focused\": {\n \"type\": \"boolean\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"readiness\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"condition\",\n \"matched\",\n \"timedOut\",\n \"elapsedMs\",\n \"state\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"condition\": {\n \"enum\": [\n \"initialized\",\n \"ready\",\n \"idle\",\n \"text\"\n ]\n },\n \"matched\": {\n \"type\": \"boolean\"\n },\n \"timedOut\": {\n \"type\": \"boolean\"\n },\n \"elapsedMs\": {\n \"type\": \"number\"\n },\n \"state\": {\n \"type\": \"object\",\n \"required\": [\n \"initialized\"\n ],\n \"properties\": {\n \"initialized\": {\n \"type\": \"boolean\"\n },\n \"isAlternateScreen\": {\n \"type\": \"boolean\"\n },\n \"activityStatus\": {\n \"enum\": [\n \"active\",\n \"idle\"\n ]\n },\n \"isCliReady\": {\n \"type\": \"boolean\"\n },\n \"isCliPanel\": {\n \"type\": \"boolean\"\n },\n \"agentType\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"lastActivity\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"panelId\"\n ],\n \"properties\": {\n \"panelId\": {\n \"type\": \"string\"\n },\n \"strategy\": {\n \"enum\": [\n \"auto\",\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"panelSubmitComposerResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"panelId\",\n \"inputBytes\",\n \"strategy\",\n \"sequenceName\",\n \"verifiedSubmitted\",\n \"sentAt\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"inputBytes\": {\n \"type\": \"number\"\n },\n \"strategy\": {\n \"enum\": [\n \"codex-ctrl-enter\",\n \"enter\"\n ]\n },\n \"sequenceName\": {\n \"enum\": [\n \"codex-ctrl-enter-cr\",\n \"enter-cr\"\n ]\n },\n \"verifiedSubmitted\": {\n \"type\": \"boolean\"\n },\n \"sentAt\": {\n \"type\": \"string\"\n },\n \"blocked\": {\n \"type\": \"object\",\n \"required\": [\n \"kind\",\n \"message\"\n ],\n \"properties\": {\n \"kind\": {\n \"enum\": [\n \"codex-update\",\n \"agent-prompt\",\n \"submission_unverified\",\n \"unknown\"\n ]\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"suggestedCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"nextCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"agentContext\": {\n \"brief\": {\n \"title\": \"Pane agent context\",\n \"summary\": \"Pane lets a developer manage saved base repositories, user-visible Panes (Pane sessions) for feature/PR work, and terminal-backed panel tabs. A Pane is a visible workspace that normally maps to one Pane-managed git worktree and branch; a panel is a terminal tab inside one Pane and shares that Pane's worktree; an agent is a CLI process running inside a panel.\",\n \"rules\": [\n \"Start with `runpane doctor --json` to understand wrapper, platform, daemon reachability, and the next safe commands before mutating Pane state.\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22, for example `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"Happy path for any user request to use Pane/RunPane: run `runpane doctor --json`, read `runpane agent-context --json`, resolve or add the saved base repo, create the requested visible Pane with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --pinned --no-focus --wait-ready --yes --json` when it should stay visible (or omit `--pinned`, or use `--tool-command <command>` instead of `--agent <agent>`), then validate with `panels wait` or `panels screen`.\",\n \"Treat Pane as the user's visible cockpit for watching/co-driving work. Do not create Panes or panels for private delegation unless the user asked for visible Pane orchestration or the result should appear in the Pane app.\",\n \"Register the saved base repository once with `repos add`; do not register a pre-created worktree as a separate repo unless the user explicitly asks.\",\n \"Use `panes create` for separate visible Panes (Pane sessions) for feature/PR work. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` for reviewer/helper/clean-context tabs that should stay inside an existing Pane and share that Pane's worktree.\",\n \"For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"Skill routing: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for those behaviors, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the workflow map at https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png. For where the local cache and Pane Chat bootstrap live in Pane, reference https://github.com/dcouple/Pane/pull/291. Use GitHub reads against https://github.com/dcouple/skills/tree/main/parsa only to inspect or refresh referenced skill files; do not clone or install it unless the user asks.\",\n \"When an agent creates Panes or panels, pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when declaring a Pane that should remain in the UI's favorite/pin set.\",\n \"Use `runpane agent-context --json` for the full agent-facing CLI context, or `runpane agent-context --command <command> --json` for one detailed command definition.\",\n \"For `agent-context --command`, use canonical spaced names like `panes create`; copied forms like `panes.create` or `runpane panes create` are accepted too.\",\n \"Use `runpane repos list --json` to find the saved repository when unsure after doctor shows the daemon is reachable.\",\n \"If WSL cannot reach Pane or `runpane` resolves to a broken Windows shim, the user may be running Windows Pane; try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`.\",\n \"If the repository exists on disk but is not saved in Pane, use `runpane repos add --path <repo> --yes --json` before creating panes.\",\n \"Use `runpane agents doctor --agent <codex|claude> --repo <selector> --json` when agent availability differs across host, Windows, WSL, or repo environments.\",\n \"Use `runpane panes create --wait-ready` to create Panes and validate initial terminal readiness in one call.\",\n \"Use `runpane panels screen` for compact current state, `panels wait` for readiness/text checks, `panels submit` for ordinary Enter-submitted input, and `panels submit-composer --strategy auto` for agent composers.\",\n \"Use `runpane panels input` only when exact bytes are required, such as Ctrl-C or handcrafted terminal input.\",\n \"After creating Panes or sending terminal input, validate with `panels wait` or bounded `panels screen` before reporting success.\"\n ],\n \"detailCommand\": \"runpane agent-context --command <command> [--json]\",\n \"tools\": [\n {\n \"name\": \"doctor\",\n \"summary\": \"Report wrapper, platform, installed Pane, and daemon reachability before an agent mutates Pane state.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\",\n \"--pane-path <path>\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"arguments\": [\n \"--command <command>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"agents doctor\",\n \"summary\": \"Check whether Codex or Claude is available in the repo environment Pane will use.\",\n \"arguments\": [\n \"--agent <codex|claude>\",\n \"--repo <selector>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"arguments\": [\n \"--path <path>\",\n \"--name <name>\",\n \"--yes\",\n \"--json\",\n \"--dry-run\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--name <name>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--prompt <text>\",\n \"--initial-input-file <path|->\",\n \"--from-json <path|->\",\n \"--source <user|agent>\",\n \"--pinned\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--ready-timeout-ms <ms>\",\n \"--concurrency <count>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--source <user|agent>\",\n \"--force\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane (the Pane UI's favorite/pin star) without changing focus.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--yes\",\n \"--dry-run\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels create\",\n \"summary\": \"Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--source <user|agent>\",\n \"--no-focus\",\n \"--focus\",\n \"--wait-ready\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"arguments\": [\n \"--pane <pane-id>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--limit <count>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit\",\n \"summary\": \"Send text plus terminal Enter to a terminal panel.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--text <text>\",\n \"--input-file <path|->\",\n \"--yes\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer with the correct key sequence, including Ctrl+Enter for Codex.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--strategy <auto|codex-ctrl-enter|enter>\",\n \"--yes\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for terminal initialized, ready, idle, or text state with compact output.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--for <condition>\",\n \"--contains <text>\",\n \"--timeout-ms <ms>\",\n \"--interval-ms <ms>\",\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panels events\",\n \"summary\": \"Replay semantic panel events after a cursor.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--event <selector>\",\n \"--since <cursor>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"panels watch\",\n \"summary\": \"Stream semantic panel events as JSONL.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--event <selector>\",\n \"--since <cursor>\",\n \"--heartbeat-ms <ms>\",\n \"--jsonl\"\n ]\n },\n {\n \"name\": \"panels await\",\n \"summary\": \"Wait for a matching semantic panel event.\",\n \"arguments\": [\n \"--panel <panel-id>\",\n \"--event <selector>\",\n \"--since <cursor>\",\n \"--timeout-ms <ms>\",\n \"--heartbeat-ms <ms>\",\n \"--json\"\n ]\n }\n ]\n },\n \"commands\": {\n \"help\": {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"details\": \"Use this when you need human-readable usage text for a command.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Optional command topic such as \\\"panes create\\\".\"\n }\n ],\n \"examples\": [\n \"runpane help\",\n \"runpane help panes create\"\n ],\n \"notes\": [\n \"Help text is generated from the runpane contract.\"\n ]\n },\n \"setup\": {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"details\": \"Use this for a human-driven Pane setup flow, not for unattended agent orchestration.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [],\n \"examples\": [\n \"runpane setup\"\n ],\n \"notes\": [\n \"In non-interactive shells, setup prints help and exits successfully.\"\n ]\n },\n \"install\": {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"details\": \"Installs or launches Pane release artifacts at command runtime. `install daemon` forwards remote setup flags to Pane.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"target\",\n \"value\": \"<client|daemon>\",\n \"required\": false,\n \"description\": \"Install the desktop client or configure a remote daemon host.\"\n },\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to install.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip interactive prompts where possible.\"\n }\n ],\n \"examples\": [\n \"runpane install client\",\n \"runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"notes\": [\n \"Package install itself is inert; work begins only when runpane is executed.\"\n ]\n },\n \"update\": {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"details\": \"Resolves and installs the selected Pane client release.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to update to.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Print the update plan without downloading.\"\n }\n ],\n \"examples\": [\n \"runpane update\",\n \"runpane update --version latest\"\n ],\n \"notes\": [\n \"Equivalent artifact selection to `runpane install client`.\"\n ]\n },\n \"version\": {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version without contacting, launching, or focusing Pane.\",\n \"details\": \"Use this to confirm which wrapper is available. App, daemon, and release diagnostics belong in doctor.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Ignored for version; retained only for parser compatibility.\"\n }\n ],\n \"examples\": [\n \"runpane version\",\n \"runpane --version\"\n ],\n \"notes\": []\n },\n \"doctor\": {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, daemon reachability, and remote setup diagnostics.\",\n \"details\": \"Use this first when an agent needs to understand whether Pane is installed, which wrapper/runtime is running, what daemon endpoint is expected, and whether the running Pane app is reachable. JSON mode should return a report even when the daemon is unreachable.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print a machine-readable environment report.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Inspect a specific Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<format>\",\n \"required\": false,\n \"description\": \"Release artifact format to inspect.\"\n },\n {\n \"name\": \"--verbose\",\n \"required\": false,\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"examples\": [\n \"runpane doctor --json\",\n \"runpane doctor\"\n ],\n \"jsonSchemas\": [\n \"doctorResult\"\n ],\n \"notes\": [\n \"Agents should run `runpane doctor --json` before mutating Pane state.\",\n \"The JSON report includes daemon reachability as data; an unreachable daemon is not a reason to skip the rest of the report.\",\n \"Use `runpane agent-context --json` after doctor when full CLI context is needed.\"\n ]\n },\n \"agent-context\": {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"details\": \"Use this first when an agent needs to discover Pane primitives. It is local/offline and does not require a running Pane app.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Print the detailed definition for one command, for example \\\"panes create\\\".\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ],\n \"notes\": [\n \"Default output is brief so AGENTS.md can point here without bloating context.\",\n \"`--command` accepts canonical spaced names and common copied forms, including `panes.create` and `runpane panes create`.\"\n ]\n },\n \"repos list\": {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"details\": \"Use this to find the right Pane-managed repository before creating panes. If the repo is missing, use `repos add` first.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable repository records.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos list --json\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ],\n \"notes\": [\n \"Requires a running Pane app or daemon for the selected Pane data directory.\",\n \"If this fails from WSL with a missing `/tmp/pane-daemon.../daemon.sock`, `volta: command not found`, or a Windows shim error, the user may be running Windows Pane. Retry via `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane repos list --json'`.\",\n \"When using PowerShell from WSL, set a Windows cwd such as `$env:TEMP` or `$env:USERPROFILE` before running runpane to avoid UNC cwd issues.\"\n ]\n },\n \"repos add\": {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"details\": \"Use this when the repository exists on disk but is not saved in Pane yet. It validates the path as a git repository.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"required\": true,\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": false,\n \"description\": \"Saved repository name; defaults to the directory name.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without adding the repo.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos add --path /path/to/repo --yes --json\"\n ],\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ],\n \"notes\": [\n \"It does not create directories or initialize git repositories by default.\",\n \"For a Windows Pane app managing WSL repositories, prefer a saved WSL repo from `repos list` when possible. Adding a brand-new WSL repo from Windows may require WSL-aware path handling.\"\n ]\n },\n \"panes list\": {\n \"name\": \"panes list\",\n \"summary\": \"List Pane sessions, optionally scoped to a saved repository.\",\n \"details\": \"Use this after selecting a repository to find existing Pane sessions and their ids before inspecting panels.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panes list --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"paneListResult\"\n ],\n \"notes\": [\n \"Without --repo, this lists sessions across saved Pane repositories.\"\n ]\n },\n \"panes create\": {\n \"name\": \"panes create\",\n \"summary\": \"Create user-visible Panes (Pane sessions) backed by Pane-managed worktrees for feature/PR work and open terminal-backed tool tabs.\",\n \"details\": \"Use this when the user wants separate visible Panes (Pane sessions) for feature or PR work. Select the saved base repository, choose a built-in agent or custom terminal command, and optionally send initial input after the tool starts. Pane creates and owns a git worktree/branch for each new Pane; do not pre-create a git worktree and register it as a separate repo unless the user explicitly asks. For private background decomposition, use your normal subagent/worktree mechanism instead of Pane.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": true,\n \"description\": \"Pane/session name.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--prompt\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Alias for --initial-input; sends text after the command is ready.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read a full panes.create request JSON payload.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background/no-focus creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the pane in the background without stealing focus.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created pane.\"\n },\n {\n \"name\": \"--pinned\",\n \"required\": false,\n \"description\": \"Create the pane already pinned (the Pane UI's favorite/pin star); does not imply focus.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait for each created terminal panel to be ready before returning.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness timeout per pane; defaults to 30000.\"\n },\n {\n \"name\": \"--concurrency\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Accepted for compatibility. Pane currently serializes multi-pane session creation so queued jobs do not time out before starting.\"\n }\n ],\n \"examples\": [\n \"runpane panes create --repo active --name issue-257 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane panes create --repo active --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ],\n \"notes\": [\n \"At least one of --agent or --tool-command is required unless --from-json is used.\",\n \"`panes create` is for user-visible Pane orchestration, not the agent's default private delegation mechanism.\",\n \"Register the saved base repository once. Pane creates and owns the worktree/branch for each new Pane.\",\n \"Use `panels create` instead when a reviewer/helper should share an existing Pane's worktree.\",\n \"Agent-created Panes should pass `--source agent --no-focus --wait-ready --yes --json` unless the user explicitly wants focus moved; add `--pinned` when the Pane should remain in the UI's favorite/pin set.\",\n \"The built-in agent templates come from the runpane contract; custom terminal commands can pass agent-specific flags when requested by the user.\",\n \"Use --initial-input-file for multi-line prompts or shell-sensitive initial input.\",\n \"With --wait-ready, verifiedSubmitted is earned from delivery evidence; routing initial input alone does not imply verified submission.\",\n \"If initialInput.blocked.kind is submission_unverified, do not submit again automatically. Inspect initialInput.staged and attempts, then run nextCommand to resolve the ambiguous composer state.\",\n \"When the JSON result includes nextCommand, run it to validate that the terminal produced output before reporting success.\",\n \"Multi-pane requests are created sequentially today. The --concurrency flag is accepted for compatibility, but agents should not rely on parallel creation.\",\n \"For POSIX or WSL command chaining, use a custom terminal command like `bash -lc 'cmd1 && cmd2 && cmd3'`.\",\n \"For Windows PowerShell command chaining, use a custom terminal command like `powershell -NoProfile -Command \\\"cmd1; if ($LASTEXITCODE) { exit $LASTEXITCODE }; cmd2\\\"`.\",\n \"From WSL with Windows Pane, invoke through PowerShell and select the saved WSL repo by name or id, for example `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane panes create --repo \\\"WSL Pane\\\" --name issue-123 --agent <agent> --prompt \\\"Plan this issue\\\" --source agent --no-focus --wait-ready --yes --json'`.\",\n \"Use --wait-ready when an agent needs to verify that an agent terminal started instead of only creating a pane.\",\n \"If readiness returns blocked, inspect blocked.suggestedCommand rather than guessing which prompt to answer.\"\n ]\n },\n \"panes archive\": {\n \"name\": \"panes archive\",\n \"summary\": \"Archive a Pane (session) exactly like the UI Archive action, including safe removal of its Pane-managed git worktree.\",\n \"details\": \"Use this to close out a Pane once its PR has merged. Refuses to archive (unless --force) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes, so an agent cannot silently discard work. A merged and pushed branch has no unpushed commits, so it archives cleanly. Waits for worktree removal to finish before returning.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to archive.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; does not change archive safety behavior.\"\n },\n {\n \"name\": \"--force\",\n \"required\": false,\n \"description\": \"Archive even if the pane's branch has uncommitted, untracked, or unpushed changes.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes archive --pane <pane-id> --source agent --yes --json\",\n \"runpane panes archive --pane <pane-id> --force --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneArchiveRequest\",\n \"paneArchiveResult\"\n ],\n \"notes\": [\n \"If the result has ok:false with a blocked field, the pane was NOT archived; inspect blocked.code and rerun with --force if discarding the flagged work is intentional.\",\n \"A successful archive waits for the Pane-managed worktree to be removed before returning; check worktreeCleanup in the result for the final outcome.\",\n \"Archiving a main-repo Pane (no Pane-managed worktree) always succeeds immediately since nothing is deleted from disk.\"\n ]\n },\n \"panes pin\": {\n \"name\": \"panes pin\",\n \"summary\": \"Declaratively pin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to true without reading and toggling it. Repeating the command is idempotent and pinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to pin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes pin --pane <pane-id> --yes --json\",\n \"runpane panes pin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-pinned Pane pinned.\",\n \"Pinning does not focus the Pane.\"\n ]\n },\n \"panes unpin\": {\n \"name\": \"panes unpin\",\n \"summary\": \"Declaratively unpin a Pane; pinned is the Pane UI's favorite/pin star.\",\n \"details\": \"Sets the requested pinned state to false without reading and toggling it. Repeating the command is idempotent and unpinning never changes focus.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id to unpin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without mutating.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes unpin --pane <pane-id> --yes --json\",\n \"runpane panes unpin --pane <pane-id> --dry-run --json\"\n ],\n \"jsonSchemas\": [\n \"panePinRequest\",\n \"panePinResult\"\n ],\n \"notes\": [\n \"This is a declarative set, not a toggle: retries leave an already-unpinned Pane unpinned.\",\n \"Unpinning does not focus the Pane.\"\n ]\n },\n \"panes status\": {\n \"name\": \"panes status\",\n \"summary\": \"Read semantic state for a Pane.\",\n \"details\": \"Returns a compact pull snapshot and lifecycle cursor.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane to inspect.\"\n },\n {\n \"name\": \"--changed-since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Only panels changed after this cursor.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes status --pane <pane-id> --json\"\n ],\n \"notes\": []\n },\n \"panes watch\": {\n \"name\": \"panes watch\",\n \"summary\": \"Stream semantic events for a Pane.\",\n \"details\": \"Filters the semantic event stream by Pane and can include panels created later.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane to watch; repeatable.\"\n },\n {\n \"name\": \"--include-future-panels\",\n \"required\": false,\n \"description\": \"Include panels created after watch starts.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional event filter.\"\n }\n ],\n \"examples\": [\n \"runpane panes watch --pane <pane-id> --include-future-panels --jsonl\"\n ],\n \"notes\": []\n },\n \"panels list\": {\n \"name\": \"panels list\",\n \"summary\": \"List tool panels inside a Pane session.\",\n \"details\": \"Use this to find the terminal panel id to inspect or control after creating or selecting a Pane session.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Pane/session id.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels list --pane <pane-id> --json\"\n ],\n \"jsonSchemas\": [\n \"panelListResult\"\n ],\n \"notes\": [\n \"The ids returned here are stable inputs for `panels output` and `panels input`.\"\n ]\n },\n \"panels output\": {\n \"name\": \"panels output\",\n \"summary\": \"Read recent terminal output from a panel.\",\n \"details\": \"Use this to inspect recent terminal output from a terminal-backed panel without loading the full history by default. Live terminal scrollback is returned as bounded recent lines; persisted output fallback is returned as bounded records. Terminal control noise is stripped before returning text.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Tool panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum recent output lines or records to read. Defaults to 200.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels output --panel <panel-id> --limit 200 --json\"\n ],\n \"jsonSchemas\": [\n \"panelOutputResult\"\n ],\n \"notes\": [\n \"Default output is bounded to the latest 200 lines or records. Use a larger --limit only when hasMore is true and more history is needed.\",\n \"Use --json when an agent needs timestamps, record types, returnedCount, or hasMore.\",\n \"The output is intended to be agent-readable, but terminal prompts and shell echoes may still appear; use the newest relevant lines before concluding success.\",\n \"Prefer `panels screen` for a smaller current-state read before increasing output limits.\"\n ]\n },\n \"panels input\": {\n \"name\": \"panels input\",\n \"summary\": \"Send input bytes to a terminal panel.\",\n \"details\": \"Use this to answer prompts or continue an agent inside an existing Pane terminal panel. Input is byte-oriented; choose --input-file for exact control over newlines and control characters.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text bytes to send.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read input from a file or stdin.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"printf 'Continue\\\\n' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"printf '\\\\003' | runpane panels input --panel <panel-id> --input-file - --yes\",\n \"runpane panels input --panel <panel-id> --text \\\"simple text\\\" --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelInputRequest\",\n \"panelInputResult\"\n ],\n \"notes\": [\n \"Input is sent exactly as provided. Include a real newline byte when the terminal should receive Enter; across shells, `--input-file` is safer than `--text \\\"...\\\\n\\\"`.\",\n \"Use `--input-file -` or a temp file for multi-line input, quotes, Ctrl-C, or shell-sensitive text.\",\n \"If interrupting a running process, send Ctrl-C first, validate/read output, then send the next command in a separate `panels input` call so bytes are not dropped.\",\n \"After sending input, validate with `runpane panels output --panel <panel-id> --json` before reporting success.\",\n \"Runpane records action metadata and errors for observability, but should not log full input text by default.\",\n \"For ordinary text plus Enter, prefer `panels submit` so the terminal receives a CR Enter byte.\"\n ]\n },\n \"agents doctor\": {\n \"name\": \"agents doctor\",\n \"summary\": \"Diagnose whether a built-in agent command is available in a Pane repository environment.\",\n \"details\": \"Use this before creating Codex or Claude panes when PATH may differ between macOS/Linux/Windows/WSL or between the wrapper shell and Pane. The check runs through Pane project context, not the wrapper process.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": true,\n \"description\": \"Built-in agent command to diagnose.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Repository selector; defaults to the active Pane repo.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane agents doctor --agent <agent> --repo active --json\"\n ],\n \"jsonSchemas\": [\n \"agentDoctorResult\"\n ],\n \"notes\": [\n \"For WSL repos, install the agent inside the WSL distro Pane uses, not only on Windows.\",\n \"This diagnoses built-in agent templates only; custom commands should be validated by creating a pane and reading its screen.\"\n ]\n },\n \"panels screen\": {\n \"name\": \"panels screen\",\n \"summary\": \"Read a compact current-screen view from a terminal panel.\",\n \"details\": \"Use this for token-safe current terminal state. It prefers active alternate-screen/TUI output, then live scrollback, then persisted output. Default output is bounded to 80 lines.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--limit\",\n \"value\": \"<count>\",\n \"required\": false,\n \"description\": \"Maximum lines to return; defaults to 80.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels screen --panel <panel-id> --limit 80 --json\"\n ],\n \"jsonSchemas\": [\n \"panelScreenResult\"\n ],\n \"notes\": [\n \"Use this before `panels output` when an agent only needs the latest visible/current state.\",\n \"If hasMore is true and context is missing, rerun with a larger --limit or use `panels output`.\"\n ]\n },\n \"panels submit\": {\n \"name\": \"panels submit\",\n \"summary\": \"Send text to a terminal panel and append a terminal Enter byte.\",\n \"details\": \"Use this for ordinary interactive submissions. The daemon normalizes a final LF or CRLF to CR, or appends CR if no newline is present. Exact byte workflows remain on `panels input`.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--text\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text to submit before Enter.\"\n },\n {\n \"name\": \"--input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read text from a file or stdin before Enter.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit --panel <panel-id> --text \\\"2\\\" --yes --json\",\n \"printf \\\"echo hello\\\" | runpane panels submit --panel <panel-id> --input-file - --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitRequest\",\n \"panelSubmitResult\"\n ],\n \"notes\": [\n \"The response includes nextCommand for validation. Run it before reporting that the input worked.\",\n \"Use `panels input` for Ctrl-C, escape sequences, or any workflow requiring exact bytes.\"\n ]\n },\n \"panels wait\": {\n \"name\": \"panels wait\",\n \"summary\": \"Wait for a terminal panel to initialize, become ready/idle, or contain text.\",\n \"details\": \"Use this to validate asynchronous terminal behavior without pulling large scrollback. It returns brief readiness state, blocker hints, a compact screen, and a next command.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--for\",\n \"value\": \"<initialized|ready|idle|text>\",\n \"required\": false,\n \"description\": \"Condition to wait for. Defaults to ready for CLI panels and idle otherwise.\"\n },\n {\n \"name\": \"--contains\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Text required for --for text; implies --for text when --for is omitted.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Wait timeout; defaults to 30000.\"\n },\n {\n \"name\": \"--interval-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Polling interval; defaults to 500.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json\",\n \"runpane panels wait --panel <panel-id> --contains \\\"Ready\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"panelWaitResult\"\n ],\n \"notes\": [\n \"If blocked is present, do not assume success. Use blocked.suggestedCommand or inspect `panels screen`.\",\n \"The default timeout and screen are intentionally small for agent context safety.\"\n ]\n },\n \"panels events\": {\n \"name\": \"panels events\",\n \"summary\": \"Replay retained semantic panel events after a cursor.\",\n \"details\": \"Request/response pull surface for the bounded semantic event ring.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": false,\n \"description\": \"Optional panel filter.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional semantic event selector.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Exclusive replay cursor.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels events --since <cursor> --json\"\n ],\n \"jsonSchemas\": [\n \"panelEventsResult\"\n ],\n \"notes\": [\n \"Exit code 3 means cursor_expired; reconcile from the command in the error.\"\n ]\n },\n \"panels watch\": {\n \"name\": \"panels watch\",\n \"summary\": \"Stream semantic panel events as JSONL.\",\n \"details\": \"Uses replay then live delivery on one retained daemon connection with periodic reconciliation.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": false,\n \"description\": \"Optional panel filter.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": false,\n \"description\": \"Optional semantic event selector.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Exclusive replay cursor.\"\n },\n {\n \"name\": \"--heartbeat-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Reconciliation interval.\"\n },\n {\n \"name\": \"--jsonl\",\n \"required\": false,\n \"description\": \"Describes the always-JSONL output.\"\n }\n ],\n \"examples\": [\n \"runpane panels watch --panel <panel-id> --jsonl\"\n ],\n \"jsonSchemas\": [\n \"semanticEvent\",\n \"cursorExpiredError\"\n ],\n \"notes\": [\n \"Stdout contains compact JSON records only. Exit codes: 0 clean stop, 1 transport error, 3 cursor expired.\"\n ]\n },\n \"panels await\": {\n \"name\": \"panels await\",\n \"summary\": \"Wait for the first matching semantic event.\",\n \"details\": \"Exit-on-first-match sugar over watch with periodic state reconciliation.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Panel to await.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Semantic event selector.\"\n },\n {\n \"name\": \"--since\",\n \"value\": \"<cursor>\",\n \"required\": false,\n \"description\": \"Exclusive replay cursor.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Wait timeout.\"\n },\n {\n \"name\": \"--heartbeat-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Reconciliation interval.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels await --panel <panel-id> --event agent-idle --json\"\n ],\n \"jsonSchemas\": [\n \"panelAwaitResult\",\n \"cursorExpiredError\"\n ],\n \"notes\": [\n \"Exit codes: 0 matched, 1 transport error, 2 timed out, 3 cursor expired.\"\n ]\n },\n \"panels await-any\": {\n \"name\": \"panels await-any\",\n \"summary\": \"Wait across multiple panels or Panes.\",\n \"details\": \"Resolves Pane targets to their terminal panels and exits on the first matching event.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": false,\n \"description\": \"Panel target; repeatable.\"\n },\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": false,\n \"description\": \"Pane target; repeatable.\"\n },\n {\n \"name\": \"--event\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Semantic event selector.\"\n }\n ],\n \"examples\": [\n \"runpane panels await-any --panel p1 --panel p2 --event agent-idle --json\"\n ],\n \"notes\": []\n },\n \"panels create\": {\n \"name\": \"panels create\",\n \"summary\": \"Create a reviewer/helper terminal tab inside an existing Pane.\",\n \"details\": \"Use this to add reviewer, helper, or clean-context tabs to an existing Pane. The new panel shares the existing Pane's worktree and branch; it does not create a separate Pane session. Use `panes create` instead when the user wants a separate visible Pane (Pane session) for feature/PR work. Agent/orchestrator-created panels should pass --source agent or --no-focus so the user stays in the implementation tab. The daemon initializes the terminal immediately and can wait for CLI readiness.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--pane\",\n \"value\": \"<pane-id>\",\n \"required\": true,\n \"description\": \"Existing Pane session id to add the panel to.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent command template to launch.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to launch instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"required\": false,\n \"description\": \"Panel title override.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Initial input to send after the tool starts.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--source\",\n \"value\": \"<user|agent>\",\n \"required\": false,\n \"description\": \"Mutation source; agent implies background creation.\"\n },\n {\n \"name\": \"--no-focus\",\n \"required\": false,\n \"description\": \"Create the panel in the background without changing active panel.\"\n },\n {\n \"name\": \"--focus\",\n \"required\": false,\n \"description\": \"Explicitly focus the created panel.\"\n },\n {\n \"name\": \"--wait-ready\",\n \"required\": false,\n \"description\": \"Wait until the terminal tool is ready.\"\n },\n {\n \"name\": \"--ready-timeout-ms\",\n \"value\": \"<ms>\",\n \"required\": false,\n \"description\": \"Readiness wait timeout.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json\",\n \"runpane panels create --pane <pane-id> --tool-command <command> --title <title> --source agent --no-focus --wait-ready --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelCreateRequest\",\n \"panelCreateResult\"\n ],\n \"notes\": [\n \"Use this for same-pane reviewer loops after PR creation/testing.\",\n \"Panels share the existing Pane's worktree and branch.\",\n \"`panels create` is for visible helper/reviewer tabs, not the agent's default private delegation mechanism.\",\n \"For agent-created panels, prefer --source agent or --no-focus to avoid stealing focus from the user or implementation tab.\"\n ]\n },\n \"panels submit-composer\": {\n \"name\": \"panels submit-composer\",\n \"summary\": \"Submit an agent composer using the panel-appropriate key sequence.\",\n \"details\": \"Use this after sending text into an agent composer when plain Enter is not the right submit action. Agents should use --strategy auto; Pane owns per-agent submit sequences internally and verifies visible composer state when possible.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--panel\",\n \"value\": \"<panel-id>\",\n \"required\": true,\n \"description\": \"Terminal panel id.\"\n },\n {\n \"name\": \"--strategy\",\n \"value\": \"<auto|codex-ctrl-enter|enter>\",\n \"required\": false,\n \"description\": \"Composer submit key sequence strategy.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for this mutating command.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panels submit-composer --panel <codex-panel-id> --strategy auto --yes --json\",\n \"runpane panels submit-composer --panel <panel-id> --strategy enter --yes --json\"\n ],\n \"jsonSchemas\": [\n \"panelSubmitComposerRequest\",\n \"panelSubmitComposerResult\"\n ],\n \"notes\": [\n \"Use `panels input` or `panels submit` to write prompt text first; this command only submits the current composer.\",\n \"Use --strategy auto for agent workflows; explicit strategies are diagnostic escape hatches.\",\n \"The JSON result includes sequenceName and verifiedSubmitted. If ok is false, follow blocked.suggestedCommand instead of assuming submission happened.\"\n ]\n }\n },\n \"managedBlock\": [\n \"## Pane\",\n \"\",\n \"The developer is using Pane for this repository. Pane can manage saved repositories and create user-visible Panes with terminal-backed tools for planning, discussion, implementation, and review work.\",\n \"\",\n \"This managed guidance was created by [runpane.com](https://runpane.com) for the [Pane repository](https://github.com/dcouple/Pane). Do not delete or overwrite this block; the developer uses it for their workflow and it should remain committed to the main repository unless they explicitly ask to remove it.\",\n \"\",\n \"Pane mental model: a repository is the saved base repo; a Pane is a user-visible feature/PR workspace (Pane session) that normally maps to one Pane-managed git worktree and branch; a panel/tab is a terminal inside one Pane and shares that Pane's worktree; an agent is the CLI process running in a panel.\",\n \"\",\n \"Default happy path when the user asks you to use Pane or RunPane: run `runpane doctor --json`; read `runpane agent-context --json`; resolve the saved base repository with `runpane repos list --json` or add it once with `runpane repos add --path <repo> --yes --json`; create one visible Pane (Pane session) for the requested feature/PR with a complete command such as `runpane panes create --repo <repo> --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json` or the equivalent `--tool-command <command>` form; then validate with `runpane panels wait` or `runpane panels screen` before reporting progress.\",\n \"\",\n \"Use Pane when the user wants visible Panes or co-drivable parallel feature/PR workspaces. Do not use Pane as your default private delegation mechanism; for private background decomposition, use your normal subagent/worktree workflow.\",\n \"\",\n \"Register the main/base repository once. Do not register pre-created git worktrees as separate Pane repositories unless the user explicitly asks.\",\n \"\",\n \"Use `runpane panes create` for separate visible Panes (Pane sessions) for feature/PR work. Use `runpane panels create` for reviewer/helper tabs inside an existing Pane that should share that Pane's worktree.\",\n \"\",\n \"Typical workflow: register the saved base repository once; create one Pane (Pane session) per feature/PR; use panels/tabs inside that Pane for helper or reviewer agents that should share the worktree; archive the Pane after the PR is done to remove it from active Panes and clean up its managed worktree when applicable.\",\n \"\",\n \"Skill routing reference: when the user says `discussion`, `plan`, `simple-plan`, `create-plan`, or `implement`, or asks for the behavior those words imply, treat three references as peer context: Pane's local skill cache under `<PANE_DIR>/skills/`, the Pane Chat orchestrator handoff at `<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md` when present, and the [workflow map](https://github.com/dcouple/skills/raw/main/docs/readme-workflow-map.png).\",\n \"Use those peer references together to choose the phase: discuss/investigate until the work is clear enough to delegate, then ticket/plan/implement/review/PR-test/teach-back as appropriate. The orchestrator and workflow map may point to different skills; reconcile them with the user's request instead of hardcoding a skill list or treating one reference as subordinate.\",\n \"For the Pane implementation source of truth for where the skill cache, cached workflow assets, and Pane Chat bootstrap live, reference [PR #291](https://github.com/dcouple/Pane/pull/291): `main/src/services/skillCacheManager.ts` owns `<PANE_DIR>/skills/`, `.sources/dcouple-skills`, and `pane-chat/runpane-orchestrator.md`; `main/src/services/paneChatManager.ts` owns the tiny bootstrap prompt that tells the selected Pane Chat agent to read that guide.\",\n \"Use GitHub reads against the [Parsa skills folder](https://github.com/dcouple/skills/tree/main/parsa) only to inspect or refresh referenced skill files; do not clone/install the repo unless the user asks.\",\n \"Do not hardcode a specific assistant brand in workflow guidance. Use the Pane agent or custom tool command the user selected, and use `runpane agents doctor --agent <agent> --repo <selector> --json` only when checking a built-in agent template.\",\n \"\",\n \"Start with `runpane doctor --json` before taking Pane actions. Use it to understand wrapper/runtime details, daemon reachability, and the next safe commands.\",\n \"\",\n \"In a Pane repository checkout, if `runpane` is not on PATH, use the built local wrapper with Node 22: `PATH=/opt/homebrew/opt/node@22/bin:$PATH node packages/runpane/dist/cli.js doctor --json`.\",\n \"\",\n \"Use `runpane agent-context --json` for full Pane CLI context. Use `runpane agent-context --command \\\"panels wait\\\" --json` or another command name for detailed schema only when needed.\",\n \"\",\n \"Default to context-safe validation: after creating Panes or sending terminal input, run `runpane panels wait` or `runpane panels screen` before reporting success. Prefer `runpane panels submit` for normal text plus Enter; use `runpane panels input` only for exact bytes such as Ctrl-C or escape sequences.\",\n \"\",\n \"Common commands:\",\n \"- `runpane doctor --json`\",\n \"- `runpane agent-context --json`\",\n \"- `runpane repos list --json`\",\n \"- `runpane repos add --path <repo> --yes --json`\",\n \"- `runpane agents doctor --agent <agent> --repo active --json`\",\n \"- `runpane panes create --repo active --name <name> --agent <agent> --prompt \\\"<task>\\\" --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels create --pane <pane-id> --agent <agent> --source agent --no-focus --wait-ready --yes --json`\",\n \"- `runpane panels list --pane <pane-id> --json`\",\n \"- `runpane panels screen --panel <panel-id> --limit 80 --json`\",\n \"- `runpane panels wait --panel <panel-id> --for ready --timeout-ms 30000 --json`\",\n \"- `runpane panels submit --panel <panel-id> --text \\\"<answer>\\\" --yes --json`\",\n \"- `runpane panels input --panel <panel-id> --input-file <path|-> --yes --json`\",\n \"\",\n \"WSL note: if `runpane doctor --json` cannot find `/tmp/pane-daemon.../daemon.sock` or `runpane` resolves to a broken Windows shim, Pane may be running on Windows. Try `powershell.exe -NoProfile -Command 'Set-Location $env:TEMP; runpane doctor --json'`, then create Panes through the same PowerShell form using the saved WSL repo name or id. Use `runpane agents doctor --agent <agent> --repo <selector> --json` to diagnose the repo environment Pane will actually use.\"\n ]\n }\n}") diff --git a/packages/runpane-py/src/runpane/local_control.py b/packages/runpane-py/src/runpane/local_control.py index addd3196..8edd8f22 100644 --- a/packages/runpane-py/src/runpane/local_control.py +++ b/packages/runpane-py/src/runpane/local_control.py @@ -286,6 +286,53 @@ def run_panels_events(parsed: Any) -> int: return 0 +def run_panes_status(parsed: Any) -> int: + if not parsed.pane_id: + raise ValueError("runpane panes status requires --pane.") + result = invoke_daemon("runpane:panes:status", [{"paneId": parsed.pane_id, "changedSince": parsed.changed_since}], pane_dir=parsed.pane_dir) + if parsed.json: + print_json(result) + else: + for panel in result.get("panels", []): + print(f"{panel.get('panelId')}\t{(panel.get('state') or {}).get('agentActivity', 'unknown')}") + return 0 + + +def _resolve_multi_target_panels(parsed: Any, retain_panes: bool, stream: Optional["EventStream"] = None) -> None: + panel_ids = set(parsed.panel_ids) + for pane_id in parsed.pane_ids: + if stream: + result = stream.connection.request("runpane:panels:list", [{"paneId": pane_id}]) + else: + result = invoke_daemon("runpane:panels:list", [{"paneId": pane_id}], pane_dir=parsed.pane_dir) + panel_ids.update(panel.get("id") for panel in result.get("panels", []) if panel.get("id")) + parsed.panel_id = None + parsed.pane_id = None + parsed.panel_ids = list(panel_ids) + if not retain_panes: + parsed.pane_ids = [] + + +def run_panes_watch(parsed: Any) -> int: + stream = EventStream(parsed) + baseline_error = stream.capture_baseline() + if baseline_error is not None: + stream.close() + return baseline_error + _resolve_multi_target_panels(parsed, parsed.include_future_panels, stream) + return run_panels_watch(parsed, stream) + + +def run_panels_await_any(parsed: Any) -> int: + stream = EventStream(parsed) + baseline_error = stream.capture_baseline() + if baseline_error is not None: + stream.close() + return baseline_error + _resolve_multi_target_panels(parsed, False, stream) + return run_panels_await(parsed, stream) + + class EventStream: def __init__(self, parsed: Any) -> None: self.connection = RetainedDaemonConnection(parsed.pane_dir) @@ -321,9 +368,21 @@ def deliver(self, event: Any, resolved_by: str, emit: Any) -> None: def close(self) -> None: self.connection.close() + def capture_baseline(self) -> Optional[int]: + if self.initial_since: + return None + response = self.connection.request("runpane:panels:events", [{"since": self.initial_since}]) + if not response.get("ok"): + print(json.dumps(response, separators=(",", ":")), file=sys.stderr) + return 3 + cursor = response.get("cursor") + if cursor: + self.processed_cursor = cursor + return None -def run_panels_watch(parsed: Any) -> int: - stream = EventStream(parsed) + +def run_panels_watch(parsed: Any, stream: Optional[EventStream] = None) -> int: + stream = stream or EventStream(parsed) stopped = False def stop(_signum: int, _frame: Any) -> None: nonlocal stopped @@ -362,10 +421,10 @@ def emit(event: Dict[str, Any], resolved_by: str) -> None: stream.close() -def run_panels_await(parsed: Any) -> int: - if not parsed.panel_id or not parsed.event_selector: +def run_panels_await(parsed: Any, stream: Optional[EventStream] = None) -> int: + if (not parsed.panel_id and not parsed.panel_ids) or not parsed.event_selector: raise ValueError("runpane panels await requires --panel and --event.") - stream = EventStream(parsed) + stream = stream or EventStream(parsed) match: Optional[Dict[str, Any]] = None resolved = "event" def emit(event: Dict[str, Any], resolved_by: str) -> None: @@ -393,16 +452,18 @@ def emit(event: Dict[str, Any], resolved_by: str) -> None: if code is not None: return code if match is None and is_state_backed(parsed.event_selector): - screen = stream.connection.request("runpane:panels:screen", [{"panelId": parsed.panel_id}]) - event_type = match_state(parsed.event_selector, screen.get("state") or {}) - if event_type: - match = {"id": stream.processed_cursor, "cursor": stream.processed_cursor, "type": event_type, "at": "", "paneId": screen.get("paneId"), "panelId": parsed.panel_id, "state": screen.get("state") or {}} - resolved = "reconciliation" + for panel_id in ([parsed.panel_id] if parsed.panel_id else parsed.panel_ids): + screen = stream.connection.request("runpane:panels:screen", [{"panelId": panel_id}]) + event_type = match_state(parsed.event_selector, screen.get("state") or {}) + if event_type: + match = {"id": stream.processed_cursor, "cursor": stream.processed_cursor, "type": event_type, "at": "", "paneId": screen.get("paneId"), "panelId": panel_id, "state": screen.get("state") or {}} + resolved = "reconciliation" + break next_heartbeat += heartbeat if match is not None: print(json.dumps({"ok": True, "timedOut": False, "matchedEvent": match["type"], "resolvedBy": resolved, "event": match, "state": match["state"]}, separators=(",", ":"))) return 0 - screen = stream.connection.request("runpane:panels:screen", [{"panelId": parsed.panel_id}]) + screen = stream.connection.request("runpane:panels:screen", [{"panelId": parsed.panel_id or parsed.panel_ids[0]}]) print(json.dumps({"ok": False, "timedOut": True, "state": screen.get("state") or {}}, separators=(",", ":"))) return 2 finally: @@ -422,6 +483,7 @@ def validate_event(event: Any) -> None: raise ValueError("Malformed semantic event envelope") def matches_event(parsed: Any, event: Dict[str, Any], await_mode: bool) -> bool: if parsed.panel_id and event.get("panelId") != parsed.panel_id: return False + if (parsed.panel_ids or parsed.pane_ids) and event.get("panelId") not in parsed.panel_ids and event.get("paneId") not in parsed.pane_ids: return False if not parsed.event_selector: return True if await_mode and parsed.event_selector == "agent-idle": return event.get("type") in {"agent_idle", "input_required", "blocked", "panel_exited"} return EVENT_TYPES.get(parsed.event_selector) == event.get("type") @@ -547,6 +609,9 @@ def build_pane_create_request(parsed: Any) -> Dict[str, Any]: **optional_value("noFocus", True if not parsed.focus and (parsed.no_focus or parsed.source == "agent" or bool(parsed.agent)) else None), **optional_value("focus", True if parsed.focus else None), **optional_value("source", parsed.source), + **optional_value("startAgent", True if parsed.start_agent else None), + **optional_value("waitActive", True if parsed.wait_active else None), + **optional_value("handleKnownInterstitials", parsed.handle_known_interstitials), } diff --git a/packages/runpane/src/cli.ts b/packages/runpane/src/cli.ts index a7cd2d41..98b856b7 100644 --- a/packages/runpane/src/cli.ts +++ b/packages/runpane/src/cli.ts @@ -26,7 +26,10 @@ import { runPanelsEvents, runPanelsWatch, runPanelsAwait, + runPanelsAwaitAny, runPanesArchive, + runPanesStatus, + runPanesWatch, runPanesCreate, runPanesList, runPanesPin, @@ -120,6 +123,8 @@ async function dispatchParsedCommand(parsed: ParsedArgs, telemetryContext: Wrapp if (parsed.command === 'panes unpin') { return runPanesPin(parsed, false); } + if (parsed.command === 'panes status') return runPanesStatus(parsed); + if (parsed.command === 'panes watch') return runPanesWatch(parsed); if (parsed.command === 'panels list') { return runPanelsList(parsed); @@ -155,6 +160,7 @@ async function dispatchParsedCommand(parsed: ParsedArgs, telemetryContext: Wrapp if (parsed.command === 'panels events') return runPanelsEvents(parsed); if (parsed.command === 'panels watch') return runPanelsWatch(parsed); if (parsed.command === 'panels await') return runPanelsAwait(parsed); + if (parsed.command === 'panels await-any') return runPanelsAwaitAny(parsed); if (parsed.command === 'agents doctor') { return runAgentsDoctor(parsed); @@ -339,6 +345,8 @@ function createParsedArgs(command: ParsedArgs['command'], overrides: Partial<Par yes: false, verbose: false, json: false, + paneIds: [], + panelIds: [], remoteSetupArgs: [], ...overrides }; diff --git a/packages/runpane/src/commands.ts b/packages/runpane/src/commands.ts index afc9f19e..0b49a0e9 100644 --- a/packages/runpane/src/commands.ts +++ b/packages/runpane/src/commands.ts @@ -27,6 +27,8 @@ export interface ParsedArgs { repo?: string; paneId?: string; panelId?: string; + paneIds: string[]; + panelIds: string[]; repoPath?: string; name?: string; worktreeName?: string; @@ -57,6 +59,11 @@ export interface ParsedArgs { since?: string; jsonl?: boolean; heartbeatMs?: number; + changedSince?: string; + includeFuturePanels?: boolean; + startAgent?: boolean; + waitActive?: boolean; + handleKnownInterstitials?: string; remoteSetupArgs: string[]; } @@ -83,6 +90,8 @@ const DEFAULTS: Omit<ParsedArgs, 'command'> = { yes: RUNPANE_CONTRACT.defaults.yes, verbose: RUNPANE_CONTRACT.defaults.verbose, json: false, + paneIds: [], + panelIds: [], remoteSetupArgs: [] }; @@ -126,6 +135,8 @@ export function parseRunpaneArgs(argv: string[]): ParsedArgs { const parsed: ParsedArgs = { command: matched.name as RunpaneCommand, ...DEFAULTS, + paneIds: [], + panelIds: [], remoteSetupArgs: [] }; @@ -280,6 +291,12 @@ function parseLocalBooleanFlag(flag: string, parsed: ParsedArgs): void { parsed.jsonl = true; return; } + if (flag === '--include-future-panels') { + parsed.includeFuturePanels = true; + return; + } + if (flag === '--start-agent') { parsed.startAgent = true; return; } + if (flag === '--wait-active') { parsed.waitActive = true; return; } throw new Error(`Unknown option for ${parsed.command}: ${flag}`); } @@ -295,10 +312,12 @@ function parseLocalValueFlag(flag: string, value: string, parsed: ParsedArgs): v } if (flag === '--pane') { parsed.paneId = value; + parsed.paneIds.push(value); return; } if (flag === '--panel') { parsed.panelId = value; + parsed.panelIds.push(value); return; } if (flag === '--path') { @@ -428,6 +447,15 @@ function parseLocalValueFlag(flag: string, value: string, parsed: ParsedArgs): v parsed.since = value; return; } + if (flag === '--changed-since') { + parsed.changedSince = value; + return; + } + if (flag === '--handle-known-interstitials') { + if (value !== 'safe') throw new Error('--handle-known-interstitials must be safe.'); + parsed.handleKnownInterstitials = value; + return; + } if (flag === '--heartbeat-ms') { const heartbeatMs = Number(value); if (!Number.isFinite(heartbeatMs) || heartbeatMs <= 0) throw new Error('--heartbeat-ms must be a positive number.'); @@ -447,6 +475,8 @@ function isRunpaneLocalCommand(command: RunpaneCommand): boolean { || command === 'panes archive' || command === 'panes pin' || command === 'panes unpin' + || command === 'panes status' + || command === 'panes watch' || command === 'panels create' || command === 'panels list' || command === 'panels output' @@ -458,14 +488,20 @@ function isRunpaneLocalCommand(command: RunpaneCommand): boolean { || command === 'panels events' || command === 'panels watch' || command === 'panels await' + || command === 'panels await-any' || command === 'agents doctor'; } function validateEventCommandFlags(parsed: ParsedArgs): void { - if (parsed.jsonl && parsed.command !== 'panels watch') throw new Error('--jsonl is only valid with panels watch.'); + if (parsed.jsonl && parsed.command !== 'panels watch' && parsed.command !== 'panes watch') throw new Error('--jsonl is only valid with panels watch or panes watch.'); if (parsed.command === 'panels await' && (!parsed.panelId || !parsed.eventSelector)) { throw new Error('panels await requires --panel and --event.'); } + if (parsed.command === 'panels await-any' && (parsed.panelIds.length + parsed.paneIds.length === 0 || !parsed.eventSelector)) { + throw new Error('panels await-any requires at least one --panel or --pane and --event.'); + } + if (parsed.command === 'panes watch' && parsed.paneIds.length === 0) throw new Error('panes watch requires --pane.'); + if (parsed.command === 'panes status' && !parsed.paneId) throw new Error('panes status requires --pane.'); } function appendRemoteArg(parsed: ParsedArgs, flag: string, value?: string): void { diff --git a/packages/runpane/src/generated/contract.ts b/packages/runpane/src/generated/contract.ts index e1fcb0df..d47858e4 100644 --- a/packages/runpane/src/generated/contract.ts +++ b/packages/runpane/src/generated/contract.ts @@ -246,6 +246,33 @@ export const RUNPANE_CONTRACT = { "panePinResult" ] }, + { + "name": "panes status", + "summary": "Read a compact semantic-state snapshot for a Pane.", + "usage": [ + "runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]" + ], + "jsonSchemas": [ + "panelStateSummary" + ] + }, + { + "name": "panes watch", + "summary": "Stream semantic events for panels in a Pane.", + "usage": [ + "runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]" + ], + "outputMode": "jsonl", + "exitCodes": { + "0": "clean stop", + "1": "transport or other error", + "3": "cursor expired" + }, + "jsonSchemas": [ + "semanticEvent", + "cursorExpiredError" + ] + }, { "name": "panels create", "summary": "Create a terminal-backed tool panel inside an existing Pane session.", @@ -383,6 +410,23 @@ export const RUNPANE_CONTRACT = { "panelAwaitResult", "cursorExpiredError" ] + }, + { + "name": "panels await-any", + "summary": "Wait for the first matching event across panels or Panes.", + "usage": [ + "runpane panels await-any (--panel <panel-id>|--pane <pane-id>)... --event <selector> [--timeout-ms <ms>] [--json]" + ], + "exitCodes": { + "0": "matched", + "1": "transport or other error", + "2": "timed out", + "3": "cursor expired" + }, + "jsonSchemas": [ + "panelAwaitResult", + "cursorExpiredError" + ] } ], "flags": { @@ -488,11 +532,13 @@ export const RUNPANE_CONTRACT = { { "name": "--pane", "value": "<pane-id>", + "repeatable": true, "description": "Pane/session id to inspect." }, { "name": "--panel", "value": "<panel-id>", + "repeatable": true, "description": "Tool panel id to inspect or control." }, { @@ -593,11 +639,21 @@ export const RUNPANE_CONTRACT = { "value": "<cursor>", "description": "Replay events strictly after this cursor." }, + { + "name": "--changed-since", + "value": "<cursor>", + "description": "Return panels with semantic transitions after this lifecycle cursor." + }, { "name": "--heartbeat-ms", "value": "<milliseconds>", "description": "Periodic event-log and state reconciliation interval." }, + { + "name": "--handle-known-interstitials", + "value": "<safe>", + "description": "Opt in to the explicit reversible interstitial allowlist." + }, { "name": "--text", "value": "<text>", @@ -647,6 +703,18 @@ export const RUNPANE_CONTRACT = { { "name": "--jsonl", "description": "Print one compact JSON object per line (panels watch always uses JSONL)." + }, + { + "name": "--include-future-panels", + "description": "Keep watching panels created in the selected Pane after the stream starts." + }, + { + "name": "--start-agent", + "description": "Require verified initial prompt submission and active agent work." + }, + { + "name": "--wait-active", + "description": "Wait for agent activity after initial prompt delivery." } ] }, @@ -1039,6 +1107,18 @@ export const RUNPANE_CONTRACT = { "", "Exit codes: 0 success, 3 cursor expired, 1 transport/error." ], + "panes status": [ + "Usage:", + " runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]", + "", + "Returns compact per-panel semantic state and a race-free snapshot cursor." + ], + "panes watch": [ + "Usage:", + " runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]", + "", + "Streams semantic events for a Pane." + ], "panels watch": [ "Usage:", " runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", @@ -1055,6 +1135,12 @@ export const RUNPANE_CONTRACT = { "", "Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport/error." ], + "panels await-any": [ + "Usage:", + " runpane panels await-any (--panel <id>|--pane <id>)... --event <selector> [--timeout-ms <ms>] [--json]", + "", + "Waits for the first matching event across all selected targets." + ], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -1459,6 +1545,18 @@ export const RUNPANE_CONTRACT = { " --pane-dir <path> Connect to a specific Pane data directory", " --json Print machine-readable output" ], + "panes status": [ + "Usage:", + " python -m runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]", + "", + "Returns compact per-panel semantic state and a race-free snapshot cursor." + ], + "panes watch": [ + "Usage:", + " python -m runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]", + "", + "Streams semantic events for a Pane." + ], "panels events": [ "Usage:", " python -m runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", @@ -1477,6 +1575,12 @@ export const RUNPANE_CONTRACT = { "", "Waits for a matching semantic event and periodically reconciles state." ], + "panels await-any": [ + "Usage:", + " python -m runpane panels await-any (--panel <id>|--pane <id>)... --event <selector> [--timeout-ms <ms>] [--json]", + "", + "Waits for the first matching event across all selected targets." + ], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -1950,6 +2054,38 @@ export const RUNPANE_CONTRACT = { "--heartbeat-ms", "500", "--json" + ], + [ + "panels", + "await-any", + "--panel", + "panel-1", + "--panel", + "panel-2", + "--pane", + "pane-1", + "--pane", + "pane-2", + "--event", + "agent-idle", + "--json" + ], + [ + "panes", + "status", + "--pane", + "pane-1", + "--changed-since", + "epoch:4", + "--json" + ], + [ + "panes", + "watch", + "--pane", + "pane-1", + "--include-future-panels", + "--jsonl" ] ], "topLevelHelpIncludes": [ @@ -2490,6 +2626,17 @@ export const RUNPANE_CONTRACT = { "user", "agent" ] + }, + "startAgent": { + "type": "boolean" + }, + "waitActive": { + "type": "boolean" + }, + "handleKnownInterstitials": { + "enum": [ + "safe" + ] } }, "additionalProperties": false @@ -2576,6 +2723,18 @@ export const RUNPANE_CONTRACT = { "focused": { "type": "boolean" }, + "verifiedSubmitted": { + "type": "boolean" + }, + "agentActivity": { + "enum": [ + "unknown", + "starting", + "active", + "idle", + "exited" + ] + }, "readiness": { "type": "object", "required": [ @@ -2669,6 +2828,25 @@ export const RUNPANE_CONTRACT = { }, "nextCommand": { "type": "string" + }, + "handledInterstitials": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "response" + ], + "properties": { + "kind": { + "type": "string" + }, + "response": { + "type": "string" + } + }, + "additionalProperties": false + } } }, "additionalProperties": false @@ -5111,6 +5289,66 @@ export const RUNPANE_CONTRACT = { "Unpinning does not focus the Pane." ] }, + "panes status": { + "name": "panes status", + "summary": "Read semantic state for a Pane.", + "details": "Returns a compact pull snapshot and lifecycle cursor.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--pane", + "value": "<pane-id>", + "required": true, + "description": "Pane to inspect." + }, + { + "name": "--changed-since", + "value": "<cursor>", + "required": false, + "description": "Only panels changed after this cursor." + }, + { + "name": "--json", + "required": false, + "description": "Print machine-readable output." + } + ], + "examples": [ + "runpane panes status --pane <pane-id> --json" + ], + "notes": [] + }, + "panes watch": { + "name": "panes watch", + "summary": "Stream semantic events for a Pane.", + "details": "Filters the semantic event stream by Pane and can include panels created later.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--pane", + "value": "<pane-id>", + "required": true, + "description": "Pane to watch; repeatable." + }, + { + "name": "--include-future-panels", + "required": false, + "description": "Include panels created after watch starts." + }, + { + "name": "--event", + "value": "<selector>", + "required": false, + "description": "Optional event filter." + } + ], + "examples": [ + "runpane panes watch --pane <pane-id> --include-future-panels --jsonl" + ], + "notes": [] + }, "panels list": { "name": "panels list", "summary": "List tool panels inside a Pane session.", @@ -5593,6 +5831,37 @@ export const RUNPANE_CONTRACT = { "Exit codes: 0 matched, 1 transport error, 2 timed out, 3 cursor expired." ] }, + "panels await-any": { + "name": "panels await-any", + "summary": "Wait across multiple panels or Panes.", + "details": "Resolves Pane targets to their terminal panels and exits on the first matching event.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--panel", + "value": "<panel-id>", + "required": false, + "description": "Panel target; repeatable." + }, + { + "name": "--pane", + "value": "<pane-id>", + "required": false, + "description": "Pane target; repeatable." + }, + { + "name": "--event", + "value": "<selector>", + "required": true, + "description": "Semantic event selector." + } + ], + "examples": [ + "runpane panels await-any --panel p1 --panel p2 --event agent-idle --json" + ], + "notes": [] + }, "panels create": { "name": "panels create", "summary": "Create a reviewer/helper terminal tab inside an existing Pane.", diff --git a/packages/runpane/src/localControl.ts b/packages/runpane/src/localControl.ts index 987f30ff..104bffa2 100644 --- a/packages/runpane/src/localControl.ts +++ b/packages/runpane/src/localControl.ts @@ -52,6 +52,9 @@ interface PaneCreateRequest { noFocus?: boolean; focus?: boolean; source?: 'user' | 'agent'; + startAgent?: boolean; + waitActive?: boolean; + handleKnownInterstitials?: 'safe'; } interface PaneCreateItem { @@ -702,8 +705,55 @@ export async function runPanelsEvents(parsed: ParsedArgs): Promise<number> { return 0; } -export async function runPanelsWatch(parsed: ParsedArgs): Promise<number> { +export async function runPanesStatus(parsed: ParsedArgs): Promise<number> { + if (!parsed.paneId) throw new Error('runpane panes status requires --pane.'); + const result = await invokeDaemon<{ ok: true; paneId: string; panels: Array<{ panelId: string; paneId: string; state: PanelStateSummary }>; cursor: string }>( + 'runpane:panes:status', + [{ paneId: parsed.paneId, changedSince: parsed.changedSince }], + { paneDir: parsed.paneDir }, + ); + if (parsed.json) printJson(result); + else for (const panel of result.panels) console.log(`${panel.panelId}\t${panel.state.agentActivity ?? 'unknown'}`); + return 0; +} + +export async function runPanesWatch(parsed: ParsedArgs): Promise<number> { const stream = await SemanticEventStream.open(parsed); + await stream.captureBaseline(); + const scoped = await resolveMultiTargetPanels(parsed, parsed.includeFuturePanels === true, stream); + return runPanelsWatch(scoped, stream); +} + +export async function runPanelsAwaitAny(parsed: ParsedArgs): Promise<number> { + const stream = await SemanticEventStream.open(parsed); + await stream.captureBaseline(); + const scoped = await resolveMultiTargetPanels(parsed, false, stream); + return runPanelsAwait(scoped, stream); +} + +async function resolveMultiTargetPanels( + parsed: ParsedArgs, + retainPaneTargets: boolean, + stream?: SemanticEventStream, +): Promise<ParsedArgs> { + const panelIds = new Set(parsed.panelIds ?? []); + for (const paneId of parsed.paneIds ?? []) { + const result = stream + ? await stream.request<PanelListResult>('runpane:panels:list', [{ paneId }]) + : await invokeDaemon<PanelListResult>('runpane:panels:list', [{ paneId }], { paneDir: parsed.paneDir }); + for (const panel of result.panels) panelIds.add(panel.id); + } + return { + ...parsed, + panelId: undefined, + paneId: undefined, + panelIds: [...panelIds], + paneIds: retainPaneTargets ? [...(parsed.paneIds ?? [])] : [], + }; +} + +export async function runPanelsWatch(parsed: ParsedArgs, retainedStream?: SemanticEventStream): Promise<number> { + const stream = retainedStream ?? await SemanticEventStream.open(parsed); let transportFailed = false; let terminalErrorCode = 1; let stopped = false; @@ -747,9 +797,9 @@ export async function runPanelsWatch(parsed: ParsedArgs): Promise<number> { return finish(transportFailed ? terminalErrorCode : 0); } -export async function runPanelsAwait(parsed: ParsedArgs): Promise<number> { - if (!parsed.panelId || !parsed.eventSelector) throw new Error('runpane panels await requires --panel and --event.'); - const stream = await SemanticEventStream.open(parsed); +export async function runPanelsAwait(parsed: ParsedArgs, retainedStream?: SemanticEventStream): Promise<number> { + if ((!parsed.panelId && (parsed.panelIds?.length ?? 0) === 0) || !parsed.eventSelector) throw new Error('runpane panels await requires --panel and --event.'); + const stream = retainedStream ?? await SemanticEventStream.open(parsed); let matched: { event: SemanticEvent; resolvedBy: 'event' | 'reconciliation' } | undefined; let transportError: Error | undefined; stream.onTransportError((error) => { transportError = error; }); @@ -770,16 +820,7 @@ export async function runPanelsAwait(parsed: ParsedArgs): Promise<number> { if (now >= nextHeartbeat) { try { await stream.replay('reconciliation'); - if (!matched && isStateBackedSelector(parsed.eventSelector)) { - const screen = await stream.request<PanelScreenResult>('runpane:panels:screen', [{ panelId: parsed.panelId }]); - const reconciledType = matchState(parsed.eventSelector, screen.state); - if (reconciledType) { - matched = { - resolvedBy: 'reconciliation', - event: syntheticEvent(parsed.panelId, screen.paneId, reconciledType, screen.state, stream.processedCursor), - }; - } - } + if (!matched && isStateBackedSelector(parsed.eventSelector)) matched = await reconcileTargetStates(stream, parsed); } catch (error) { stream.close(); return handleStreamError(error); @@ -807,7 +848,8 @@ export async function runPanelsAwait(parsed: ParsedArgs): Promise<number> { return 0; } try { - const screen = await stream.request<PanelScreenResult>('runpane:panels:screen', [{ panelId: parsed.panelId }]); + const targetPanelId = parsed.panelId ?? parsed.panelIds?.[0]; + const screen = await stream.request<PanelScreenResult>('runpane:panels:screen', [{ panelId: targetPanelId }]); stream.close(); console.log(JSON.stringify({ ok: false, timedOut: true, state: screen.state })); return 2; @@ -851,6 +893,13 @@ class SemanticEventStream { noteEmitted(cursor: string): void { this.emittedCursor = cursor; } request<T>(channel: string, args: unknown[]): Promise<T> { return this.connection.request<T>(channel, args); } + async captureBaseline(): Promise<void> { + if (this.initialSince) return; + const response = await this.connection.request<EventsResponse>('runpane:panels:events', [{ since: this.initialSince }]); + if (!response.ok) throw new CursorExpiredError(response); + this.processedCursor = response.cursor; + } + async replay(resolvedBy: 'event' | 'reconciliation'): Promise<void> { this.buffering = true; const since = this.processedCursor || this.initialSince; @@ -903,10 +952,31 @@ function splitCursor(cursor: string): { epoch: string; n: number } { } function matchesParsedEvent(parsed: ParsedArgs, event: SemanticEvent, awaitMode: boolean): boolean { if (parsed.panelId && event.panelId !== parsed.panelId) return false; + const panelIds = parsed.panelIds ?? []; + const paneIds = parsed.paneIds ?? []; + const hasMultiTargets = panelIds.length > 0 || paneIds.length > 0; + const panelMatches = panelIds.includes(event.panelId); + const paneMatches = Boolean(event.paneId && paneIds.includes(event.paneId)); + if (hasMultiTargets && !panelMatches && !paneMatches) return false; if (!parsed.eventSelector) return true; if (awaitMode && parsed.eventSelector === 'agent-idle') return ['agent_idle', 'input_required', 'blocked', 'panel_exited'].includes(event.type); return EVENT_SELECTOR_TYPES[parsed.eventSelector] === event.type; } +async function reconcileTargetStates( + stream: SemanticEventStream, + parsed: ParsedArgs, +): Promise<{ event: SemanticEvent; resolvedBy: 'reconciliation' } | undefined> { + const targets = parsed.panelId ? [parsed.panelId] : (parsed.panelIds ?? []); + for (const panelId of targets) { + const screen = await stream.request<PanelScreenResult>('runpane:panels:screen', [{ panelId }]); + const reconciledType = matchState(parsed.eventSelector ?? '', screen.state); + if (reconciledType) return { + resolvedBy: 'reconciliation', + event: syntheticEvent(panelId, screen.paneId, reconciledType, screen.state, stream.processedCursor), + }; + } + return undefined; +} function isStateBackedSelector(selector: string): boolean { return ['terminal-ready', 'agent-active', 'agent-idle', 'input-required', 'blocked', 'unblocked', 'panel-exited'].includes(selector); } @@ -1060,6 +1130,9 @@ async function buildPaneCreateRequest(parsed: ParsedArgs): Promise<PaneCreateReq noFocus: !parsed.focus && (parsed.noFocus || source === 'agent' || Boolean(parsed.agent)) ? true : undefined, focus: parsed.focus || undefined, source, + startAgent: parsed.startAgent || undefined, + waitActive: parsed.waitActive || undefined, + handleKnownInterstitials: parsed.handleKnownInterstitials === 'safe' ? 'safe' : undefined, }; return request; diff --git a/packages/runpane/src/localControl.watch.test.ts b/packages/runpane/src/localControl.watch.test.ts index 33be7116..faee635a 100644 --- a/packages/runpane/src/localControl.watch.test.ts +++ b/packages/runpane/src/localControl.watch.test.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import { spawn } from 'node:child_process'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { getPaneDaemonEndpoint } from './daemonClient'; -import { runPanelsAwait, runPanelsEvents, runPanelsWatch } from './localControl'; +import { runPanelsAwait, runPanelsAwaitAny, runPanelsEvents, runPanelsWatch, runPanesStatus, runPanesWatch } from './localControl'; import type { ParsedArgs } from './commands'; type EventType = 'panel_created' | 'terminal_ready' | 'prompt_staged' | 'prompt_submitted' | 'agent_active' @@ -22,6 +22,12 @@ class FakeDaemon { suppressLive = false; eventRequests = 0; duringReplay?: ReturnType<typeof semanticEvent>; + panelsByPane = new Map<string, Array<{ id: string; paneId: string; type: string }>>([ + ['pane-1', [{ id: 'panel-1', paneId: 'pane-1', type: 'terminal' }]], + ]); + statusResponse?: { ok: true; paneId: string; panels: Array<{ panelId: string; paneId: string; state: ReturnType<typeof panelState> }>; cursor: string }; + statusRequests: Array<Record<string, unknown>> = []; + duringPanelList?: ReturnType<typeof semanticEvent>; async start(): Promise<void> { fs.mkdirSync(path.dirname(this.endpoint), { recursive: true }); @@ -53,8 +59,8 @@ class FakeDaemon { fs.rmSync(path.dirname(this.endpoint), { recursive: true, force: true }); } - append(type: EventType, state = this.state, live = true): ReturnType<typeof semanticEvent> { - const event = semanticEvent(this.events.length + 1, type, state); + append(type: EventType, state = this.state, live = true, ids: { panelId?: string; paneId?: string } = {}): ReturnType<typeof semanticEvent> { + const event = semanticEvent(this.events.length + 1, type, state, ids); this.events.push(event); if (live && !this.suppressLive) this.broadcast(event); return event; @@ -67,7 +73,30 @@ class FakeDaemon { private respond(socket: net.Socket, request: { id: number; channel: string; args: Array<Record<string, unknown>> }): void { if (request.channel === 'runpane:panels:screen') { - this.writeResponse(socket, request.id, { ok: true, panelId: 'panel-1', paneId: 'pane-1', source: 'empty', limit: 80, returnedLineCount: 0, hasMore: false, text: '', state: this.state }); + const panelId = typeof request.args[0]?.panelId === 'string' ? request.args[0].panelId : 'panel-1'; + const paneId = this.paneIdForPanel(panelId) ?? 'pane-1'; + this.writeResponse(socket, request.id, { ok: true, panelId, paneId, source: 'empty', limit: 80, returnedLineCount: 0, hasMore: false, text: '', state: this.state }); + return; + } + if (request.channel === 'runpane:panels:list') { + const paneId = String(request.args[0]?.paneId ?? 'pane-1'); + if (this.duringPanelList) { + const interleaved = this.duringPanelList; + this.duringPanelList = undefined; + this.events.push(interleaved); + this.broadcast(interleaved); + } + this.writeResponse(socket, request.id, { ok: true, paneId, panels: this.panelsByPane.get(paneId) ?? [] }); + return; + } + if (request.channel === 'runpane:panes:status') { + this.statusRequests.push(request.args[0] ?? {}); + this.writeResponse(socket, request.id, this.statusResponse ?? { + ok: true, + paneId: 'pane-1', + panels: [{ panelId: 'panel-1', paneId: 'pane-1', state: this.state }], + cursor: `epoch:${this.events.length}`, + }); return; } if (request.channel !== 'runpane:panels:events') throw new Error(`Unexpected channel ${request.channel}`); @@ -91,10 +120,26 @@ class FakeDaemon { private writeResponse(socket: net.Socket, id: number, result: unknown): void { socket.write(`${JSON.stringify({ type: 'response', id, ok: true, result })}\n`); } + + private paneIdForPanel(panelId: string): string | undefined { + for (const panels of this.panelsByPane.values()) { + const panel = panels.find(candidate => candidate.id === panelId); + if (panel) return panel.paneId; + } + return undefined; + } } -function semanticEvent(n: number, type: EventType, state = panelState('active')) { - return { id: `epoch:${n}`, cursor: `epoch:${n}`, type, at: new Date().toISOString(), paneId: 'pane-1', panelId: 'panel-1', state }; +function semanticEvent(n: number, type: EventType, state = panelState('active'), ids: { panelId?: string; paneId?: string } = {}) { + return { + id: `epoch:${n}`, + cursor: `epoch:${n}`, + type, + at: new Date().toISOString(), + paneId: ids.paneId ?? 'pane-1', + panelId: ids.panelId ?? 'panel-1', + state, + }; } function panelState(activity: 'active' | 'idle' | 'exited') { return { initialized: true, terminalReady: true, agentActivity: activity, blocked: false, inputRequired: false }; @@ -107,6 +152,139 @@ let daemon: FakeDaemon | undefined; afterEach(() => { daemon?.close(); daemon = undefined; vi.restoreAllMocks(); }); describe('semantic event wrapper', () => { + it('phase3 AC1: await-any identifies the winning panelId and paneId', async () => { + daemon = new FakeDaemon(); + daemon.panelsByPane.set('pane-2', [{ id: 'panel-2', paneId: 'pane-2', type: 'terminal' }]); + daemon.duringPanelList = semanticEvent(1, 'agent_idle', panelState('idle'), { panelId: 'panel-2', paneId: 'pane-2' }); + await daemon.start(); + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + const promise = runPanelsAwaitAny(parsed('panels await-any', daemon.paneDir, { + eventSelector: 'agent-idle', + panelIds: ['panel-1'], + paneIds: ['pane-2'], + timeoutMs: 500, + })); + + expect(await promise).toBe(0); + const result = JSON.parse(String(log.mock.calls.at(-1)?.[0] ?? '{}')) as { + event?: { panelId?: string; paneId?: string }; + matchedEvent?: string; + }; + expect(result).toMatchObject({ + matchedEvent: 'agent_idle', + event: { panelId: 'panel-2', paneId: 'pane-2' }, + }); + }); + + it('MF-4: await-any captures a matching event emitted during pane resolution', async () => { + daemon = new FakeDaemon(); + daemon.panelsByPane.set('pane-2', [{ id: 'panel-2', paneId: 'pane-2', type: 'terminal' }]); + daemon.duringPanelList = semanticEvent(1, 'prompt_staged', panelState('active'), { panelId: 'panel-2', paneId: 'pane-2' }); + await daemon.start(); + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + expect(await runPanelsAwaitAny(parsed('panels await-any', daemon.paneDir, { + eventSelector: 'prompt-staged', + paneIds: ['pane-2'], + timeoutMs: 100, + }))).toBe(0); + + const result = JSON.parse(String(log.mock.calls.at(-1)?.[0] ?? '{}')) as { + event?: { panelId?: string; paneId?: string }; + resolvedBy?: string; + }; + expect(result).toMatchObject({ + resolvedBy: 'event', + event: { panelId: 'panel-2', paneId: 'pane-2' }, + }); + }); + + it('MF-4 Python parity: await-any captures a matching event emitted during pane resolution', async () => { + daemon = new FakeDaemon(); + daemon.panelsByPane.set('pane-2', [{ id: 'panel-2', paneId: 'pane-2', type: 'terminal' }]); + daemon.duringPanelList = semanticEvent(1, 'prompt_staged', panelState('active'), { panelId: 'panel-2', paneId: 'pane-2' }); + await daemon.start(); + + const result = await runPython(daemon.paneDir, [ + 'panels', 'await-any', + '--pane', 'pane-2', + '--event', 'prompt-staged', + '--timeout-ms', '500', + '--json', + ]); + + expect(result.code).toBe(0); + expect(result.stdout).toContain('"panelId":"panel-2"'); + expect(result.stdout).toContain('"paneId":"pane-2"'); + }, 30_000); + + it('phase3 AC2: panes watch include-future-panels emits panel_created and later transitions', async () => { + daemon = new FakeDaemon(); await daemon.start(); + const writes: string[] = []; + vi.spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { writes.push(String(chunk)); return true; }); + + const promise = runPanesWatch(parsed('panes watch', daemon.paneDir, { + paneIds: ['pane-1'], + includeFuturePanels: true, + jsonl: true, + })); + await new Promise(resolve => setTimeout(resolve, 40)); + daemon.append('panel_created', panelState('active'), true, { panelId: 'panel-new', paneId: 'pane-1' }); + daemon.append('agent_active', panelState('active'), true, { panelId: 'panel-new', paneId: 'pane-1' }); + await new Promise(resolve => setTimeout(resolve, 40)); + process.emit('SIGINT'); + + expect(await promise).toBe(0); + expect(writes.map(line => (JSON.parse(line) as { type: string; panelId: string; paneId: string }))) + .toMatchObject([ + { type: 'panel_created', panelId: 'panel-new', paneId: 'pane-1' }, + { type: 'agent_active', panelId: 'panel-new', paneId: 'pane-1' }, + ]); + }); + + it('phase3 AC3: panes status changed-since returns changed panels and cursor for gap-free watch', async () => { + daemon = new FakeDaemon(); + daemon.statusResponse = { + ok: true, + paneId: 'pane-1', + panels: [{ panelId: 'panel-2', paneId: 'pane-1', state: panelState('idle') }], + cursor: 'epoch:2', + }; + daemon.events.push( + semanticEvent(1, 'agent_active', panelState('active'), { panelId: 'panel-1', paneId: 'pane-1' }), + semanticEvent(2, 'agent_idle', panelState('idle'), { panelId: 'panel-2', paneId: 'pane-1' }), + ); + await daemon.start(); + const logs: string[] = []; + vi.spyOn(console, 'log').mockImplementation((chunk: string) => { logs.push(chunk); }); + + expect(await runPanesStatus(parsed('panes status', daemon.paneDir, { + paneId: 'pane-1', + changedSince: 'epoch:1', + }))).toBe(0); + expect(daemon.statusRequests).toEqual([{ paneId: 'pane-1', changedSince: 'epoch:1' }]); + const status = JSON.parse(logs[0]) as { panels: Array<{ panelId: string }>; cursor: string }; + expect(status.panels.map(panel => panel.panelId)).toEqual(['panel-2']); + expect(status.cursor).toBe('epoch:2'); + + const writes: string[] = []; + vi.spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { writes.push(String(chunk)); return true; }); + const watch = runPanesWatch(parsed('panes watch', daemon.paneDir, { + paneIds: ['pane-1'], + includeFuturePanels: true, + since: status.cursor, + jsonl: true, + })); + await new Promise(resolve => setTimeout(resolve, 40)); + daemon.append('agent_active', panelState('active'), true, { panelId: 'panel-2', paneId: 'pane-1' }); + await new Promise(resolve => setTimeout(resolve, 40)); + process.emit('SIGINT'); + + expect(await watch).toBe(0); + expect(writes.map(line => (JSON.parse(line) as { cursor: string }).cursor)).toEqual(['epoch:3']); + }); + it('AC1 remains blocked while ready and active, then resolves agent-idle on an idle transition', async () => { daemon = new FakeDaemon(); await daemon.start(); const promise = runPanelsAwait(parsed('panels await', daemon.paneDir, { eventSelector: 'agent-idle', timeoutMs: 500 })); @@ -190,7 +368,7 @@ describe('semantic event wrapper', () => { const result = await runPython(daemon.paneDir, ['panels', 'watch', '--panel', 'panel-1', '--since', 'epoch:0', '--jsonl'], 2); expect(result.code).toBe(0); expect(result.stdout.trim().split('\n').map(line => (JSON.parse(line) as { cursor: string }).cursor)).toEqual(['epoch:1', 'epoch:2']); - }); + }, 30_000); it('Python parity: heartbeat recovery, cursor expiry, timeout, and transport use exit codes 0/3/2/1', async () => { daemon = new FakeDaemon(); daemon.suppressLive = true; await daemon.start(); @@ -210,7 +388,7 @@ describe('semantic event wrapper', () => { const transport = await runPython(missingDir, ['panels', 'events', '--json']); fs.rmSync(missingDir, { recursive: true, force: true }); expect(transport.code).toBe(1); - }); + }, 30_000); }); function runPython(paneDir: string, args: string[], stopAfterLines = 0): Promise<{ code: number | null; stdout: string; stderr: string }> { diff --git a/scripts/fixtures/runpane-contract.json b/scripts/fixtures/runpane-contract.json index ca7f8e5e..9b75d8e4 100644 --- a/scripts/fixtures/runpane-contract.json +++ b/scripts/fixtures/runpane-contract.json @@ -317,6 +317,38 @@ "--heartbeat-ms", "500", "--json" + ], + [ + "panels", + "await-any", + "--panel", + "panel-1", + "--panel", + "panel-2", + "--pane", + "pane-1", + "--pane", + "pane-2", + "--event", + "agent-idle", + "--json" + ], + [ + "panes", + "status", + "--pane", + "pane-1", + "--changed-since", + "epoch:4", + "--json" + ], + [ + "panes", + "watch", + "--pane", + "pane-1", + "--include-future-panels", + "--jsonl" ] ], "help": { diff --git a/scripts/test-runpane-contract.js b/scripts/test-runpane-contract.js index 3ef5ca4e..762771d5 100644 --- a/scripts/test-runpane-contract.js +++ b/scripts/test-runpane-contract.js @@ -145,6 +145,8 @@ function compareParserParity() { repo: parsed.repo ?? null, paneId: parsed.paneId ?? null, panelId: parsed.panelId ?? null, + paneIds: parsed.paneIds, + panelIds: parsed.panelIds, repoPath: parsed.repoPath ?? null, name: parsed.name ?? null, worktreeName: parsed.worktreeName ?? null, @@ -169,6 +171,8 @@ function compareParserParity() { noFocus: parsed.noFocus ?? false, focus: parsed.focus ?? false, composerStrategy: parsed.composerStrategy ?? null, + changedSince: parsed.changedSince ?? null, + includeFuturePanels: parsed.includeFuturePanels ?? false, remoteSetupArgs: parsed.remoteSetupArgs }; }); @@ -200,6 +204,8 @@ for args in samples: "repo": parsed.repo, "paneId": parsed.pane_id, "panelId": parsed.panel_id, + "paneIds": parsed.pane_ids, + "panelIds": parsed.panel_ids, "repoPath": parsed.repo_path, "name": parsed.name, "worktreeName": parsed.worktree_name, @@ -224,6 +230,8 @@ for args in samples: "noFocus": parsed.no_focus, "focus": parsed.focus, "composerStrategy": parsed.composer_strategy, + "changedSince": parsed.changed_since, + "includeFuturePanels": parsed.include_future_panels, "remoteSetupArgs": parsed.remote_setup_args, }) print(json.dumps(normalized)) diff --git a/shared/types/generatedRunpaneContract.ts b/shared/types/generatedRunpaneContract.ts index e1fcb0df..d47858e4 100644 --- a/shared/types/generatedRunpaneContract.ts +++ b/shared/types/generatedRunpaneContract.ts @@ -246,6 +246,33 @@ export const RUNPANE_CONTRACT = { "panePinResult" ] }, + { + "name": "panes status", + "summary": "Read a compact semantic-state snapshot for a Pane.", + "usage": [ + "runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]" + ], + "jsonSchemas": [ + "panelStateSummary" + ] + }, + { + "name": "panes watch", + "summary": "Stream semantic events for panels in a Pane.", + "usage": [ + "runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]" + ], + "outputMode": "jsonl", + "exitCodes": { + "0": "clean stop", + "1": "transport or other error", + "3": "cursor expired" + }, + "jsonSchemas": [ + "semanticEvent", + "cursorExpiredError" + ] + }, { "name": "panels create", "summary": "Create a terminal-backed tool panel inside an existing Pane session.", @@ -383,6 +410,23 @@ export const RUNPANE_CONTRACT = { "panelAwaitResult", "cursorExpiredError" ] + }, + { + "name": "panels await-any", + "summary": "Wait for the first matching event across panels or Panes.", + "usage": [ + "runpane panels await-any (--panel <panel-id>|--pane <pane-id>)... --event <selector> [--timeout-ms <ms>] [--json]" + ], + "exitCodes": { + "0": "matched", + "1": "transport or other error", + "2": "timed out", + "3": "cursor expired" + }, + "jsonSchemas": [ + "panelAwaitResult", + "cursorExpiredError" + ] } ], "flags": { @@ -488,11 +532,13 @@ export const RUNPANE_CONTRACT = { { "name": "--pane", "value": "<pane-id>", + "repeatable": true, "description": "Pane/session id to inspect." }, { "name": "--panel", "value": "<panel-id>", + "repeatable": true, "description": "Tool panel id to inspect or control." }, { @@ -593,11 +639,21 @@ export const RUNPANE_CONTRACT = { "value": "<cursor>", "description": "Replay events strictly after this cursor." }, + { + "name": "--changed-since", + "value": "<cursor>", + "description": "Return panels with semantic transitions after this lifecycle cursor." + }, { "name": "--heartbeat-ms", "value": "<milliseconds>", "description": "Periodic event-log and state reconciliation interval." }, + { + "name": "--handle-known-interstitials", + "value": "<safe>", + "description": "Opt in to the explicit reversible interstitial allowlist." + }, { "name": "--text", "value": "<text>", @@ -647,6 +703,18 @@ export const RUNPANE_CONTRACT = { { "name": "--jsonl", "description": "Print one compact JSON object per line (panels watch always uses JSONL)." + }, + { + "name": "--include-future-panels", + "description": "Keep watching panels created in the selected Pane after the stream starts." + }, + { + "name": "--start-agent", + "description": "Require verified initial prompt submission and active agent work." + }, + { + "name": "--wait-active", + "description": "Wait for agent activity after initial prompt delivery." } ] }, @@ -1039,6 +1107,18 @@ export const RUNPANE_CONTRACT = { "", "Exit codes: 0 success, 3 cursor expired, 1 transport/error." ], + "panes status": [ + "Usage:", + " runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]", + "", + "Returns compact per-panel semantic state and a race-free snapshot cursor." + ], + "panes watch": [ + "Usage:", + " runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]", + "", + "Streams semantic events for a Pane." + ], "panels watch": [ "Usage:", " runpane panels watch [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--heartbeat-ms <ms>] [--jsonl]", @@ -1055,6 +1135,12 @@ export const RUNPANE_CONTRACT = { "", "Exit codes: 0 matched, 2 timed out, 3 cursor expired, 1 transport/error." ], + "panels await-any": [ + "Usage:", + " runpane panels await-any (--panel <id>|--pane <id>)... --event <selector> [--timeout-ms <ms>] [--json]", + "", + "Waits for the first matching event across all selected targets." + ], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -1459,6 +1545,18 @@ export const RUNPANE_CONTRACT = { " --pane-dir <path> Connect to a specific Pane data directory", " --json Print machine-readable output" ], + "panes status": [ + "Usage:", + " python -m runpane panes status --pane <pane-id> [--changed-since <cursor>] [--json]", + "", + "Returns compact per-panel semantic state and a race-free snapshot cursor." + ], + "panes watch": [ + "Usage:", + " python -m runpane panes watch --pane <pane-id> [--include-future-panels] [--event <selector>] [--since <cursor>] [--jsonl]", + "", + "Streams semantic events for a Pane." + ], "panels events": [ "Usage:", " python -m runpane panels events [--panel <panel-id>] [--event <selector>] [--since <cursor>] [--json]", @@ -1477,6 +1575,12 @@ export const RUNPANE_CONTRACT = { "", "Waits for a matching semantic event and periodically reconciles state." ], + "panels await-any": [ + "Usage:", + " python -m runpane panels await-any (--panel <id>|--pane <id>)... --event <selector> [--timeout-ms <ms>] [--json]", + "", + "Waits for the first matching event across all selected targets." + ], "panels create": [ "Create a terminal-backed tool panel inside an existing Pane session.", "", @@ -1950,6 +2054,38 @@ export const RUNPANE_CONTRACT = { "--heartbeat-ms", "500", "--json" + ], + [ + "panels", + "await-any", + "--panel", + "panel-1", + "--panel", + "panel-2", + "--pane", + "pane-1", + "--pane", + "pane-2", + "--event", + "agent-idle", + "--json" + ], + [ + "panes", + "status", + "--pane", + "pane-1", + "--changed-since", + "epoch:4", + "--json" + ], + [ + "panes", + "watch", + "--pane", + "pane-1", + "--include-future-panels", + "--jsonl" ] ], "topLevelHelpIncludes": [ @@ -2490,6 +2626,17 @@ export const RUNPANE_CONTRACT = { "user", "agent" ] + }, + "startAgent": { + "type": "boolean" + }, + "waitActive": { + "type": "boolean" + }, + "handleKnownInterstitials": { + "enum": [ + "safe" + ] } }, "additionalProperties": false @@ -2576,6 +2723,18 @@ export const RUNPANE_CONTRACT = { "focused": { "type": "boolean" }, + "verifiedSubmitted": { + "type": "boolean" + }, + "agentActivity": { + "enum": [ + "unknown", + "starting", + "active", + "idle", + "exited" + ] + }, "readiness": { "type": "object", "required": [ @@ -2669,6 +2828,25 @@ export const RUNPANE_CONTRACT = { }, "nextCommand": { "type": "string" + }, + "handledInterstitials": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "response" + ], + "properties": { + "kind": { + "type": "string" + }, + "response": { + "type": "string" + } + }, + "additionalProperties": false + } } }, "additionalProperties": false @@ -5111,6 +5289,66 @@ export const RUNPANE_CONTRACT = { "Unpinning does not focus the Pane." ] }, + "panes status": { + "name": "panes status", + "summary": "Read semantic state for a Pane.", + "details": "Returns a compact pull snapshot and lifecycle cursor.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--pane", + "value": "<pane-id>", + "required": true, + "description": "Pane to inspect." + }, + { + "name": "--changed-since", + "value": "<cursor>", + "required": false, + "description": "Only panels changed after this cursor." + }, + { + "name": "--json", + "required": false, + "description": "Print machine-readable output." + } + ], + "examples": [ + "runpane panes status --pane <pane-id> --json" + ], + "notes": [] + }, + "panes watch": { + "name": "panes watch", + "summary": "Stream semantic events for a Pane.", + "details": "Filters the semantic event stream by Pane and can include panels created later.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--pane", + "value": "<pane-id>", + "required": true, + "description": "Pane to watch; repeatable." + }, + { + "name": "--include-future-panels", + "required": false, + "description": "Include panels created after watch starts." + }, + { + "name": "--event", + "value": "<selector>", + "required": false, + "description": "Optional event filter." + } + ], + "examples": [ + "runpane panes watch --pane <pane-id> --include-future-panels --jsonl" + ], + "notes": [] + }, "panels list": { "name": "panels list", "summary": "List tool panels inside a Pane session.", @@ -5593,6 +5831,37 @@ export const RUNPANE_CONTRACT = { "Exit codes: 0 matched, 1 transport error, 2 timed out, 3 cursor expired." ] }, + "panels await-any": { + "name": "panels await-any", + "summary": "Wait across multiple panels or Panes.", + "details": "Resolves Pane targets to their terminal panels and exits on the first matching event.", + "requiresPaneDaemon": true, + "mutates": false, + "arguments": [ + { + "name": "--panel", + "value": "<panel-id>", + "required": false, + "description": "Panel target; repeatable." + }, + { + "name": "--pane", + "value": "<pane-id>", + "required": false, + "description": "Pane target; repeatable." + }, + { + "name": "--event", + "value": "<selector>", + "required": true, + "description": "Semantic event selector." + } + ], + "examples": [ + "runpane panels await-any --panel p1 --panel p2 --event agent-idle --json" + ], + "notes": [] + }, "panels create": { "name": "panels create", "summary": "Create a reviewer/helper terminal tab inside an existing Pane.", diff --git a/shared/types/panels.ts b/shared/types/panels.ts index a5289808..b61a7075 100644 --- a/shared/types/panels.ts +++ b/shared/types/panels.ts @@ -33,6 +33,7 @@ export interface TerminalPanelState { initialInput?: string; // First input to send once the initial command is ready initialInputMode?: 'stdin' | 'argument'; // How initialInput is delivered to the initial command initialInputSubmitStrategy?: 'enter' | 'codex-ctrl-enter'; // How stdin initialInput should be submitted + initialInputDeliveryOwner?: 'runpane-create'; // Owner for create-time guarded delivery; generic callbacks must not deliver this input initialInputDeliveryVersion?: number; // Bumps when a feature changes delivery semantics initialInputSentAt?: string; // Set after initialInput has been written once initialInputError?: string; // Best-effort error if initialInput could not be written diff --git a/shared/types/runpaneOrchestration.ts b/shared/types/runpaneOrchestration.ts index 4523184d..dccf5f68 100644 --- a/shared/types/runpaneOrchestration.ts +++ b/shared/types/runpaneOrchestration.ts @@ -101,6 +101,9 @@ export interface RunpanePaneCreateRequest { noFocus?: boolean; focus?: boolean; source?: RunpanePanelCreateSource; + startAgent?: boolean; + waitActive?: boolean; + handleKnownInterstitials?: 'safe'; } export interface RunpaneErrorPayload { @@ -219,6 +222,24 @@ export interface RunpanePanelsAwaitTimeoutResult { state: RunpanePanelStateSummary; } +export interface RunpanePaneStatusRequest { + paneId: string; + changedSince?: string; +} + +export interface RunpanePaneStatusPanel { + paneId: string; + panelId: string; + state: RunpanePanelStateSummary; +} + +export interface RunpanePaneStatusResult { + ok: true; + paneId: string; + panels: RunpanePaneStatusPanel[]; + cursor: string; +} + export interface RunpanePaneReadiness { ok: boolean; condition: RunpanePanelWaitCondition; @@ -243,6 +264,7 @@ export interface RunpaneInitialInputDeliveryResult { blocked?: RunpanePanelBlockedState; error?: RunpaneErrorPayload; nextCommand?: string; + handledInterstitials?: Array<{ kind: string; response: string }>; } export interface RunpanePaneCreateSuccessItem { @@ -264,6 +286,8 @@ export interface RunpanePaneCreateSuccessItem { focused?: boolean; readiness?: RunpanePaneReadiness; initialInput?: RunpaneInitialInputDeliveryResult; + verifiedSubmitted?: boolean; + agentActivity?: RunpaneAgentActivity; } export interface RunpanePaneCreateFailureItem { From 2f674933dda71244293747a6d968dd68e65e72ae Mon Sep 17 00:00:00 2001 From: parsakhaz <khazapar7@gmail.com> Date: Thu, 23 Jul 2026 01:34:30 -0700 Subject: [PATCH 4/4] fix: address P1 review findings on interstitial detection and from-json startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three P1 findings from review on #363. Consequential-prompt patterns required actual prompt structure. `permission`, `delete`, `payment` and `billing` previously matched as bare keywords anywhere on the recent screen, so ordinary agent output — "deleting obsolete files", "reviewing permission handling" — tripped CreateInputGuard, emitted input_required, and marked pane creation unsuccessful when no interstitial existed. A consequential keyword now blocks only when it sits in an actual interactive prompt: a trailing question, a [y/n] affordance, a confirm/press directive, or a selection menu. Self-contained prompts (directory trust, authentication) still match on their own, so the gate stays fail-closed. The ongoing blocker scanner now reuses the interstitial classifier instead of maintaining a second, weaker matcher. It previously recognised only the Codex update prompt and the literal phrase "press enter to continue", so selection menus, [y/n] prompts, directory trust, authentication and permission decisions produced no blocked/input_required event after startup — leaving agent_idle indistinguishable from "waiting for input", the exact confusion this epic exists to remove. The codex-update blocker kind and its suggested command are preserved. `panes create --from-json` now carries the startup guarantees. --start-agent, --wait-active and --handle-known-interstitials were applied only on the non-JSON path, so a --from-json create reported success with no submission or activity verification. Fixed in both the npm and Python wrappers with a named test each, keeping wrapper parity. Tests cover both directions per keyword family: a genuine auth, permission, destructive, payment or directory-trust prompt still blocks, and prose merely mentioning the keyword does not. Refs #356 --- main/src/ipc/runpane.ts | 5 ++ main/src/services/runpaneBlockerDetection.ts | 14 ++-- .../src/services/runpaneInterstitials.test.ts | 61 ++++++++++++++-- main/src/services/runpaneInterstitials.ts | 43 +++++++++--- .../runpane-py/src/runpane/local_control.py | 6 ++ packages/runpane/src/localControl.ts | 9 +++ .../runpane/src/localControl.watch.test.ts | 70 ++++++++++++++++++- 7 files changed, 182 insertions(+), 26 deletions(-) diff --git a/main/src/ipc/runpane.ts b/main/src/ipc/runpane.ts index b91457b7..6962d088 100644 --- a/main/src/ipc/runpane.ts +++ b/main/src/ipc/runpane.ts @@ -871,6 +871,11 @@ async function submitCreateInitialInput( })); } + if (!effectiveReadiness.ok && effectiveReadiness.blocked) { + const gate = await guard.ensureClear(); + if (gate.blocked) return blockedInitialInput(tool.initialInput, gate.blocked, guard.handledInterstitials()); + } + if (!effectiveReadiness.ok) { return { delivered: false, diff --git a/main/src/services/runpaneBlockerDetection.ts b/main/src/services/runpaneBlockerDetection.ts index 4d2165c4..11cc65af 100644 --- a/main/src/services/runpaneBlockerDetection.ts +++ b/main/src/services/runpaneBlockerDetection.ts @@ -3,6 +3,7 @@ import type { RunpanePanelBlockedState, } from '../../../shared/types/runpaneOrchestration'; import { sanitizeTerminalOutput } from '../utils/terminalOutputSanitizer'; +import { classifyRunpaneInterstitial } from './runpaneInterstitials'; export function boundSanitizedLines( rawText: string, @@ -22,22 +23,19 @@ export function detectPanelBlocker( panelId: string, ): RunpanePanelBlockedState | undefined { if (!text) return undefined; - if ( - (agentType === 'codex' || /codex/i.test(text)) && - /update available/i.test(text) && - (/skip/i.test(text) || /npm install -g @openai\/codex/i.test(text)) - ) { + const classification = classifyRunpaneInterstitial(text, agentType, panelId); + if (classification.disposition === 'allow') { return { kind: 'codex-update', message: 'Codex is showing an update prompt instead of accepting the task prompt.', suggestedCommand: `runpane panels submit --panel ${panelId} --text "2" --yes --json`, }; } - if (/press enter to continue/i.test(text)) { + if (classification.disposition === 'deny' || classification.disposition === 'unknown') { return { kind: 'agent-prompt', - message: 'The terminal is waiting at an interactive prompt.', - suggestedCommand: `runpane panels screen --panel ${panelId} --json`, + message: classification.blocker.message, + suggestedCommand: classification.blocker.suggestedCommand, }; } return undefined; diff --git a/main/src/services/runpaneInterstitials.test.ts b/main/src/services/runpaneInterstitials.test.ts index 09a452cb..ef149631 100644 --- a/main/src/services/runpaneInterstitials.test.ts +++ b/main/src/services/runpaneInterstitials.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import { detectPanelBlocker } from './runpaneBlockerDetection'; import { classifyRunpaneInterstitial } from './runpaneInterstitials'; describe('classifyRunpaneInterstitial', () => { @@ -15,20 +16,66 @@ describe('classifyRunpaneInterstitial', () => { }); it.each([ - 'Authentication required', - 'Grant permission to continue', - 'This cannot be undone. Continue?', - 'Accept terms of service', - 'Enter payment information', - ])('denies consequential prompt: %s', (screen) => { - expect(classifyRunpaneInterstitial(screen, 'codex', 'p1').disposition).toBe('deny'); + ['trust-canonical', 'Do you trust the contents of this directory?\n> 1. Yes, proceed\n 2. No, exit'], + ['auth-trailing-prose', 'Authentication required to continue using this service.'], + ['auth-colon', 'Authentication required:'], + ['auth-mcp-signin', 'Please sign in to continue with your account'], + ['perm-question', 'Grant access to your GitHub repositories? [y/N]'], + ['perm-allow-line', 'Allow access to the filesystem?'], + ['destructive-confirm', 'This will delete 42 files and cannot be undone. Are you sure?'], + ['destructive-typeconfirm', 'Type DELETE to confirm removal of the production database'], + ['payment-question', 'Confirm payment of $20.00 to continue?'], + ['terms-accept', 'Do you accept the terms of service?'], + ['perm-caret', 'Permission required for shell access\n❯ Yes'], + ])('D6 fail-closed probe blocks %s', (_name, screen) => { + expect(classifyRunpaneInterstitial(screen, 'codex', 'p1').disposition).not.toBe('clear'); + expect(detectPanelBlocker(screen, 'codex', 'p1')).toMatchObject({ kind: 'agent-prompt' }); + }); + + it.each([ + ['prose-delete', 'deleting obsolete files from the build directory'], + ['prose-permission', 'reviewing permission handling in the auth module'], + ['prose-billing', 'updated the billing module and removed the payment stub'], + ['prose-destroy', 'the destroy() method overwrites the cache on teardown'], + ['prose-terms', 'documented the terms of the license in README'], + ])('D6 false-positive probe clears %s', (_name, screen) => { + expect(classifyRunpaneInterstitial(screen, 'codex', 'p1')).toEqual({ disposition: 'clear' }); + expect(detectPanelBlocker(screen, 'codex', 'p1')).toBeUndefined(); + }); + + it.each([ + ['auth-trailing-prose', 'Authentication required to continue using this service.'], + ['auth-colon', 'Authentication required:'], + ['auth-mcp-signin', 'Please sign in to continue with your account'], + ])('D6 detectPanelBlocker blocks auth prompt %s', (_name, screen) => { + expect(detectPanelBlocker(screen, 'codex', 'p1')).toMatchObject({ kind: 'agent-prompt' }); }); it('stops on an unknown planning modal', () => { expect(classifyRunpaneInterstitial('Planning suggestion: would you like to switch modes?', 'codex', 'p1').disposition).toBe('unknown'); }); + it('recognizes numbered and arrowed selection menus without mistaking the Codex composer for one', () => { + expect(classifyRunpaneInterstitial('Choose an option:\n› Continue\n Cancel', 'codex', 'p1').disposition).toBe('unknown'); + expect(classifyRunpaneInterstitial('› /do TM-x', 'codex', 'p1')).toEqual({ disposition: 'clear' }); + }); + it('does not treat a passive MCP authentication banner as an interstitial', () => { expect(classifyRunpaneInterstitial('4 MCP servers need authentication\n›', 'claude', 'p1').disposition).toBe('clear'); }); + + it('P1-B maps unknown prompts to an agent-prompt blocker', () => { + expect(detectPanelBlocker('Planning suggestion: would you like to switch modes?', 'codex', 'p1')).toMatchObject({ + kind: 'agent-prompt', + message: 'An unrecognized interactive prompt requires explicit input.', + }); + }); + + it('P1-B preserves the Codex update blocker response command', () => { + expect(detectPanelBlocker('Codex update available — Skip', 'codex', 'p1')).toEqual({ + kind: 'codex-update', + message: 'Codex is showing an update prompt instead of accepting the task prompt.', + suggestedCommand: 'runpane panels submit --panel p1 --text "2" --yes --json', + }); + }); }); diff --git a/main/src/services/runpaneInterstitials.ts b/main/src/services/runpaneInterstitials.ts index ae1dbebc..7bc62f2c 100644 --- a/main/src/services/runpaneInterstitials.ts +++ b/main/src/services/runpaneInterstitials.ts @@ -5,29 +5,54 @@ export type RunpaneInterstitialClassification = | { disposition: 'allow'; kind: 'codex-update'; response: '2'; justification: string } | { disposition: 'deny' | 'unknown'; blocker: RunpanePanelBlockedState }; -const DENY_PATTERNS: Array<{ pattern: RegExp; message: string }> = [ - { pattern: /(?:trust|do you trust).*(?:directory|folder|workspace)|(?:directory|folder|workspace).*(?:trust|trusted)/i, message: 'Directory trust requires explicit human input.' }, - { pattern: /authentication required|authenticate to continue|authorization required|sign[ -]?in to continue|log[ -]?in to continue|enter (?:an? )?(?:api )?(?:key|token)/i, message: 'Authentication requires explicit human input.' }, +const SELF_CONTAINED_DENY_PATTERNS: Array<{ pattern: RegExp; message: string }> = [ + { pattern: /(?:do you trust|trust (?:the contents of )?)(?:this |the )?(?:directory|folder|workspace)|(?:directory|folder|workspace) (?:is not trusted|requires trust)/i, message: 'Directory trust requires explicit human input.' }, + { pattern: /(?:authentication required|authenticate to continue|authorization required|sign[ -]?in to continue|log[ -]?in to continue|enter (?:an? )?(?:api )?(?:key|token))/i, message: 'Authentication requires explicit human input.' }, +]; + +const CONSEQUENTIAL_DENY_PATTERNS: Array<{ pattern: RegExp; message: string }> = [ { pattern: /permission|grant access|allow access/i, message: 'A permission decision requires explicit human input.' }, { pattern: /delete|destroy|overwrite|irreversible|cannot be undone/i, message: 'A destructive confirmation requires explicit human input.' }, { pattern: /terms (?:of|and)|privacy policy|payment|purchase|billing/i, message: 'Terms or payment decisions require explicit human input.' }, ]; +const PROMPT_STRUCTURE_PATTERN = /(?:\?\s*(?:\[[^\]]+\]|\([^)]+\))?\s*$|\[[yn]\s*\/\s*[yn]\]|\([yn]\s*\/\s*[yn]\)|\bpress (?:enter|return|any key|[a-z0-9])\b|\btype .+ to confirm\b|\bdo you want\b|\bwould you like\b|\bare you sure\b|\bconfirm(?:\s+(?:this|the|your|to)\b.*)?\??\s*$|\bcontinue\?\s*$|^\s*(?:please\s+)?(?:grant|allow|accept|agree|enter|provide|delete|destroy|overwrite)\b)/i; +const PROMPT_CONTINUATION_PATTERN = /^\s*(?:[>›❯]\s*|\[[yn]\s*\/\s*[yn]\]|\([yn]\s*\/\s*[yn]\)|(?:\d+|[a-z])[.)]\s+\S.*)$/i; + +function hasPromptStructure(lines: string[], index: number): boolean { + return PROMPT_STRUCTURE_PATTERN.test(lines[index]) + || (index + 1 < lines.length && PROMPT_CONTINUATION_PATTERN.test(lines[index + 1])); +} + export function classifyRunpaneInterstitial( text: string, agentType: RunpaneAgentId | undefined, panelId: string, ): RunpaneInterstitialClassification { - for (const denied of DENY_PATTERNS) { - if (denied.pattern.test(text)) return { - disposition: 'deny', - blocker: { kind: 'unknown', message: denied.message, suggestedCommand: `runpane panels screen --panel ${panelId} --json` }, - }; + const lines = text.split(/\r?\n/); + for (const line of lines) { + for (const denied of SELF_CONTAINED_DENY_PATTERNS) { + if (denied.pattern.test(line.trim())) return { + disposition: 'deny', + blocker: { kind: 'unknown', message: denied.message, suggestedCommand: `runpane panels screen --panel ${panelId} --json` }, + }; + } + } + for (const [index, line] of lines.entries()) { + for (const denied of CONSEQUENTIAL_DENY_PATTERNS) { + if (denied.pattern.test(line) && hasPromptStructure(lines, index)) return { + disposition: 'deny', + blocker: { kind: 'unknown', message: denied.message, suggestedCommand: `runpane panels screen --panel ${panelId} --json` }, + }; + } } if ((agentType === 'codex' || /codex/i.test(text)) && /update available/i.test(text) && /skip/i.test(text)) { return { disposition: 'allow', kind: 'codex-update', response: '2', justification: 'Skipping an optional update is reversible and leaves the installed agent unchanged.' }; } - if (/planning suggestion|press enter to continue|would you like to|(?:choose|select) (?:an?|one)|\[[yn]\/?[yn]?\]/i.test(text)) { + const numberedOptions = lines.filter(line => /^\s*\d+[.)]\s+\S/.test(line)).length; + if (/planning suggestion|press enter to continue|would you like to|(?:choose|select) (?:an?|one)|\[[yn]\s*\/\s*[yn]\]|\([yn]\s*\/\s*[yn]\)/i.test(text) + || numberedOptions >= 2 + || lines.some(line => /^\s*(?:>|›|❯|→)\s*(?:\d+[.)]\s+|yes\b|no\b|continue\b|cancel\b|skip\b|exit\b)/i.test(line))) { return { disposition: 'unknown', blocker: { kind: 'unknown', message: 'An unrecognized interactive prompt requires explicit input.', suggestedCommand: `runpane panels screen --panel ${panelId} --json` }, diff --git a/packages/runpane-py/src/runpane/local_control.py b/packages/runpane-py/src/runpane/local_control.py index 8edd8f22..ed9b84ab 100644 --- a/packages/runpane-py/src/runpane/local_control.py +++ b/packages/runpane-py/src/runpane/local_control.py @@ -577,6 +577,12 @@ def build_pane_create_request(parsed: Any) -> Dict[str, Any]: payload["readyTimeoutMs"] = parsed.ready_timeout_ms if parsed.concurrency is not None: payload["concurrency"] = parsed.concurrency + if parsed.start_agent: + payload["startAgent"] = True + if parsed.wait_active: + payload["waitActive"] = True + if parsed.handle_known_interstitials == "safe": + payload["handleKnownInterstitials"] = "safe" if parsed.pinned: payload["panes"] = [ {**item, "pinned": True} if isinstance(item, dict) else item diff --git a/packages/runpane/src/localControl.ts b/packages/runpane/src/localControl.ts index 104bffa2..14b2250e 100644 --- a/packages/runpane/src/localControl.ts +++ b/packages/runpane/src/localControl.ts @@ -1094,6 +1094,15 @@ async function buildPaneCreateRequest(parsed: ParsedArgs): Promise<PaneCreateReq if (parsed.concurrency !== undefined) { request.concurrency = parsed.concurrency; } + if (parsed.startAgent) { + request.startAgent = true; + } + if (parsed.waitActive) { + request.waitActive = true; + } + if (parsed.handleKnownInterstitials === 'safe') { + request.handleKnownInterstitials = 'safe'; + } if (parsed.pinned) { request.panes = request.panes.map(item => ({ ...item, pinned: true })); } diff --git a/packages/runpane/src/localControl.watch.test.ts b/packages/runpane/src/localControl.watch.test.ts index faee635a..c7eece3b 100644 --- a/packages/runpane/src/localControl.watch.test.ts +++ b/packages/runpane/src/localControl.watch.test.ts @@ -4,9 +4,9 @@ import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { parseRunpaneArgs, type ParsedArgs } from './commands'; import { getPaneDaemonEndpoint } from './daemonClient'; -import { runPanelsAwait, runPanelsAwaitAny, runPanelsEvents, runPanelsWatch, runPanesStatus, runPanesWatch } from './localControl'; -import type { ParsedArgs } from './commands'; +import { runPanelsAwait, runPanelsAwaitAny, runPanelsEvents, runPanelsWatch, runPanesCreate, runPanesStatus, runPanesWatch } from './localControl'; type EventType = 'panel_created' | 'terminal_ready' | 'prompt_staged' | 'prompt_submitted' | 'agent_active' | 'agent_idle' | 'input_required' | 'blocked' | 'unblocked' | 'panel_exited' | 'panel_archived'; @@ -27,6 +27,7 @@ class FakeDaemon { ]); statusResponse?: { ok: true; paneId: string; panels: Array<{ panelId: string; paneId: string; state: ReturnType<typeof panelState> }>; cursor: string }; statusRequests: Array<Record<string, unknown>> = []; + paneCreateRequests: Array<Record<string, unknown>> = []; duringPanelList?: ReturnType<typeof semanticEvent>; async start(): Promise<void> { @@ -99,6 +100,11 @@ class FakeDaemon { }); return; } + if (request.channel === 'runpane:panes:create') { + this.paneCreateRequests.push(request.args[0] ?? {}); + this.writeResponse(socket, request.id, { ok: true, repo: {}, items: [] }); + return; + } if (request.channel !== 'runpane:panels:events') throw new Error(`Unexpected channel ${request.channel}`); this.eventRequests += 1; if (this.expire) { @@ -152,6 +158,66 @@ let daemon: FakeDaemon | undefined; afterEach(() => { daemon?.close(); daemon = undefined; vi.restoreAllMocks(); }); describe('semantic event wrapper', () => { + it('P1-C npm: --from-json carries startup guarantee flags', async () => { + daemon = new FakeDaemon(); + await daemon.start(); + const payloadPath = path.join(daemon.paneDir, 'create.json'); + fs.writeFileSync(payloadPath, JSON.stringify({ + repo: 'active', + panes: [{ name: 'json-pane', tool: { command: 'echo ready' } }], + startAgent: false, + waitActive: false, + })); + vi.spyOn(console, 'log').mockImplementation(() => undefined); + + const args = parseRunpaneArgs([ + 'panes', 'create', + '--from-json', payloadPath, + '--start-agent', + '--wait-active', + '--handle-known-interstitials', 'safe', + '--pane-dir', daemon.paneDir, + '--yes', + '--json', + ]); + + expect(await runPanesCreate(args)).toBe(0); + expect(daemon.paneCreateRequests).toContainEqual(expect.objectContaining({ + startAgent: true, + waitActive: true, + handleKnownInterstitials: 'safe', + })); + }); + + it('P1-C Python: --from-json carries startup guarantee flags', async () => { + daemon = new FakeDaemon(); + await daemon.start(); + const payloadPath = path.join(daemon.paneDir, 'create-python.json'); + fs.writeFileSync(payloadPath, JSON.stringify({ + repo: 'active', + panes: [{ name: 'json-pane', tool: { command: 'echo ready' } }], + startAgent: false, + waitActive: false, + })); + + const result = await runPython(daemon.paneDir, [ + 'panes', 'create', + '--from-json', payloadPath, + '--start-agent', + '--wait-active', + '--handle-known-interstitials', 'safe', + '--yes', + '--json', + ]); + + expect(result.code).toBe(0); + expect(daemon.paneCreateRequests).toContainEqual(expect.objectContaining({ + startAgent: true, + waitActive: true, + handleKnownInterstitials: 'safe', + })); + }, 30_000); + it('phase3 AC1: await-any identifies the winning panelId and paneId', async () => { daemon = new FakeDaemon(); daemon.panelsByPane.set('pane-2', [{ id: 'panel-2', paneId: 'pane-2', type: 'terminal' }]);