@kaiba-cloud/orbital 0.0.6 → 0.0.7

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 CHANGED
@@ -1 +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.6\",\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; question: 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), question: text(record.question), text: record.text };\n}\n\nexport type Answered = { mission: string; question: string | null; by: string; text: string };\n\nexport function answeredOf(record: unknown): Answered | null {\n if (!isRecord(record) || record.kind !== 'answered') return null;\n const mission = text(record.mission);\n if (!mission || typeof record.text !== 'string') return null;\n return { mission, question: text(record.question), by: text(record.by) ?? 'another receiver', text: record.text };\n}\n\nexport function answeredText(answered: Answered): string {\n return `[orbital] ${answered.question ? `the question ${answered.question}` : 'the question'} of the mission ${answered.mission} is answered, by ${answered.by}:\\n\\n${answered.text}\\n\\nDo not answer it again, and tell the person it is answered.`;\n}\n\nexport function withoutAnswered(queued: readonly Queued[], answered: Answered): Queued[] {\n return queued.filter((one) => one.mission !== answered.mission || (answered.question !== null && one.question !== answered.question));\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 && delivery.question) return `[orbital] the lead of the mission ${delivery.mission} asks you the question ${delivery.question}, while it works:\\n\\n${delivery.text}\\n\\nShow the person this question and wait for their answer. Then send their answer to the lead with missionReply on ${delivery.mission} with the question ${delivery.question}, or run: orbital mission reply ${delivery.mission} \"<answer>\" --question ${delivery.question}.`;\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 receivers: string[];\n question: string | null;\n questions: Array<{ id: string; question: string; at: string }>;\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 receivers: Array.isArray(one.receivers) ? one.receivers.filter((key): key is string => typeof key === 'string') : [],\n question: text(one.question),\n questions: Array.isArray(one.questions) ? one.questions.flatMap((asked) => (isRecord(asked) && typeof asked.id === 'string' && typeof asked.question === 'string' ? [{ id: asked.id, question: asked.question, at: typeof asked.at === 'string' ? asked.at : '' }] : [])) : [],\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 waitsOnYou(mission: Mission): boolean {\n return mission.state === 'waiting' || (mission.state === 'active' && mission.questions.length > 0);\n}\n\nexport function shownState(mission: Mission): MissionState {\n return waitsOnYou(mission) ? 'waiting' : mission.state;\n}\n\nexport function missionOrder(missions: readonly Mission[]): Mission[] {\n return [...missions].sort((a, b) => ORDER[shownState(a)] - ORDER[shownState(b)] || 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(waitsOnYou).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 receiverKeys(sessionId: string, claudeSessionId: string): string[] {\n return [sessionId, `external:${claudeSessionId}`];\n}\n\nexport function waitingNotice(listed: readonly Mission[], mine: readonly string[], seen: readonly string[]): { fresh: Mission[]; seen: string[] } {\n const others = listed.filter((mission) => !mission.receivers.some((key) => mine.includes(key)));\n const keyed = [\n ...others.filter((mission) => mission.state === 'waiting').map((mission) => ({ key: `${mission.node}/${mission.missionId}`, mission })),\n ...others.flatMap((mission) => (mission.state === 'active' || mission.state === 'waiting' ? mission.questions.map((asked) => ({ key: `${mission.node}/${mission.missionId}/${asked.id}`, mission })) : [])),\n ];\n const fresh = keyed.filter((one) => !seen.includes(one.key)).map((one) => one.mission);\n return { fresh: fresh.filter((mission, index) => fresh.indexOf(mission) === index), seen: keyed.map((one) => one.key) };\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 ?? mission.questions.at(-1)?.question ?? 'its lead stopped without a hand-off'}`.slice(0, 200);\n case 'active':\n return `orbital: ${mission.missionId} asks you: ${mission.questions.at(-1)?.question ?? ''}`.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.questions.length > 0 ? ['', 'open questions of the lead:', ...mission.questions.map((asked) => ` ${asked.id}: ${asked.question}`), `answer one with missionReply on ${mission.missionId} with its question id, or run: orbital mission reply ${mission.missionId} \"<answer>\" --question <id>`] : []),\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; question: string | null };\n\nexport function stillAsked(queued: readonly Queued[], missions: readonly Mission[] | null): Queued[] {\n if (!missions) return [...queued];\n const open = (one: Queued) => missions.some((mission) => mission.missionId === one.mission && (one.question === null ? mission.state === 'waiting' : mission.questions.some((asked) => asked.id === one.question)));\n const kept = queued.filter((one) => one.mission === null || open(one));\n return kept.filter((one, index) => one.mission === null || !kept.slice(index + 1).some((later) => later.mission === one.mission && later.question === one.question));\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, answeredOf, answeredText, attachArgv, attachedOf, backgroundLines, bandText, binCandidates, changedMissions, checkAnswerOf, decisionOf, deliveryOf, detachedOf, elapsed, framed, isMissionTarget, joinTarget, stillAsked, linesOf, missionDetail, missionOrder, missionsOf, shownState, fitLine, frameOf, opensTranscript, peersOf, recordsOf, sessionHeader, stateMark, toastOf, transcriptOf, versionOf, receiverKeys, waitingNotice, withoutAnswered } from './logic.ts'\nimport type { Answered, 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-3' })\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-3' })\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' })\nconst link = atom({ plugin: 'orbital', key: 'link' } as const, 0, { shape: 'link-1' })\n\nconst TRANSCRIPT_LIMIT = 30\nconst RELINK_MS = 2000\nconst NO_NODE = 'No orbital node runs for this project. orbital joins it by itself when it runs. Start the nodes with: orbital up, or join a mission of another node with: /orbital join <mission>'\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, question: delivery.question }])\n return\n }\n if (await read($, isTurnRunning)) {\n if (delivery.mission) {\n await update($, queued, (list) => [...list, { text: framed(delivery), mission: delivery.mission, question: delivery.question }])\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 answered($: EngineInterface, answer: Answered): Promise<void> {\n const before = await read($, queued)\n const kept = withoutAnswered(before, answer)\n await update($, queued, () => kept)\n await note($, `${answer.mission} answered by ${answer.by}`)\n $.ui.toast(`orbital: ${answer.mission} is answered by ${answer.by}`)\n if (kept.length === before.length) await $.session.append({ message: { type: 'user', content: [{ type: 'text', text: answeredText(answer) }] } })\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, receiverKeys(joined.sessionId, await $.session.id()), 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\ntype Link = { id: number; first: Promise<OrbitalAttached | null>; ended: Promise<void> }\n\nasync function attach($: EngineInterface, argv: string[]): Promise<OrbitalAttached | null> {\n return (await linked($, argv)).first\n}\n\nasync function linked($: EngineInterface, argv: string[]): Promise<Link> {\n const id = (await read($, link)) + 1\n await update($, link, () => id)\n let ready: (joined: OrbitalAttached | null) => void = () => undefined\n const first = new Promise<OrbitalAttached | null>((done) => { ready = done })\n const ended = (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 answer = answeredOf(record)\n if (answer) {\n await answered($, answer)\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 ready(null)\n })()\n return { id, first, ended }\n}\n\nasync function stayJoined($: EngineInterface, argv: string[]): Promise<void> {\n for (;;) {\n const one = await linked($, argv)\n await one.ended.catch(() => undefined)\n if ((await read($, link)) !== one.id) return\n if (!(await read($, attached)) && (await read($, problem)) !== DETACHED_PROBLEM) await update($, problem, () => NO_NODE)\n await $.clock.sleep(RELINK_MS)\n if ((await read($, link)) !== one.id) return\n }\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(shownState(mission), 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.questions.map((asked) => ` · asks ${asked.id}: ${asked.question}`).join('')}${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 await stayJoined($, attachArgv(bin, { cwd: await $.session.cwd(), sessionId: await $.session.id(), node: config.node ?? '', team: config.team ?? '' }))\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 a question of a mission: the question of a waiting mission, or an open question its lead asked with `missionAsk`, by its question id. The answer reaches the lead and closes that question for every decision receiver.\n- `missionJoin` makes this session a decision receiver of a running mission: each question of its lead comes to this session, and the questions that are open now come at once. Any session can join, also this Claude Code session. A mission can have many receivers: each question goes to every live one, and the first answer closes it for all.\n This session stays a receiver after it or the node restarts, so join once. In a terminal of this session, `orbital mission join <mission>` does the same.\n- `missionLeave` removes this session from the decision receivers of a mission.\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- `missionAsk` asks the decision receivers of the mission of your session a question, as its lead, at any moment, also while your workers run. The node keeps each question open until a receiver answers it, shows it as waiting on the person, and pushes it at once to every live receiver.\n- `missionLink` adds a link to the mission of your session, as its lead: a label, a URL and a state, such as `PR #42`, its URL and `open`. A call with the same URL replaces the link.\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 one of its decision receivers, so each question of the lead comes to the session. Show the person the question, and send their answer with `missionSay`. A question that the lead asked with `missionAsk` names its question id: send the answer with `missionReply` and that question id, so that the answer closes the right question.\nWhen the person asks you to watch a mission, or to get its decisions, call `missionJoin` on it.\nA message that starts with `[orbital]` and says a question is answered means another receiver answered it: do not answer it again, and tell the person.\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- As the lead of a mission, report each pull request you open with `missionLink`: its label, its URL and the state `open`.\n When its state changes, such as to merged or closed, call `missionLink` again with the same URL and the new state.\n The person sees the links of each mission in the live view.\n- As the lead of a mission, ask each decision that you need with `missionAsk`, at once, also while your workers run.\n Never put a question only in your reply text: text that ends no turn reaches no one, and the mission does not show it.\n Ask each question once, with the options you see and what each one means. Several questions stay open side by side, and each answer comes back to you as a message that names its question.\n Go on with the work that does not depend on the answer.\n- When nothing is left to do but wait for an answer, end your turn with the question: 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; receivers: string[]; question: string | null; questions: Array<{ id: string; question: string; at: string }>; 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; question: 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 link: Shaped<number>;\n said: Shaped<string[]>;\n isCommandRunning: Shaped<boolean>;\n };\n }\n}\n"}}
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.7\",\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; question: 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), question: text(record.question), text: record.text };\n}\n\nexport type Answered = { mission: string; question: string | null; by: string; text: string };\n\nexport function answeredOf(record: unknown): Answered | null {\n if (!isRecord(record) || record.kind !== 'answered') return null;\n const mission = text(record.mission);\n if (!mission || typeof record.text !== 'string') return null;\n return { mission, question: text(record.question), by: text(record.by) ?? 'another receiver', text: record.text };\n}\n\nexport function answeredText(answered: Answered): string {\n return `[orbital] ${answered.question ? `the question ${answered.question}` : 'the question'} of the mission ${answered.mission} is answered, by ${answered.by}:\\n\\n${answered.text}\\n\\nDo not answer it again, and tell the person it is answered.`;\n}\n\nexport function withoutAnswered(queued: readonly Queued[], answered: Answered): Queued[] {\n return queued.filter((one) => one.mission !== answered.mission || (answered.question !== null && one.question !== answered.question));\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 && delivery.question) return `[orbital] the lead of the mission ${delivery.mission} asks you the question ${delivery.question}, while it works:\\n\\n${delivery.text}\\n\\nShow the person this question and wait for their answer. Then send their answer to the lead with missionReply on ${delivery.mission} with the question ${delivery.question}, or run: orbital mission reply ${delivery.mission} \"<answer>\" --question ${delivery.question}.`;\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 viewArgv(bin: readonly string[], options: { cwd: string; node: string; width: number }): string[] {\n return [...bin, 'view', '--watch', '--width', String(options.width), '--workspace', options.cwd, ...(options.node ? ['--node', options.node] : []), '--output', 'ndjson'];\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 receivers: string[];\n question: string | null;\n questions: Array<{ id: string; question: string; at: string }>;\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 receivers: Array.isArray(one.receivers) ? one.receivers.filter((key): key is string => typeof key === 'string') : [],\n question: text(one.question),\n questions: Array.isArray(one.questions) ? one.questions.flatMap((asked) => (isRecord(asked) && typeof asked.id === 'string' && typeof asked.question === 'string' ? [{ id: asked.id, question: asked.question, at: typeof asked.at === 'string' ? asked.at : '' }] : [])) : [],\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 waitsOnYou(mission: Mission): boolean {\n return mission.state === 'waiting' || (mission.state === 'active' && mission.questions.length > 0);\n}\n\nexport function shownState(mission: Mission): MissionState {\n return waitsOnYou(mission) ? 'waiting' : mission.state;\n}\n\nexport function missionOrder(missions: readonly Mission[]): Mission[] {\n return [...missions].sort((a, b) => ORDER[shownState(a)] - ORDER[shownState(b)] || 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(waitsOnYou).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 receiverKeys(sessionId: string, claudeSessionId: string): string[] {\n return [sessionId, `external:${claudeSessionId}`];\n}\n\nexport function waitingNotice(listed: readonly Mission[], mine: readonly string[], seen: readonly string[]): { fresh: Mission[]; seen: string[] } {\n const others = listed.filter((mission) => !mission.receivers.some((key) => mine.includes(key)));\n const keyed = [\n ...others.filter((mission) => mission.state === 'waiting').map((mission) => ({ key: `${mission.node}/${mission.missionId}`, mission })),\n ...others.flatMap((mission) => (mission.state === 'active' || mission.state === 'waiting' ? mission.questions.map((asked) => ({ key: `${mission.node}/${mission.missionId}/${asked.id}`, mission })) : [])),\n ];\n const fresh = keyed.filter((one) => !seen.includes(one.key)).map((one) => one.mission);\n return { fresh: fresh.filter((mission, index) => fresh.indexOf(mission) === index), seen: keyed.map((one) => one.key) };\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 ?? mission.questions.at(-1)?.question ?? 'its lead stopped without a hand-off'}`.slice(0, 200);\n case 'active':\n return `orbital: ${mission.missionId} asks you: ${mission.questions.at(-1)?.question ?? ''}`.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.questions.length > 0 ? ['', 'open questions of the lead:', ...mission.questions.map((asked) => ` ${asked.id}: ${asked.question}`), `answer one with missionReply on ${mission.missionId} with its question id, or run: orbital mission reply ${mission.missionId} \"<answer>\" --question <id>`] : []),\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; question: string | null };\n\nexport function stillAsked(queued: readonly Queued[], missions: readonly Mission[] | null): Queued[] {\n if (!missions) return [...queued];\n const open = (one: Queued) => missions.some((mission) => mission.missionId === one.mission && (one.question === null ? mission.state === 'waiting' : mission.questions.some((asked) => asked.id === one.question)));\n const kept = queued.filter((one) => one.mission === null || open(one));\n return kept.filter((one, index) => one.mission === null || !kept.slice(index + 1).some((later) => later.mission === one.mission && later.question === one.question));\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, viewArgv, answeredOf, answeredText, attachArgv, attachedOf, backgroundLines, bandText, binCandidates, changedMissions, checkAnswerOf, decisionOf, deliveryOf, detachedOf, elapsed, framed, isMissionTarget, joinTarget, stillAsked, linesOf, missionDetail, missionOrder, missionsOf, shownState, fitLine, frameOf, opensTranscript, peersOf, recordsOf, sessionHeader, stateMark, toastOf, transcriptOf, versionOf, receiverKeys, waitingNotice, withoutAnswered } from './logic.ts'\nimport type { Answered, 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-3' })\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-3' })\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' })\nconst link = atom({ plugin: 'orbital', key: 'link' } as const, 0, { shape: 'link-1' })\n\nconst TRANSCRIPT_LIMIT = 30\nconst RELINK_MS = 2000\nconst NO_NODE = 'No orbital node runs for this project. orbital joins it by itself when it runs. Start the nodes with: orbital up, or join a mission of another node with: /orbital join <mission>'\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, question: delivery.question }])\n return\n }\n if (await read($, isTurnRunning)) {\n if (delivery.mission) {\n await update($, queued, (list) => [...list, { text: framed(delivery), mission: delivery.mission, question: delivery.question }])\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 answered($: EngineInterface, answer: Answered): Promise<void> {\n const before = await read($, queued)\n const kept = withoutAnswered(before, answer)\n await update($, queued, () => kept)\n await note($, `${answer.mission} answered by ${answer.by}`)\n $.ui.toast(`orbital: ${answer.mission} is answered by ${answer.by}`)\n if (kept.length === before.length) await $.session.append({ message: { type: 'user', content: [{ type: 'text', text: answeredText(answer) }] } })\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, receiverKeys(joined.sessionId, await $.session.id()), 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\ntype Link = { id: number; first: Promise<OrbitalAttached | null>; ended: Promise<void> }\n\nasync function attach($: EngineInterface, argv: string[]): Promise<OrbitalAttached | null> {\n return (await linked($, argv)).first\n}\n\nasync function linked($: EngineInterface, argv: string[]): Promise<Link> {\n const id = (await read($, link)) + 1\n await update($, link, () => id)\n let ready: (joined: OrbitalAttached | null) => void = () => undefined\n const first = new Promise<OrbitalAttached | null>((done) => { ready = done })\n const ended = (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 answer = answeredOf(record)\n if (answer) {\n await answered($, answer)\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 ready(null)\n })()\n return { id, first, ended }\n}\n\nasync function stayJoined($: EngineInterface, argv: string[]): Promise<void> {\n for (;;) {\n const one = await linked($, argv)\n await one.ended.catch(() => undefined)\n if ((await read($, link)) !== one.id) return\n if (!(await read($, attached)) && (await read($, problem)) !== DETACHED_PROBLEM) await update($, problem, () => NO_NODE)\n await $.clock.sleep(RELINK_MS)\n if ((await read($, link)) !== one.id) return\n }\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[], node: string): Promise<void> {\n let carry = ''\n for await (const piece of $.process.spawn({ argv: viewArgv(bin, { cwd: await $.session.cwd(), node, width: VIEW_WIDTH }) })) {\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(shownState(mission), 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.questions.map((asked) => ` · asks ${asked.id}: ${asked.question}`).join('')}${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, config.node ?? '')\n await stayJoined($, attachArgv(bin, { cwd: await $.session.cwd(), sessionId: await $.session.id(), node: config.node ?? '', team: config.team ?? '' }))\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 a question of a mission: the question of a waiting mission, or an open question its lead asked with `missionAsk`, by its question id. The answer reaches the lead and closes that question for every decision receiver.\n- `missionJoin` makes this session a decision receiver of a running mission: each question of its lead comes to this session, and the questions that are open now come at once. Any session can join, also this Claude Code session. A mission can have many receivers: each question goes to every live one, and the first answer closes it for all.\n This session stays a receiver after it or the node restarts, so join once. In a terminal of this session, `orbital mission join <mission>` does the same.\n- `missionLeave` removes this session from the decision receivers of a mission.\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- `missionAsk` asks the decision receivers of the mission of your session a question, as its lead, at any moment, also while your workers run. The node keeps each question open until a receiver answers it, shows it as waiting on the person, and pushes it at once to every live receiver.\n- `missionLink` adds a link to the mission of your session, as its lead: a label, a URL and a state, such as `PR #42`, its URL and `open`. A call with the same URL replaces the link.\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 one of its decision receivers, so each question of the lead comes to the session. Show the person the question, and send their answer with `missionSay`. A question that the lead asked with `missionAsk` names its question id: send the answer with `missionReply` and that question id, so that the answer closes the right question.\nWhen the person asks you to watch a mission, or to get its decisions, call `missionJoin` on it.\nA message that starts with `[orbital]` and says a question is answered means another receiver answered it: do not answer it again, and tell the person.\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- As the lead of a mission, report each pull request you open with `missionLink`: its label, its URL and the state `open`.\n When its state changes, such as to merged or closed, call `missionLink` again with the same URL and the new state.\n The person sees the links of each mission in the live view.\n- As the lead of a mission, ask each decision that you need with `missionAsk`, at once, also while your workers run.\n Never put a question only in your reply text: text that ends no turn reaches no one, and the mission does not show it.\n Ask each question once, with the options you see and what each one means. Several questions stay open side by side, and each answer comes back to you as a message that names its question.\n Go on with the work that does not depend on the answer.\n- When nothing is left to do but wait for an answer, end your turn with the question: 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; receivers: string[]; question: string | null; questions: Array<{ id: string; question: string; at: string }>; 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; question: 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 link: Shaped<number>;\n said: Shaped<string[]>;\n isCommandRunning: Shaped<boolean>;\n };\n }\n}\n"}}