@kaiba-cloud/orbital 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mod.json ADDED
@@ -0,0 +1 @@
1
+ {"files":{".claude-plugin/marketplace.json":"{\n \"name\": \"orbital\",\n \"owner\": { \"name\": \"orbital\" },\n \"plugins\": [{ \"name\": \"orbital\", \"source\": \"./\" }]\n}\n",".claude-plugin/plugin.json":"{\n \"name\": \"orbital\",\n \"version\": \"0.0.2\",\n \"description\": \"Joins this Claude Code session to an orbital node: the agent tools of the team, one policy chain on every tool call, messages from peers as turns, and a pane of the node.\",\n \"license\": \"UNLICENSED\",\n \"types\": \"./types/index.d.ts\",\n \"userConfig\": {\n \"orbital_bin\": {\n \"type\": \"string\",\n \"title\": \"orbital binary\",\n \"description\": \"The program that runs orbital. Empty: orbital, on the PATH. Set it to run orbital through another program.\",\n \"default\": \"\"\n },\n \"node\": {\n \"type\": \"string\",\n \"title\": \"Node\",\n \"description\": \"The node to join. Empty: ORBITAL_NODE, then the node key of the definition, then the node of the folder.\",\n \"default\": \"\"\n },\n \"team\": {\n \"type\": \"string\",\n \"title\": \"Team\",\n \"description\": \"The team this session joins. Sessions of one team are peers.\",\n \"default\": \"\"\n },\n \"governance\": {\n \"type\": \"string\",\n \"title\": \"Governance\",\n \"description\": \"enforce: a tool call is denied when the node does not answer. observe: the node only reports.\",\n \"options\": [\n \"enforce\",\n \"observe\"\n ],\n \"default\": \"enforce\"\n }\n }\n}\n",".mcp.json":"{\n \"mcpServers\": {\n \"orbital\": {\n \"command\": \"orbital\",\n \"args\": [\"mcp\", \"--attach\", \"--harness\", \"claude-code\", \"--agent\", \"claude-code\", \"--external-id-env\", \"CLAUDE_CODE_SESSION_ID\"]\n }\n }\n}\n","hooks/hooks.json":"{ \"modules\": [\"./register.tsx\"] }\n","hooks/logic.ts":"export type Attached = { sessionId: string; token: string; node: string; nodeUrl: string };\nexport type Delivery = { taskId: string; fromAgent: string | null; fromSession: string | null; mission: string | null; text: string };\nexport type Decision = { verdict: 'allow' } | { verdict: 'deny' | 'ask'; reason: string; policy: string };\nexport type Peer = { sessionId: string; agent: string; role: string; harness: string; status: string; self: boolean; parent: string | null };\nexport type CheckAnswer = { decision: 'allow' } | { decision: 'deny' | 'ask'; reason: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction text(value: unknown): string | null {\n return typeof value === 'string' && value !== '' ? value : null;\n}\n\nexport function recordsOf(chunk: string, carry: string): { records: unknown[]; carry: string } {\n const lines = (carry + chunk).split('\\n');\n const rest = lines.pop() ?? '';\n const records = lines.filter((line) => line.trim() !== '').flatMap((line) => {\n try {\n return [JSON.parse(line) as unknown];\n } catch {\n return [];\n }\n });\n return { records, carry: rest };\n}\n\nexport function attachedOf(record: unknown): Attached | null {\n if (!isRecord(record) || record.kind !== 'attached') return null;\n const sessionId = text(record.sessionId);\n const token = text(record.token);\n const node = text(record.node);\n const nodeUrl = text(record.nodeUrl);\n return sessionId && token && node && nodeUrl ? { sessionId, token, node, nodeUrl } : null;\n}\n\nexport function detachedOf(record: unknown): string | null {\n if (!isRecord(record) || record.kind !== 'detached') return null;\n return typeof record.reason === 'string' ? record.reason : '';\n}\n\nexport const DETACHED_PROBLEM = 'The node is gone. orbital joins it again by itself when it runs.';\n\nexport function deliveryOf(record: unknown): Delivery | null {\n if (!isRecord(record) || record.kind !== 'delivery') return null;\n const taskId = text(record.taskId);\n if (!taskId || typeof record.text !== 'string') return null;\n return { taskId, fromAgent: text(record.fromAgent), fromSession: text(record.fromSession), mission: text(record.mission), text: record.text };\n}\n\nexport function decisionOf(body: unknown): Decision | null {\n if (!isRecord(body) || !isRecord(body.decision)) return null;\n const decision = body.decision;\n if (decision.verdict === 'allow') return { verdict: 'allow' };\n if ((decision.verdict === 'deny' || decision.verdict === 'ask') && typeof decision.reason === 'string' && typeof decision.policy === 'string') {\n return { verdict: decision.verdict, reason: decision.reason, policy: decision.policy };\n }\n return null;\n}\n\nexport function peersOf(body: unknown): Peer[] | null {\n if (!isRecord(body) || !Array.isArray(body.peers)) return null;\n return body.peers.flatMap((peer): Peer[] => {\n if (!isRecord(peer)) return [];\n const sessionId = text(peer.sessionId);\n const agent = text(peer.agent);\n if (!sessionId || !agent) return [];\n return [{ sessionId, agent, role: text(peer.role) ?? 'member', harness: text(peer.harness) ?? '', status: text(peer.status) ?? '', self: peer.self === true, parent: text(peer.parentSessionId) }];\n });\n}\n\n\n\nexport function checkAnswerOf(decision: Decision, hasApprover: boolean): CheckAnswer {\n if (decision.verdict === 'allow') return { decision: 'allow' };\n const reason = `${decision.policy}: ${decision.reason}`;\n if (decision.verdict === 'ask' && hasApprover) return { decision: 'ask', reason };\n return { decision: 'deny', reason };\n}\n\nexport function framed(delivery: Delivery): string {\n if (delivery.mission) return `[orbital] the mission ${delivery.mission} waits for you:\\n\\n${delivery.text}\\n\\nShow the person this question and wait for their answer. Then send their answer to the lead with missionSay on ${delivery.mission}, or run: orbital mission say ${delivery.mission} \"<answer>\".`;\n const from = delivery.fromAgent ? `${delivery.fromAgent}${delivery.fromSession ? ` (session ${delivery.fromSession})` : ''}` : 'the node';\n return `[orbital] message from ${from}:\\n\\n${delivery.text}\\n\\nAnswer it in this turn: your final answer goes back to the sender.`;\n}\n\nexport function attachArgv(bin: readonly string[], options: { cwd: string; sessionId: string; node: string; team: string; mission?: string }): string[] {\n return [\n ...bin,\n 'session',\n 'attach',\n '--harness',\n 'claude-code',\n '--agent',\n 'claude-code',\n '--workspace',\n options.cwd,\n '--external-id',\n options.sessionId,\n '--output',\n 'ndjson',\n ...(options.mission ? ['--mission', options.mission] : options.node ? ['--node', options.node] : []),\n ...(options.team ? ['--team', options.team] : []),\n ];\n}\n\nexport function binCandidates(configured: string): string[][] {\n if (configured.trim() !== '') return [configured.trim().split(/\\s+/)];\n return [['orbital']];\n}\n\nexport function versionOf(stdout: string): string | null {\n try {\n const parsed = JSON.parse(stdout) as unknown;\n if (isRecord(parsed) && isRecord(parsed.data) && typeof parsed.data.version === 'string') return parsed.data.version;\n } catch {\n return null;\n }\n return null;\n}\n\nexport type MissionState = 'open' | 'active' | 'waiting' | 'paused' | 'done' | 'failed';\nexport type Mission = {\n missionId: string;\n node: string;\n team: string | null;\n state: MissionState;\n lead: string | null;\n giver: string | null;\n question: string | null;\n result: string | null;\n brief: string | null;\n claim: string | null;\n claimedAt: string | null;\n stale: boolean;\n lastActivity: string | null;\n sessionId: string | null;\n startedAt: string | null;\n history: Array<{ state: string; at: string; by: string; note: string | null }>;\n};\n\nconst STATES: readonly MissionState[] = ['open', 'active', 'waiting', 'paused', 'done', 'failed'];\n\nexport function missionsOf(body: unknown): Mission[] | null {\n if (!isRecord(body) || !Array.isArray(body.missions)) return null;\n return body.missions.flatMap((one): Mission[] => {\n if (!isRecord(one)) return [];\n const missionId = text(one.missionId);\n const node = text(one.node);\n const state = STATES.find((known) => known === one.state);\n if (!missionId || !node || !state) return [];\n const history = Array.isArray(one.history) ? one.history.flatMap((change) => (isRecord(change) && typeof change.state === 'string' && typeof change.at === 'string' ? [{ state: change.state, at: change.at, by: typeof change.by === 'string' ? change.by : '', note: text(change.note) }] : [])) : [];\n return [{\n missionId,\n node,\n team: text(one.team),\n state,\n lead: text(one.lead),\n giver: text(one.giver),\n question: text(one.question),\n result: text(one.result),\n brief: text(one.brief),\n claim: text(one.claim),\n claimedAt: text(one.claimedAt),\n stale: one.stale === true,\n lastActivity: text(one.lastActivity),\n sessionId: text(one.sessionId),\n startedAt: history.find((change) => change.state === 'active')?.at ?? text(one.claimedAt),\n history,\n }];\n });\n}\n\nexport function linesOf(body: unknown): string[] {\n if (!isRecord(body) || !Array.isArray(body.lines)) return [];\n return body.lines.flatMap((line): string[] => (isRecord(line) && typeof line.text === 'string' ? [`${typeof line.at === 'string' ? line.at.slice(11, 19) : '--:--:--'} ${line.text}`] : []));\n}\n\nconst ORDER: Record<MissionState, number> = { waiting: 0, active: 1, paused: 2, open: 3, failed: 4, done: 5 };\n\nexport function missionOrder(missions: readonly Mission[]): Mission[] {\n return [...missions].sort((a, b) => ORDER[a.state] - ORDER[b.state] || a.missionId.localeCompare(b.missionId));\n}\n\nconst SPIN = ['◐', '◓', '◑', '◒'];\n\nexport function stateMark(state: MissionState, tick: number): { mark: string; color: 'warning' | 'suggestion' | 'success' | 'error' | 'subtle' } {\n switch (state) {\n case 'waiting':\n return { mark: '●', color: 'warning' };\n case 'active':\n return { mark: SPIN[tick % SPIN.length] ?? '◐', color: 'suggestion' };\n case 'paused':\n return { mark: '❄', color: 'subtle' };\n case 'done':\n return { mark: '✓', color: 'success' };\n case 'failed':\n return { mark: '✗', color: 'error' };\n default:\n return { mark: '○', color: 'subtle' };\n }\n}\n\nexport function elapsed(fromIso: string | null, now: number): string {\n if (!fromIso) return '';\n const seconds = Math.max(0, Math.floor((now - Date.parse(fromIso)) / 1000));\n if (seconds < 60) return `${seconds}s`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m`;\n return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;\n}\n\nexport function bandText(node: string, missions: readonly Mission[], sessions: number, toAnswer: number): string {\n const active = missions.filter((mission) => mission.state === 'active').length;\n const waiting = missions.filter((mission) => mission.state === 'waiting').length;\n return [\n 'orbital',\n node,\n ...(active > 0 ? [`${active} mission${active === 1 ? '' : 's'} active`] : []),\n ...(waiting > 0 ? [`${waiting} waiting`] : []),\n `${sessions} session${sessions === 1 ? '' : 's'}`,\n ...(toAnswer > 0 ? [`${toAnswer} to answer`] : []),\n ].join(' · ');\n}\n\nexport function changedMissions(before: readonly Mission[], after: readonly Mission[]): Mission[] {\n return after.filter((mission) => (mission.state === 'waiting' || mission.state === 'done' || mission.state === 'failed') && before.some((old) => old.missionId === mission.missionId && old.node === mission.node && old.state !== mission.state));\n}\n\nexport function waitingNotice(listed: readonly Mission[], sessionId: string, seen: readonly string[]): { fresh: Mission[]; seen: string[] } {\n const asked = listed.filter((mission) => mission.state === 'waiting' && mission.giver !== sessionId);\n const keyOf = (mission: Mission) => `${mission.node}/${mission.missionId}`;\n return { fresh: asked.filter((mission) => !seen.includes(keyOf(mission))), seen: asked.map(keyOf) };\n}\n\nexport function toastOf(mission: Mission): string {\n switch (mission.state) {\n case 'waiting':\n return `orbital: ${mission.missionId} waits for you: ${mission.question ?? 'its lead stopped without a hand-off'}`.slice(0, 200);\n case 'done':\n return `orbital: ${mission.missionId} is done: ${mission.result ?? ''}`.slice(0, 200);\n default:\n return `orbital: ${mission.missionId} failed: ${mission.result ?? ''}`.slice(0, 200);\n }\n}\n\n\n\nexport function missionDetail(mission: Mission, activity: readonly string[]): string[] {\n return [\n `${mission.missionId} on ${mission.node}: ${mission.state}${mission.lead ? ` · lead ${mission.lead}` : ''}${mission.sessionId ? ` · session ${mission.sessionId.slice(0, 8)}` : ''}`,\n ...(mission.claim ? [`claim ${mission.claim}${mission.claimedAt ? ` since ${mission.claimedAt}` : ''}${mission.stale ? ' (stale)' : ''}`] : []),\n ...(mission.brief ? ['', mission.brief] : []),\n ...(mission.state === 'waiting' && mission.question ? ['', 'question:', mission.question] : []),\n ...(mission.state === 'paused' ? ['', ...(mission.question ? ['question (it waits again on resume):', mission.question, ''] : []), `paused: resume it with orbital mission resume ${mission.missionId}, or with missionResume`] : []),\n ...(mission.result ? ['', 'result:', mission.result] : []),\n ...(mission.history.length > 0 ? ['', 'history:', ...mission.history.map((change) => ` ${change.at.slice(0, 19).replace('T', ' ')} ${change.state.padEnd(7)} ${change.by}${change.note ? ` ${change.note}` : ''}`)] : []),\n ...(activity.length > 0 ? ['', 'activity of the lead:', ...activity.map((line) => ` ${line}`)] : []),\n ];\n}\n\nexport type BackgroundWork = { id: string; kind: string; description: string; state: string; lastTool: string | null; at: string };\nexport type TranscriptSession = { id: string; agent: string; status: string; isRunning: boolean; team: string | null; mission: string | null; since: string | null; doing: string | null; lastActivityAt: string | null; background: BackgroundWork[] };\nexport type TranscriptLine = { mark: string; tone: 'plain' | 'dim' | 'good' | 'bad' | 'busy' | 'ask'; text: string };\nexport type Transcript = { session: TranscriptSession; lines: TranscriptLine[] };\n\nfunction clipLine(value: string, length: number): string {\n const line = value.replace(/\\s+/g, ' ').trim();\n return line.length > length ? `${line.slice(0, length - 1)}…` : line;\n}\n\nfunction resultLine(value: string): string {\n const lines = value.replace(/\\s+$/, '').split('\\n').filter((line) => line.trim() !== '');\n if (lines.length === 0) return '(no output)';\n return `${clipLine(lines[0] ?? '', 90)}${lines.length > 1 ? ` … +${lines.length - 1} lines` : ''}`;\n}\n\nexport function transcriptOf(body: unknown): Transcript | null {\n if (!isRecord(body) || !isRecord(body.session) || !Array.isArray(body.entries)) return null;\n const raw = body.session;\n const id = text(raw.id);\n if (!id) return null;\n const background = Array.isArray(raw.background) ? raw.background.flatMap((one): BackgroundWork[] => (isRecord(one) && text(one.id) && text(one.at) ? [{ id: text(one.id)!, kind: text(one.kind) ?? 'task', description: text(one.description) ?? '', state: text(one.state) ?? 'running', lastTool: text(one.lastTool), at: text(one.at)! }] : [])) : [];\n const session: TranscriptSession = { id, agent: text(raw.agent) ?? 'agent', status: text(raw.status) ?? '', isRunning: raw.isRunning === true, team: text(raw.team), mission: text(raw.mission), since: text(raw.since), doing: text(raw.doing), lastActivityAt: text(raw.lastActivityAt), background };\n const lines = body.entries.flatMap((entry): TranscriptLine[] => {\n if (!isRecord(entry)) return [];\n switch (entry.kind) {\n case 'user':\n return [{ mark: '>', tone: 'dim', text: `${text(entry.from) ? `from ${text(entry.from)}: ` : ''}${clipLine(String(entry.text ?? ''), 200)}` }];\n case 'assistant':\n return String(entry.text ?? '').split('\\n').filter((line) => line.trim() !== '').slice(0, 8).map((line, index) => ({ mark: index === 0 ? '⏺' : ' ', tone: 'plain' as const, text: line }));\n case 'tool': {\n const name = text(entry.name) ?? 'tool';\n const input = text(entry.input);\n const call: TranscriptLine = { mark: '⏺', tone: isRecord(entry.result) ? (entry.result.isError === true ? 'bad' : 'good') : 'busy', text: input ? `${name}(${input})` : name };\n if (!isRecord(entry.result)) return [call];\n return [call, { mark: ' ', tone: entry.result.isError === true ? 'bad' : 'dim', text: `⎿ ${resultLine(String(entry.result.text ?? ''))}` }];\n }\n case 'end':\n if (entry.state === 'failed') return [{ mark: '✗', tone: 'bad', text: clipLine(String(entry.text ?? 'The turn failed.'), 200) }];\n if (entry.state === 'canceled') return [{ mark: ' ', tone: 'dim', text: '⎿ Stopped' }];\n if (entry.state === 'waiting') return [{ mark: '⏺', tone: 'ask', text: `Waits for an answer: ${clipLine(String(entry.text ?? ''), 200)}` }];\n return text(entry.text) ? [{ mark: '⏺', tone: 'plain', text: clipLine(String(entry.text), 300) }] : [];\n default:\n return [];\n }\n });\n return { session, lines };\n}\n\nconst SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\n\nexport function isMissionTarget(target: string, missions: readonly Mission[], peers: readonly Peer[]): boolean {\n const asked = target.trim();\n if (missions.some((one) => one.missionId === asked)) return true;\n if (SESSION_ID.test(asked) || peers.some((one) => one.sessionId === asked || (asked.length >= 6 && one.sessionId.startsWith(asked)))) return false;\n return /^[a-z0-9][a-z0-9-]{0,62}$/.test(asked);\n}\n\nexport function joinTarget(target: string, missions: readonly Mission[], peers: readonly Peer[]): { sessionId: string; label: string } | { error: string } {\n const asked = target.trim();\n if (asked === '') return { error: 'Name a mission by its id, or a session id: /orbital join <mission-or-session>.' };\n if (isMissionTarget(asked, missions, peers)) {\n const mission = missions.find((one) => one.missionId === asked && one.sessionId) ?? null;\n if (!mission?.sessionId) return { error: `The mission ${asked} has no lead session yet. /orbital missions lists them.` };\n return { sessionId: mission.sessionId, label: `${mission.missionId} · ${mission.lead ?? 'lead'}` };\n }\n const peer = peers.find((one) => one.sessionId === asked || (asked.length >= 6 && one.sessionId.startsWith(asked)));\n return peer ? { sessionId: peer.sessionId, label: peer.agent } : { sessionId: asked, label: asked.slice(0, 8) };\n}\n\nexport function sessionHeader(session: TranscriptSession, now: number): string {\n const state = session.isRunning ? 'working' : session.status === 'input_required' ? 'waiting' : session.status === 'completed' ? 'idle' : session.status;\n return [\n session.agent,\n session.id.slice(0, 8),\n ...(session.mission ? [session.mission] : []),\n state,\n ...(session.isRunning ? [`${session.doing ?? 'working'}${session.since ? ` ${elapsed(session.since, now)}` : ''}`] : []),\n ...(session.isRunning && session.lastActivityAt ? [`last activity ${elapsed(session.lastActivityAt, now)} ago`] : []),\n ].join(' · ');\n}\n\nexport function backgroundLines(session: TranscriptSession, now: number): string[] {\n const running = session.background.filter((task) => task.state === 'running').slice(-5);\n const ended = session.background.length - session.background.filter((task) => task.state === 'running').length;\n return [\n ...running.map((task) => `↳ running ${task.description ? clipLine(task.description, 60) : task.kind}${task.description ? ` · ${task.kind}` : ''}${task.lastTool ? ` · ${task.lastTool}` : ''} · ${elapsed(task.at, now)} ago`),\n ...(ended > 0 ? [`↳ ${ended} completed`] : []),\n ];\n}\n\nexport type Queued = { text: string; mission: string | null };\n\nexport function stillAsked(queued: readonly Queued[], missions: readonly Mission[] | null): Queued[] {\n if (!missions) return [...queued];\n const kept = queued.filter((one) => one.mission === null || missions.some((mission) => mission.missionId === one.mission && mission.state === 'waiting'));\n return kept.filter((one, index) => one.mission === null || !kept.slice(index + 1).some((later) => later.mission === one.mission));\n}\n\nexport type Segment = { text: string; tone?: 'yellow' | 'gray' | 'cyan' | 'green' | 'red'; dim?: boolean; bold?: boolean };\nexport type ViewTarget = { kind: 'mission'; node: string; missionId: string; sessionId: string } | { kind: 'member'; node: string; team: string; agent: string } | { kind: 'session'; node: string; sessionId: string } | { kind: 'worker'; node: string; sessionId: string; taskId: string; label: string } | { kind: 'tasks'; node: string; sessionId: string };\nexport type Frame = { header: Segment[][]; rows: Array<{ lines: Segment[][]; target: ViewTarget | null }> };\n\nconst TONES = ['yellow', 'gray', 'cyan', 'green', 'red'] as const;\n\nfunction segmentsOf(value: unknown): Segment[][] {\n if (!Array.isArray(value)) return [];\n return value.map((line) => (Array.isArray(line) ? line.flatMap((segment): Segment[] => {\n if (!isRecord(segment) || typeof segment.text !== 'string') return [];\n const tone = TONES.find((one) => one === segment.tone);\n return [{ text: segment.text, ...(tone && { tone }), ...(segment.dim === true && { dim: true }), ...(segment.bold === true && { bold: true }) }];\n }) : []));\n}\n\nfunction targetOfView(value: unknown): ViewTarget | null {\n if (!isRecord(value)) return null;\n const node = text(value.node);\n if (!node) return null;\n switch (value.kind) {\n case 'mission': {\n const missionId = text(value.missionId);\n const sessionId = text(value.sessionId);\n return missionId && sessionId ? { kind: 'mission', node, missionId, sessionId } : null;\n }\n case 'session': {\n const sessionId = text(value.sessionId);\n return sessionId ? { kind: 'session', node, sessionId } : null;\n }\n case 'worker': {\n const sessionId = text(value.sessionId);\n const taskId = text(value.taskId);\n return sessionId && taskId ? { kind: 'worker', node, sessionId, taskId, label: text(value.label) ?? 'worker' } : null;\n }\n case 'member': {\n const team = text(value.team);\n const agent = text(value.agent);\n return team && agent ? { kind: 'member', node, team, agent } : null;\n }\n case 'tasks': {\n const sessionId = text(value.sessionId);\n return sessionId ? { kind: 'tasks', node, sessionId } : null;\n }\n default:\n return null;\n }\n}\n\nexport function frameOf(record: unknown): Frame | null {\n if (!isRecord(record) || record.kind !== 'view' || !Array.isArray(record.rows)) return null;\n return {\n header: segmentsOf(record.header),\n rows: record.rows.flatMap((row) => (isRecord(row) ? [{ lines: segmentsOf(row.lines), target: targetOfView(row.target) }] : [])),\n };\n}\n\nexport function fitLine(line: readonly Segment[], width: number): Segment[] {\n const fitted: Segment[] = [];\n let used = 0;\n for (const segment of line) {\n if (used >= width) break;\n const room = width - used;\n if (segment.text.length <= room) {\n fitted.push(segment);\n used += segment.text.length;\n continue;\n }\n fitted.push({ ...segment, text: `${segment.text.slice(0, Math.max(0, room - 1))}…` });\n used = width;\n }\n return fitted;\n}\n\nexport function opensTranscript(target: ViewTarget | null): target is Extract<ViewTarget, { kind: 'mission' | 'session' | 'worker' }> {\n return target !== null && (target.kind === 'mission' || target.kind === 'session' || target.kind === 'worker');\n}\n","hooks/register.tsx":"import { atom, read, update } from 'claude-code'\nimport type { EngineInterface, Register } from 'claude-code'\n\nimport type { OrbitalAttached, OrbitalFrame, OrbitalJoined, OrbitalMission, OrbitalPeer, OrbitalSegment, OrbitalTarget, OrbitalTranscript } from '../types'\nimport { DETACHED_PROBLEM, attachArgv, attachedOf, backgroundLines, bandText, binCandidates, changedMissions, checkAnswerOf, decisionOf, deliveryOf, detachedOf, elapsed, framed, isMissionTarget, joinTarget, stillAsked, linesOf, missionDetail, missionOrder, missionsOf, fitLine, frameOf, opensTranscript, peersOf, recordsOf, sessionHeader, stateMark, toastOf, transcriptOf, versionOf, waitingNotice } from './logic.ts'\nimport type { Delivery, Queued } from './logic.ts'\n\nconst PANE = 'orbital'\nconst attached = atom({ plugin: 'orbital', key: 'attached' } as const, null as OrbitalAttached | null, { shape: 'attached-1' })\nconst peers = atom({ plugin: 'orbital', key: 'peers' } as const, [] as OrbitalPeer[], { shape: 'peers-2' })\nconst missions = atom({ plugin: 'orbital', key: 'missions' } as const, [] as OrbitalMission[], { shape: 'missions-2' })\nconst frame = atom({ plugin: 'orbital', key: 'frame' } as const, null as OrbitalFrame | null, { shape: 'frame-1' })\nconst queued = atom({ plugin: 'orbital', key: 'queued' } as const, [] as Queued[], { shape: 'queued-2' })\nconst detail = atom({ plugin: 'orbital', key: 'detail' } as const, null as string[] | null, { shape: 'detail-1' })\nconst activity = atom({ plugin: 'orbital', key: 'activity' } as const, [] as string[], { shape: 'activity-1' })\nconst pending = atom({ plugin: 'orbital', key: 'pending' } as const, [] as string[], { shape: 'pending-1' })\nconst isTurnRunning = atom({ plugin: 'orbital', key: 'isTurnRunning' } as const, false, { shape: 'running-1' })\nconst seen = atom({ plugin: 'orbital', key: 'seen' } as const, [] as string[], { shape: 'seen-1' })\nconst problem = atom({ plugin: 'orbital', key: 'problem' } as const, null as string | null, { shape: 'problem-1' })\nconst joinedSession = atom({ plugin: 'orbital', key: 'joined' } as const, null as OrbitalJoined | null, { shape: 'joined-1' })\nconst transcript = atom({ plugin: 'orbital', key: 'transcript' } as const, null as OrbitalTranscript | null, { shape: 'transcript-2' })\nconst isCommandRunning = atom({ plugin: 'orbital', key: 'isCommandRunning' } as const, false, { shape: 'command-1' })\nconst said = atom({ plugin: 'orbital', key: 'said' } as const, [] as string[], { shape: 'said-1' })\nconst program = atom({ plugin: 'orbital', key: 'program' } as const, null as string[] | null, { shape: 'program-1' })\n\nconst TRANSCRIPT_LIMIT = 30\n\ntype Options = { orbital_bin?: string; node?: string; team?: string; governance?: string }\n\nasync function note($: EngineInterface, line: string): Promise<void> {\n await update($, activity, (list) => [...list, line].slice(-50))\n}\n\nasync function deliver($: EngineInterface, delivery: Delivery): Promise<void> {\n if (!delivery.mission) await update($, pending, (list) => [...list, delivery.taskId])\n await note($, delivery.mission ? `${delivery.mission} asks you` : `message from ${delivery.fromAgent ?? 'the node'}`)\n $.ui.toast(delivery.mission ? `orbital: ${delivery.mission} waits for you` : `orbital: message from ${delivery.fromAgent ?? 'the node'}`)\n if (await read($, isCommandRunning)) {\n await update($, queued, (list) => [...list, { text: framed(delivery), mission: delivery.mission }])\n return\n }\n if (await read($, isTurnRunning)) {\n if (delivery.mission) {\n await update($, queued, (list) => [...list, { text: framed(delivery), mission: delivery.mission }])\n return\n }\n await $.session.append({ message: { type: 'user', content: [{ type: 'text', text: framed(delivery) }] } })\n return\n }\n await $.prompt.submit({ text: framed(delivery) })\n void refresh($)\n}\n\nasync function getJson($: EngineInterface, joined: OrbitalAttached, path: string): Promise<unknown> {\n const response = await $.http.fetch(`${joined.nodeUrl}${path}`, { headers: { authorization: `Bearer ${joined.token}` } })\n return response.ok ? JSON.parse(response.text) : null\n}\n\nasync function flush($: EngineInterface): Promise<void> {\n if (await read($, isTurnRunning) || await read($, isCommandRunning)) return\n const queuedNow = await read($, queued)\n if (queuedNow.length === 0) return\n const joined = await read($, attached)\n const asking = queuedNow.some((one) => one.mission !== null) && joined ? missionsOf(await getJson($, joined, '/orbital/missions')) : null\n const waiting = stillAsked(queuedNow, asking)\n await update($, queued, () => [])\n if (waiting.length === 0) return\n void $.prompt.submit({ text: waiting.map((one) => one.text).join('\\n\\n') })\n}\n\nasync function refresh($: EngineInterface): Promise<void> {\n const joined = await read($, attached)\n if (!joined) return\n const found = peersOf(await getJson($, joined, '/orbital/peers'))\n if (found && JSON.stringify(found) !== JSON.stringify(await read($, peers))) await update($, peers, () => found)\n const listed = missionsOf(await getJson($, joined, '/orbital/missions'))\n if (listed) {\n const before = await read($, missions)\n const changed = changedMissions(before, listed)\n for (const one of changed) $.ui.toast(toastOf(one))\n const notice = waitingNotice(listed, joined.sessionId, await read($, seen))\n await update($, seen, () => notice.seen)\n for (const one of notice.fresh) {\n if (!changed.some((known) => known.node === one.node && known.missionId === one.missionId)) $.ui.toast(toastOf(one))\n await note($, `${one.missionId} waits for you`)\n }\n if (JSON.stringify(listed) !== JSON.stringify(before)) await update($, missions, () => listed)\n }\n await follow($, joined)\n}\n\nasync function follow($: EngineInterface, joined: OrbitalAttached): Promise<void> {\n const target = await read($, joinedSession)\n if (!target) return\n const response = await $.http.fetch(`${joined.nodeUrl}/orbital/sessions/${encodeURIComponent(target.sessionId)}/transcript?limit=${TRANSCRIPT_LIMIT}${target.worker ? `&worker=${encodeURIComponent(target.worker)}` : ''}`, { headers: { authorization: `Bearer ${joined.token}` } })\n const body: unknown = response.ok ? JSON.parse(response.text) : null\n const parsed = transcriptOf(body)\n const error = parsed ? null : (body && typeof body === 'object' && 'error' in body ? String((body as { error: unknown }).error) : `The node answered ${response.status}.`)\n const next: OrbitalTranscript = parsed ? { ...parsed, error: null } : { session: null, lines: [], error: response.ok ? 'The node sent no transcript.' : error ?? `The node answered ${response.status}.` }\n if (JSON.stringify(next) !== JSON.stringify(await read($, transcript))) await update($, transcript, () => next)\n}\n\nasync function attach($: EngineInterface, argv: string[]): Promise<OrbitalAttached | null> {\n let ready: (joined: OrbitalAttached | null) => void = () => undefined\n const first = new Promise<OrbitalAttached | null>((done) => { ready = done })\n void (async () => {\n let carry = ''\n let mine: string | null = null\n for await (const piece of $.process.spawn({ argv })) {\n if (!('text' in piece) || piece.stream !== 'stdout') continue\n const parsed = recordsOf(piece.text, carry)\n carry = parsed.carry\n for (const record of parsed.records) {\n const joined = attachedOf(record)\n if (joined) {\n mine = joined.sessionId\n await update($, attached, () => joined)\n await update($, problem, () => null)\n await update($, seen, () => [])\n await note($, `joined node ${joined.node} as ${joined.sessionId}`)\n ready(joined)\n continue\n }\n if (detachedOf(record) !== null) {\n await update($, attached, () => null)\n await update($, problem, () => DETACHED_PROBLEM)\n await note($, 'the node is gone')\n continue\n }\n const delivery = deliveryOf(record)\n if (delivery) await deliver($, delivery)\n }\n }\n if (mine && (await read($, attached))?.sessionId === mine) await update($, attached, () => null)\n if (mine && (await read($, problem)) === DETACHED_PROBLEM) await update($, problem, () => 'The link to the node ended. Start a new session to join it again.')\n ready(null)\n })()\n return first\n}\n\nasync function controlMission($: EngineInterface, verb: 'pause' | 'resume', missionId: string): Promise<string> {\n const joined = await read($, attached)\n if (!joined) return (await read($, problem)) ?? 'Not joined to a node yet.'\n const response = await $.http.fetch(`${joined.nodeUrl}/orbital/missions/${verb}`, {\n method: 'POST',\n headers: { authorization: `Bearer ${joined.token}`, 'content-type': 'application/json' },\n body: JSON.stringify({ missionId }),\n })\n const body: unknown = JSON.parse(response.text || '{}')\n if (!response.ok) return `Not ${verb === 'pause' ? 'paused' : 'resumed'}: ${body && typeof body === 'object' && 'error' in body ? String((body as { error: unknown }).error) : `the node answered ${response.status}`}`\n await refresh($)\n const state = body && typeof body === 'object' && 'state' in body ? String((body as { state: unknown }).state) : verb === 'pause' ? 'paused' : 'active'\n return `${missionId} is ${state}.`\n}\n\nconst VIEW_WIDTH = 60\n\nasync function watchView($: EngineInterface, bin: readonly string[]): Promise<void> {\n let carry = ''\n for await (const piece of $.process.spawn({ argv: [...bin, 'view', '--watch', '--width', String(VIEW_WIDTH), '--output', 'ndjson'] })) {\n if (!('text' in piece) || piece.stream !== 'stdout') continue\n const parsed = recordsOf(piece.text, carry)\n carry = parsed.carry\n for (const record of parsed.records) {\n const next = frameOf(record)\n if (next) await update($, frame, () => next)\n }\n }\n}\n\nasync function openTarget($: EngineInterface, target: OrbitalTarget): Promise<string> {\n if (target.kind === 'mission') return join($, target.missionId)\n if (target.kind !== 'session' && target.kind !== 'worker') return 'This row opens nothing.'\n const bin = await read($, program)\n if ((await read($, attached))?.node !== target.node) {\n if (!bin) return (await read($, problem)) ?? 'No orbital program on the PATH.'\n const moved = await attach($, attachArgv(bin, { cwd: await $.session.cwd(), sessionId: await $.session.id(), node: target.node, team: '' }))\n if (!moved) return `The node ${target.node} did not take this session.`\n }\n const label = target.kind === 'worker' ? `the worker ${target.label}` : `session ${target.sessionId.slice(0, 8)}`\n await update($, joinedSession, () => ({ sessionId: target.sessionId, label, worker: target.kind === 'worker' ? target.taskId : null }))\n await update($, transcript, () => null)\n await update($, detail, () => null)\n const joined = await read($, attached)\n if (joined) await follow($, joined)\n return `orbital follows ${label} on ${target.node}.`\n}\n\nconst TONE = { yellow: 'warning', green: 'success', red: 'error', cyan: 'suggestion', gray: undefined } as const\n\nasync function claimMission($: EngineInterface, missionId: string): Promise<string | null> {\n const joined = await read($, attached)\n const runsHere = joined && (await read($, missions)).some((mission) => mission.missionId === missionId && mission.node === joined.node && (mission.state === 'active' || mission.state === 'waiting' || mission.state === 'paused'))\n if (joined) {\n const response = await $.http.fetch(`${joined.nodeUrl}/orbital/missions/join`, {\n method: 'POST',\n headers: { authorization: `Bearer ${joined.token}`, 'content-type': 'application/json' },\n body: JSON.stringify({ missionId }),\n })\n if (response.ok) return null\n const body: unknown = JSON.parse(response.text || '{}')\n if (runsHere) return body && typeof body === 'object' && 'error' in body ? String((body as { error: unknown }).error) : `The node answered ${response.status}.`\n }\n const bin = await read($, program)\n if (!bin) return (await read($, problem)) ?? 'No orbital program on the PATH.'\n const moved = await attach($, attachArgv(bin, { cwd: await $.session.cwd(), sessionId: await $.session.id(), node: '', team: '', mission: missionId }))\n if (!moved) return `No running node on this machine runs the mission ${missionId}, or it refused this session. orbital mission list shows the missions.`\n await refresh($)\n return null\n}\n\nasync function join($: EngineInterface, target: string): Promise<string> {\n await refresh($)\n const asked = target.trim()\n const mission = isMissionTarget(asked, await read($, missions), await read($, peers))\n const refused = mission ? await claimMission($, asked) : null\n if (refused) return refused\n const chosen = joinTarget(target, await read($, missions), await read($, peers))\n if ('error' in chosen) return chosen.error\n await update($, joinedSession, () => chosen)\n await update($, transcript, () => null)\n await update($, detail, () => null)\n const joined = await read($, attached)\n if (joined) await follow($, joined)\n return mission ? `orbital follows ${chosen.label} (session ${chosen.sessionId.slice(0, 8)}): its questions come to this session, and anything the person says to the lead goes to it at once with missionSay ${asked} or /orbital say <message>.` : `orbital follows ${chosen.label} (session ${chosen.sessionId.slice(0, 8)}): /orbital say <message> talks to it.`\n}\n\nconst DELIVERY: Record<string, string> = { steer: 'the lead takes it during its current turn', turn: 'the lead takes it as its next turn', reply: 'it answers the question of the mission' }\n\nasync function say($: EngineInterface, text: string): Promise<string> {\n const joined = await read($, attached)\n const following = await read($, joinedSession)\n if (!joined || !following) return 'Follow a lead first: /orbital join <mission>.'\n if (following.worker) return 'A worker takes no message: talk to its lead. /orbital goes back.'\n if (!text) return 'Write the message: /orbital say <message>.'\n await update($, said, (list) => [...list, text])\n const response = await $.http.fetch(`${joined.nodeUrl}/orbital/missions/say`, {\n method: 'POST',\n headers: { authorization: `Bearer ${joined.token}`, 'content-type': 'application/json' },\n body: JSON.stringify({ target: following.sessionId, text }),\n })\n const body: unknown = JSON.parse(response.text || '{}')\n if (!response.ok) {\n await update($, said, (list) => list.filter((one) => one !== text))\n return `Not sent: ${body && typeof body === 'object' && 'error' in body ? String((body as { error: unknown }).error) : `the node answered ${response.status}`}`\n }\n const delivery = body && typeof body === 'object' && 'delivery' in body ? String((body as { delivery: unknown }).delivery) : ''\n return `Sent to ${following.label}: ${DELIVERY[delivery] ?? 'the lead has it'}.`\n}\n\nasync function leave($: EngineInterface): Promise<void> {\n await update($, joinedSession, () => null)\n await update($, transcript, () => null)\n}\n\nasync function detailOf($: EngineInterface, id: string): Promise<string[]> {\n const joined = await read($, attached)\n if (!joined) return [await read($, problem) ?? 'Not joined to a node yet.']\n await refresh($)\n const mission = (await read($, missions)).find((one) => one.missionId === id)\n if (!mission) return [`No mission ${id}. /orbital missions lists them.`]\n const activity = linesOf(await getJson($, joined, `/orbital/missions/${encodeURIComponent(id)}/activity?limit=20`))\n return missionDetail(mission, activity)\n}\n\nfunction missionList(list: readonly OrbitalMission[], now: number): string[] {\n if (list.length === 0) return ['No mission.']\n return missionOrder(list).map((mission) => `${stateMark(mission.state, 0).mark} ${mission.missionId}${mission.team ? ` · team ${mission.team}` : ''}${mission.lead ? ` · lead ${mission.lead}` : ''} · ${mission.state}${mission.state === 'active' ? ` ${elapsed(mission.startedAt, now)}` : ''}${mission.state === 'waiting' ? (mission.question ? ` · asks: ${mission.question}` : ' · stopped without a hand-off') : ''}${mission.state === 'paused' ? ` · resume with orbital mission resume ${mission.missionId}` : ''}`)\n}\n\nexport const register: Register = (on, options) => {\n const config = (options ?? {}) as Options\n const enforce = config.governance !== 'observe'\n\n on('session.start', async ($, e, next) => {\n const started = await next(e)\n await $.command.register({ name: 'orbital', description: 'Show the missions of orbital, the sessions you reach and the lan peers, in a pane', argumentHint: '[missions | mission <id> | join <mission-or-session> | say <message> | pause <id> | resume <id>]' }).catch(() => undefined)\n void (async () => {\n let bin: string[] | null = null\n for (const candidate of binCandidates(config.orbital_bin ?? '')) {\n try {\n const ran = await $.process.run([...candidate, '--version', '--output', 'json'], { timeoutMs: 20000 })\n if (ran.exitCode === 0 && versionOf(ran.stdout) !== null) {\n bin = candidate\n break\n }\n } catch {\n continue\n }\n }\n if (!bin) {\n await update($, problem, () => 'No orbital program on the PATH. Install it with: npm i -g <scope>/orbital')\n $.ui.toast('orbital: no orbital program on the PATH. Install it, then start a new session.')\n return\n }\n await update($, program, () => bin)\n void watchView($, bin)\n const joined = await attach($, attachArgv(bin, { cwd: await $.session.cwd(), sessionId: await $.session.id(), node: config.node ?? '', team: config.team ?? '' }))\n if (!joined && !(await read($, attached))) await update($, problem, () => 'No orbital node runs for this project. Start the nodes with: orbital up, or join a mission of another node with: /orbital join <mission>')\n })()\n void $.clock.every(2000, () => void refresh($).then(() => flush($)))\n return started\n })\n\n on('turn.start', async ($, e, next) => {\n await update($, isTurnRunning, () => true)\n return next(e)\n })\n\n on('turn.complete', async ($, e, next) => {\n const done = await next(e)\n await update($, isTurnRunning, () => false)\n await flush($)\n const joined = await read($, attached)\n const open = await read($, pending)\n if (!joined || open.length === 0) return done\n const [taskId, ...rest] = open\n await update($, pending, () => rest)\n const answer = 'answer' in e && typeof e.answer === 'string' ? e.answer : ''\n await $.http.fetch(`${joined.nodeUrl}/orbital/sessions/${encodeURIComponent(joined.sessionId)}/settled`, {\n method: 'POST',\n headers: { authorization: `Bearer ${joined.token}`, 'content-type': 'application/json' },\n body: JSON.stringify({ taskId, text: answer }),\n })\n await note($, `answered ${taskId}`)\n return done\n })\n\n on('tool.check', async ($, e, next) => {\n const joined = await read($, attached)\n if (!joined) return next(e)\n const response = await $.http.fetch(`${joined.nodeUrl}/orbital/policy/tool`, {\n method: 'POST',\n headers: { authorization: `Bearer ${joined.token}`, 'content-type': 'application/json' },\n body: JSON.stringify({ toolName: e.tool, input: e.input ?? {}, ask: 'caller' }),\n })\n const decision = response.ok ? decisionOf(JSON.parse(response.text)) : null\n if (!decision) {\n if (!enforce) return next(e)\n return { decision: 'deny', reason: `orbital: the node ${joined.node} gave no decision (${response.status})` }\n }\n const answer = checkAnswerOf(decision, e.approver !== undefined)\n if (answer.decision === 'allow') return next(e)\n await note($, `${answer.decision} ${e.tool}: ${answer.reason}`)\n if (answer.decision === 'deny') $.ui.toast(`orbital denied ${e.tool}: ${answer.reason}`)\n if (!enforce) return next(e)\n return answer\n }).catch(($, e, next) => (enforce ? { decision: 'deny', reason: 'orbital: the node did not answer, and governance is enforce' } : next(e)))\n\n on('command.run', { command: 'orbital' }, async ($, e) => {\n const args = e.args.trim()\n if (args === 'missions') {\n await refresh($)\n return { text: missionList(await read($, missions), Date.now()).join('\\n') }\n }\n if (args.startsWith('join')) {\n await update($, isCommandRunning, () => true)\n const said = await join($, args.slice('join'.length)).finally(() => update($, isCommandRunning, () => false))\n const placed = await $.ui.open({ id: PANE, title: 'orbital' })\n const shown = await read($, transcript)\n return { text: placed.isPlaced || !shown ? said : [said, ...(shown.session ? [sessionHeader(shown.session, Date.now())] : []), ...shown.lines.slice(-12).map((line) => `${line.mark} ${line.text}`)].join('\\n') }\n }\n if (args === 'say' || args.startsWith('say ')) {\n const answer = await say($, args.slice('say'.length).trim())\n await $.ui.open({ id: PANE, title: 'orbital' })\n return { text: answer }\n }\n const control = /^(pause|resume)\\s+(\\S+)$/.exec(args)\n if (control) return { text: await controlMission($, control[1] as 'pause' | 'resume', control[2]!) }\n if (args.startsWith('mission ')) {\n const lines = await detailOf($, args.slice('mission '.length).trim())\n await update($, detail, () => lines)\n const placed = await $.ui.open({ id: PANE, title: 'orbital' })\n return { text: placed.isPlaced ? 'orbital pane shows the mission.' : lines.join('\\n') }\n }\n await update($, detail, () => null)\n await leave($)\n await refresh($)\n const placed = await $.ui.open({ id: PANE, title: 'orbital' })\n const joined = await read($, attached)\n return { text: placed.isPlaced ? 'orbital pane opened.' : joined ? missionList(await read($, missions), Date.now()).join('\\n') : (await read($, problem)) ?? 'Not joined to a node yet.' }\n })\n\n on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {\n const { Box, Text, Button } = $.ui.resolve(e)\n const joined = await read($, attached)\n const view = await read($, frame)\n const width = Math.max(30, Math.min(VIEW_WIDTH, (e.props as { bodyColumns?: number } | undefined)?.bodyColumns ?? VIEW_WIDTH))\n const line = (segments: readonly OrbitalSegment[]) => (\n <Box flexDirection=\"row\">\n {fitLine(segments, width).map((segment) => <Text color={segment.tone ? TONE[segment.tone] : undefined} dimColor={segment.dim === true || segment.tone === 'gray'} bold={segment.bold === true}>{segment.text}</Text>)}\n </Box>\n )\n if (!joined && !view) return <Text dimColor>{(await read($, problem)) ?? 'Not joined to a node yet.'}</Text>\n const following = await read($, joinedSession)\n if (following && joined) {\n const shown = await read($, transcript)\n const room = Math.max(4, (e.viewport?.rows ?? 30) - 5)\n const colour = { plain: undefined, dim: undefined, good: 'success', bad: 'error', busy: 'warning', ask: 'suggestion' } as const\n return (\n <Box flexDirection=\"column\">\n <Box flexDirection=\"row\">\n <Button key=\"back\" plain onPress={() => void leave($)}>missions</Button>\n <Text dimColor>{` orbital · ${joined.node}`}</Text>\n </Box>\n <Text bold>{following.worker ? following.label : shown?.session ? sessionHeader(shown.session, Date.now()) : following.label}</Text>\n {(shown?.session ? backgroundLines(shown.session, Date.now()) : []).map((line) => <Text dimColor>{` ${line}`}</Text>)}\n {!shown && <Text dimColor>Reading the transcript…</Text>}\n {shown?.error && <Text color=\"error\">{shown.error}</Text>}\n {(shown?.lines ?? []).slice(-room).map((line) => (\n <Box flexDirection=\"row\">\n <Text color={colour[line.tone]}>{`${line.mark} `}</Text>\n <Text dimColor={line.tone === 'dim'} color={line.tone === 'bad' ? 'error' : line.tone === 'ask' ? 'suggestion' : undefined}>{line.text}</Text>\n </Box>\n ))}\n {shown?.session?.isRunning && <Text color=\"warning\">{`✻ ${shown.session.doing ?? 'Working'}… (${elapsed(shown.session.since, Date.now()) || '0s'})`}</Text>}\n {(await read($, said)).filter((text) => !(shown?.lines ?? []).some((line) => line.text.includes(text.slice(0, 60)))).map((text) => (\n <Box flexDirection=\"row\">\n <Text>{'> '}</Text>\n <Text>{text}</Text>\n <Text dimColor>{' (queued)'}</Text>\n </Box>\n ))}\n <Text dimColor>{`/orbital say <message> talks to the lead · /orbital goes back`}</Text>\n </Box>\n )\n }\n const shown = await read($, detail)\n if (shown) {\n return (\n <Box flexDirection=\"column\">\n {shown.map((line) => <Text>{line}</Text>)}\n <Text dimColor>/orbital goes back to the missions.</Text>\n </Box>\n )\n }\n if (!view) return <Text dimColor>{`orbital · ${joined?.node ?? ''} · reading the nodes…`}</Text>\n return (\n <Box flexDirection=\"column\">\n {view.header.map((segments) => line(segments))}\n {view.rows.map((row, index) => {\n const target = row.target\n if (!opensTranscript(target)) return <Box flexDirection=\"column\">{row.lines.map((segments) => line(segments))}</Box>\n return (\n <Box flexDirection=\"column\">\n <Button key={`row:${index}`} plain onPress={() => void openTarget($, target)}>{fitLine(row.lines[0] ?? [], width).map((segment) => segment.text).join('')}</Button>\n {row.lines.slice(1).map((segments) => line(segments))}\n </Box>\n )\n })}\n {!joined && <Text dimColor>{(await read($, problem)) ?? 'Not joined to a node.'}</Text>}\n </Box>\n )\n })\n\n on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {\n const joined = await read($, attached)\n if (!joined) return next(e)\n const reached = (await read($, peers)).filter((peer) => !peer.self).length\n const open = await read($, pending)\n const { Text } = $.ui.resolve(e)\n return <Text dimColor>{bandText(joined.node, await read($, missions), reached, open.length)}</Text>\n })\n}\n","skills/orbital-team/SKILL.md":"---\nname: orbital-team\ndescription: Use when this session works with other agent sessions through an orbital node - to see the peers of the team, send a peer a message and wait for the answer, dispatch a job to an agent of the node, answer a message that a peer sent, or give a team a mission, such as work on a GitHub issue.\n---\n\n# Work with the team through orbital\n\nThis session is a member of a team on an orbital node.\nThe node carries the messages between the sessions of the team, on any harness.\n\n## The tools\n\n- `agentPeers` lists the sessions of the team: the agent, the session id, the role, the harness and the status.\n- `agentMessage` sends a message to a session by its id. A session that works takes it during its current turn. With `wait: true`, it returns the answer of the receiver.\n- `agentDispatch` starts an agent of the node on a task, in a session of its own, and `agentWait` waits for its result.\n- `agentList` lists the agents of the node.\n- `agentInbox` takes the messages that peers sent this session, and `agentAnswer` answers one by its `taskId`.\n- `missionNew` writes a mission for a team, `missionStart` starts it, and `missionList` and `missionShow` report its state and result.\n- `sessionTranscript` reads what a session does now: its state, its current action and its last transcript entries.\n Give it a session id, or a mission id for the lead of that mission.\n- `missionSay` gives the lead of a mission a message from the person: during its current turn when it works, as its next turn when it is idle, and as the answer when the mission waits.\n- `missionReply` answers the question of a waiting mission; the lead resumes with the answer.\n- `missionJoin` makes this session the giver of a running mission: the questions of its lead come to this session.\n- `missionPause` stops the lead of an active or waiting mission and keeps its session; `missionResume` lets the lead go on in that session.\n\n## Missions\n\nA mission is one objective of a team, from its intent to its hand-off.\nWhen the person asks a team to work on something, such as \"build, take issue 123\", give the team a mission:\n\n1. Read the source of the work, for example `gh issue view 123`.\n2. Write the mission with `missionNew`: the team, an id such as `issue-123`, the person as `given-by`, and a brief.\n The node writes `missions/<id>/MISSION.md`. One id names one mission on a node, so choose an id that no other mission has.\n The intent says what to do and links the source.\n The end state says what is true when the mission is done, for example \"a PR off main closes #123\".\n The limits say what the team must not do.\n3. Start it with `missionStart`, and tell the person the id.\n4. When the person asks, report the state and the result with `missionShow`.\n5. When the person asks what the lead does, read it with `sessionTranscript` on the mission id.\n Tell the person the current action and the last steps, in a few lines.\n6. When the person says something to the lead, send it at once with `missionSay`, in the words of the person.\n Do this whether or not the lead asked a question: a lead that works takes it during its turn, and a waiting mission takes it as the answer.\n Never hold a message back until the lead asks.\n7. When the person asks to stop a mission for now, pause it with `missionPause`, and resume it with `missionResume` when they ask.\n A paused mission takes no reply: resume it first.\n A node that stops pauses each active mission, and no lead goes on until a person resumes it.\n\nThe person follows a lead live with `/orbital join <mission>` in the orbital pane, or with `orbital session join <mission>` in a terminal. `/orbital join` of a mission works from any folder: the session attaches to the node that runs the mission and becomes its giver, so each question of the lead comes to the session. Show the person the question, and send their answer with `missionSay`.\nWhile the session follows a lead, everything the person addresses to the lead goes to it with `missionSay`; the person can also type `/orbital say <message>`, or run `orbital mission say <mission> \"<message>\"` in a terminal.\n\n## The rules\n\n- A message from a peer arrives as a turn that starts with `[orbital] message from`.\n Answer it in that turn: the answer of the turn goes back to the sender.\n- State a message in full, because the receiver has none of your context.\n- Do not wait on a peer that may be waiting on you.\n- As the lead of a team, run long work in the background: start a worker (a member with `agentDispatch`, or a sub-agent in the background), end your turn, and answer the person.\n Do not sleep or poll for a worker: each result wakes you with a new turn, and the mission stays active while your workers run.\n A sub-agent whose type is the name of a team member shows as the work of that member.\n- End a turn with a question only when you need an answer from the person: a turn that ends with no hand-off and no running work makes the mission wait.\n- A tool call that the policy of the node denies fails with the policy name in the reason. Do not try the same call another way.\n","types/index.d.ts":"export type OrbitalAttached = { sessionId: string; token: string; node: string; nodeUrl: string };\nexport type OrbitalPeer = { sessionId: string; agent: string; role: string; harness: string; status: string; self: boolean; parent: string | null };\nexport type OrbitalMission = { missionId: string; node: string; team: string | null; state: 'open' | 'active' | 'waiting' | 'paused' | 'done' | 'failed'; lead: string | null; giver: string | null; question: string | null; result: string | null; brief: string | null; claim: string | null; claimedAt: string | null; stale: boolean; lastActivity: string | null; sessionId: string | null; startedAt: string | null; history: Array<{ state: string; at: string; by: string; note: string | null }> };\nexport type OrbitalBackground = { id: string; kind: string; description: string; state: string; lastTool: string | null; at: string };\nexport type OrbitalJoined = { sessionId: string; label: string; worker?: string | null };\nexport type OrbitalSegment = { text: string; tone?: 'yellow' | 'gray' | 'cyan' | 'green' | 'red'; dim?: boolean; bold?: boolean };\nexport type OrbitalTarget = { kind: 'mission'; node: string; missionId: string; sessionId: string } | { kind: 'member'; node: string; team: string; agent: string } | { kind: 'session'; node: string; sessionId: string } | { kind: 'worker'; node: string; sessionId: string; taskId: string; label: string } | { kind: 'tasks'; node: string; sessionId: string };\nexport type OrbitalFrame = { header: OrbitalSegment[][]; rows: Array<{ lines: OrbitalSegment[][]; target: OrbitalTarget | null }> };\nexport type OrbitalTranscript = {\n session: { id: string; agent: string; status: string; isRunning: boolean; team: string | null; mission: string | null; since: string | null; doing: string | null; lastActivityAt: string | null; background: OrbitalBackground[] } | null;\n lines: Array<{ mark: string; tone: 'plain' | 'dim' | 'good' | 'bad' | 'busy' | 'ask'; text: string }>;\n error: string | null;\n};\n\ndeclare module 'claude-code' {\n interface PluginState {\n orbital: {\n attached: Shaped<OrbitalAttached | null>;\n peers: Shaped<OrbitalPeer[]>;\n missions: Shaped<OrbitalMission[]>;\n frame: Shaped<OrbitalFrame | null>;\n detail: Shaped<string[] | null>;\n queued: Shaped<Array<{ text: string; mission: string | null }>>;\n activity: Shaped<string[]>;\n pending: Shaped<string[]>;\n isTurnRunning: Shaped<boolean>;\n problem: Shaped<string | null>;\n seen: Shaped<string[]>;\n joined: Shaped<OrbitalJoined | null>;\n transcript: Shaped<OrbitalTranscript | null>;\n program: Shaped<string[] | null>;\n said: Shaped<string[]>;\n isCommandRunning: Shaped<boolean>;\n };\n }\n}\n"}}