{"version":3,"file":"inspector-runner.d.ts","sourceRoot":"","sources":["../../../../src/inspectors/herdr/inspector-runner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAWD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC/C,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,MAAM,CA0BT;AAmCD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAmCnF;AAED,wBAAgB,YAAY,CAAC,IAAI,WAAwB,GAAG,IAAI,CAkC/D","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as readline from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseMissionRecord } from \"../../missions/store.ts\";\nimport type { MissionRecord } from \"../../missions/types.ts\";\nimport { requestAsyncSteer, requestAsyncStop } from \"../../runs/background/control-channel.ts\";\nimport { formatAsyncRunTranscript } from \"../../runs/background/fleet-view.ts\";\nimport type { AsyncStatus } from \"../../shared/types.ts\";\nimport { readStatus } from \"../../shared/utils.ts\";\n\nexport interface RunnerOptions {\n\tasyncDir: string;\n\trunId: string;\n\tindex?: number;\n\tmissionPath?: string;\n\trefreshMs: number;\n\tallowSteer?: boolean;\n\tallowStop?: boolean;\n}\n\nfunction readMission(filePath: string | undefined): MissionRecord | undefined {\n\tif (!filePath) return undefined;\n\ttry {\n\t\treturn parseMissionRecord(JSON.parse(fs.readFileSync(filePath, \"utf-8\")), filePath);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function formatInspectorDashboard(input: {\n\tstatus: AsyncStatus;\n\tasyncDir: string;\n\tindex?: number;\n\tmission?: MissionRecord;\n\tallowSteer?: boolean;\n\tallowStop?: boolean;\n}): string {\n\tconst { status, asyncDir, mission } = input;\n\tconst lines = [\n\t\t`pi-subagents inspector for ${status.runId}`,\n\t\t\"This pane mirrors lifecycle artifacts; closing it does not stop the run.\",\n\t\t\"\",\n\t];\n\tif (mission) {\n\t\tlines.push(`Mission: ${mission.title} (${mission.status})`, `Mission id: ${mission.id}`);\n\t\tconst open = mission.decisions.filter((decision) => decision.status === \"open\");\n\t\tif (open.length)\n\t\t\tlines.push(`Open decisions: ${open.map((decision) => `${decision.id}: ${decision.title}`).join(\" | \")}`);\n\t\tlines.push(\"\");\n\t}\n\tlines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60 }));\n\tconst controls = [\n\t\tinput.allowSteer === false ? undefined : \"steer <message>\",\n\t\tinput.allowStop === false ? undefined : \"stop\",\n\t\t\"status\",\n\t].filter(Boolean);\n\tlines.push(\n\t\t\"\",\n\t\t`Controls: ${controls.join(\" | \")}`,\n\t\t\"Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).\",\n\t);\n\treturn lines.join(\"\\n\");\n}\n\nfunction parseArgs(argv: string[]): RunnerOptions {\n\tconst values = new Map<string, string>();\n\tfor (let index = 0; index < argv.length; index += 2) {\n\t\tconst key = argv[index];\n\t\tconst value = argv[index + 1];\n\t\tif (!key?.startsWith(\"--\") || value === undefined) throw new Error(`Invalid inspector argument '${key ?? \"\"}'.`);\n\t\tvalues.set(key, value);\n\t}\n\tconst asyncDir = values.get(\"--async-dir\");\n\tconst runId = values.get(\"--run-id\");\n\tif (!asyncDir || !runId) throw new Error(\"Inspector requires --async-dir and --run-id.\");\n\tconst indexRaw = values.get(\"--index\");\n\tconst childIndex = indexRaw === undefined ? undefined : Number(indexRaw);\n\tif (childIndex !== undefined && (!Number.isInteger(childIndex) || childIndex < 0))\n\t\tthrow new Error(\"--index must be a non-negative integer.\");\n\tconst refreshRaw = values.get(\"--refresh-ms\");\n\tconst refreshMs = refreshRaw === undefined ? 1_500 : Number(refreshRaw);\n\tif (!Number.isInteger(refreshMs) || refreshMs < 250) throw new Error(\"--refresh-ms must be an integer >= 250.\");\n\treturn {\n\t\tasyncDir: path.resolve(asyncDir),\n\t\trunId,\n\t\t...(childIndex !== undefined ? { index: childIndex } : {}),\n\t\t...(values.get(\"--mission-path\") ? { missionPath: path.resolve(values.get(\"--mission-path\")!) } : {}),\n\t\trefreshMs,\n\t\tallowSteer: values.get(\"--allow-steer\") !== \"false\",\n\t\tallowStop: values.get(\"--allow-stop\") !== \"false\",\n\t};\n}\n\nfunction isTerminal(status: AsyncStatus): boolean {\n\treturn status.state !== \"queued\" && status.state !== \"running\";\n}\n\nexport function submitInspectorControl(options: RunnerOptions, line: string): string {\n\tconst command = line.trim();\n\tif (!command || command === \"status\") return \"Status refreshed.\";\n\tconst status = readStatus(options.asyncDir);\n\tif (!status || status.runId !== options.runId)\n\t\tthrow new Error(`Lifecycle status for run '${options.runId}' is unavailable.`);\n\tif (command === \"stop\") {\n\t\tif (options.allowStop === false) throw new Error(\"Authority policy does not allow stop from this inspector.\");\n\t\tif (isTerminal(status)) throw new Error(`Run '${options.runId}' is ${status.state} and cannot be stopped.`);\n\t\trequestAsyncStop(options.asyncDir, { source: \"herdr-inspector\" });\n\t\treturn `Stop requested for run ${options.runId}.`;\n\t}\n\tif (command.startsWith(\"steer \")) {\n\t\tif (options.allowSteer === false) throw new Error(\"Authority policy does not allow steer from this inspector.\");\n\t\tconst message = command.slice(\"steer \".length).trim();\n\t\tif (!message) throw new Error(\"steer requires a message.\");\n\t\tif (isTerminal(status)) throw new Error(`Run '${options.runId}' is ${status.state} and cannot be steered.`);\n\t\tconst runningIndexes = (status.steps ?? [])\n\t\t\t.map((step, index) => (step.status === \"running\" ? index : undefined))\n\t\t\t.filter((index): index is number => index !== undefined);\n\t\tconst targetIndex = options.index ?? (status.mode === \"single\" ? 0 : undefined);\n\t\tif (targetIndex === undefined && runningIndexes.length === 0)\n\t\t\tthrow new Error(\n\t\t\t\t\"No running child is available to steer. Open a child-specific inspector for a pending child.\",\n\t\t\t);\n\t\trequestAsyncSteer(options.asyncDir, {\n\t\t\tmessage,\n\t\t\t...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),\n\t\t\tsource: \"herdr-inspector\",\n\t\t});\n\t\treturn `Steering queued for run ${options.runId}.`;\n\t}\n\tif (command.startsWith(\"reply \"))\n\t\tthrow new Error(\"Supervisor replies are owned by the parent Pi session; use subagent_supervisor/intercom there.\");\n\tthrow new Error(\"Unknown control. Use steer <message>, stop, or status.\");\n}\n\nexport function runInspector(argv = process.argv.slice(2)): void {\n\tconst options = parseArgs(argv);\n\tlet notice = \"\";\n\tlet timer: ReturnType<typeof setInterval> | undefined;\n\tconst render = () => {\n\t\tconst status = readStatus(options.asyncDir);\n\t\tif (!status || status.runId !== options.runId) {\n\t\t\tprocess.stdout.write(\n\t\t\t\t`\\x1b[2J\\x1b[Hpi-subagents inspector\\n\\nLifecycle status for ${options.runId} is unavailable.\\n`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tprocess.stdout.write(\n\t\t\t`\\x1b[2J\\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop })}${notice ? `\\n\\n${notice}` : \"\"}\\n> `,\n\t\t);\n\t\tif (isTerminal(status) && timer) {\n\t\t\tclearInterval(timer);\n\t\t\ttimer = undefined;\n\t\t}\n\t};\n\tconst rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });\n\trl.on(\"line\", (line) => {\n\t\ttry {\n\t\t\tnotice = submitInspectorControl(options, line);\n\t\t} catch (cause) {\n\t\t\tnotice = `Control error: ${cause instanceof Error ? cause.message : String(cause)}`;\n\t\t}\n\t\trender();\n\t});\n\trender();\n\tif (!isTerminal(readStatus(options.asyncDir) ?? ({ state: \"failed\" } as AsyncStatus))) {\n\t\ttimer = setInterval(render, options.refreshMs);\n\t\ttimer.unref?.();\n\t}\n}\n\nif (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {\n\ttry {\n\t\trunInspector();\n\t} catch (cause) {\n\t\tprocess.stderr.write(`Herdr inspector failed: ${cause instanceof Error ? cause.message : String(cause)}\\n`);\n\t\tprocess.exitCode = 1;\n\t}\n}\n"]}