{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../../../src/inspectors/herdr/actions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,KAAK,qBAAqB,EAA4B,MAAM,2BAA2B,CAAC;AAGjG,OAAO,EAAE,KAAK,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE/E,OAAO,EAAkC,KAAK,WAAW,EAAyC,MAAM,aAAa,CAAC;AAEtH,eAAO,MAAM,uBAAuB,oEAAqE,CAAC;AAC1G,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5E,MAAM,WAAW,qBAAqB;IACrC,aAAa,EAAE,CAAC,CAAC;IACjB,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,eAAe;IACxB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,aAAa;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAmCD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAM7G;AA0HD,wBAAsB,0BAA0B,CAC/C,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,aAAa,GACjB,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CA0FnC","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { AgentToolResult } from \"@lpb-work/pi-agent-core\";\nimport { readMissionBinding } from \"../../missions/lifecycle.ts\";\nimport { listMissions, missionRecordPath, resolveMissionStoreLocation } from \"../../missions/store.ts\";\nimport type { MissionStoreConfig } from \"../../missions/types.ts\";\nimport { type AuthorityPolicyConfig, resolveAuthorityDecision } from \"../../policy/authority.ts\";\nimport { resolveSubagentRunId } from \"../../runs/background/run-id-resolver.ts\";\nimport { writeAtomicJson } from \"../../shared/atomic-json.ts\";\nimport { type Details, DIRS, type SubagentState } from \"../../shared/types.ts\";\nimport { readStatus } from \"../../shared/utils.ts\";\nimport { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from \"./client.ts\";\n\nexport const HERDR_INSPECTOR_ACTIONS = [\"inspector.open\", \"inspector.status\", \"inspector.close\"] as const;\nexport type HerdrInspectorAction = (typeof HERDR_INSPECTOR_ACTIONS)[number];\n\nexport interface HerdrInspectorBinding {\n\tschemaVersion: 1;\n\tkind: \"herdr-inspector\";\n\trunId: string;\n\tasyncDir: string;\n\tchildIndex?: number;\n\tmissionId?: string;\n\tmissionPath?: string;\n\tpaneId: string;\n\topenedAt: string;\n\tlastFocusedAt?: string;\n\therdrVersion?: string;\n\tcommand: string;\n}\n\ninterface InspectorParams {\n\tid?: string;\n\trunId?: string;\n\tdir?: string;\n\tindex?: number;\n\tfocus?: boolean;\n}\n\ninterface InspectorDeps {\n\tstate?: SubagentState;\n\tasyncDirRoot?: string;\n\tresultsDir?: string;\n\tclient?: HerdrClient;\n\tmissions?: MissionStoreConfig;\n\tauthorityPolicy?: AuthorityPolicyConfig;\n\tcwd: string;\n\tsignal?: AbortSignal;\n\tnow?: () => Date;\n\trunnerPath?: string;\n}\n\nfunction result(text: string, isError = false): AgentToolResult<Details> {\n\treturn {\n\t\tcontent: [{ type: \"text\", text }],\n\t\t...(isError ? { isError: true } : {}),\n\t\tdetails: { mode: \"management\", results: [] },\n\t};\n}\n\nfunction formatHerdrError(input: { code: HerdrErrorCode; message: string }): string {\n\treturn `Herdr inspector error (${input.code}): ${input.message}`;\n}\n\nfunction bindingPath(asyncDir: string, index?: number): string {\n\treturn path.join(asyncDir, \"inspectors\", `herdr${index === undefined ? \"\" : `-${index}`}.json`);\n}\n\nfunction parseBinding(value: unknown): HerdrInspectorBinding | undefined {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n\tconst input = value as Partial<HerdrInspectorBinding>;\n\tif (input.schemaVersion !== 1 || input.kind !== \"herdr-inspector\") return undefined;\n\tif (\n\t\ttypeof input.runId !== \"string\" ||\n\t\ttypeof input.asyncDir !== \"string\" ||\n\t\ttypeof input.paneId !== \"string\" ||\n\t\ttypeof input.openedAt !== \"string\" ||\n\t\ttypeof input.command !== \"string\"\n\t)\n\t\treturn undefined;\n\tif (input.childIndex !== undefined && (!Number.isInteger(input.childIndex) || input.childIndex < 0))\n\t\treturn undefined;\n\treturn input as HerdrInspectorBinding;\n}\n\nexport function readHerdrInspectorBinding(asyncDir: string, index?: number): HerdrInspectorBinding | undefined {\n\ttry {\n\t\treturn parseBinding(JSON.parse(fs.readFileSync(bindingPath(asyncDir, index), \"utf-8\")));\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction extractPaneId(value: unknown): string | undefined {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tconst pane =\n\t\trecord.pane && typeof record.pane === \"object\" && !Array.isArray(record.pane)\n\t\t\t? (record.pane as Record<string, unknown>)\n\t\t\t: record;\n\tfor (const key of [\"pane_id\", \"paneId\", \"id\"]) if (typeof pane[key] === \"string\") return pane[key];\n\treturn undefined;\n}\n\nfunction shellQuote(value: string): string {\n\tif (process.platform === \"win32\") return `\"${value.replaceAll('\"', '\\\\\"')}\"`;\n\treturn `'${value.replaceAll(\"'\", \"'\\\\''\")}'`;\n}\n\nfunction inspectorCommand(input: {\n\trunnerPath: string;\n\tasyncDir: string;\n\trunId: string;\n\tindex?: number;\n\tmissionPath?: string;\n\tallowSteer: boolean;\n\tallowStop: boolean;\n}): string {\n\tconst args = [\n\t\tprocess.execPath,\n\t\tinput.runnerPath,\n\t\t\"--async-dir\",\n\t\tinput.asyncDir,\n\t\t\"--run-id\",\n\t\tinput.runId,\n\t\t\"--allow-steer\",\n\t\tString(input.allowSteer),\n\t\t\"--allow-stop\",\n\t\tString(input.allowStop),\n\t];\n\tif (input.index !== undefined) args.push(\"--index\", String(input.index));\n\tif (input.missionPath) args.push(\"--mission-path\", input.missionPath);\n\treturn args.map(shellQuote).join(\" \");\n}\n\nfunction missionForRun(\n\tasyncDir: string,\n\tcwd: string,\n\tconfig: MissionStoreConfig | undefined,\n\trunId: string,\n): { id: string; path: string } | undefined {\n\ttry {\n\t\tconst binding = readMissionBinding(asyncDir);\n\t\tif (binding) return { id: binding.missionId, path: missionRecordPath(binding.location, binding.missionId) };\n\t\tconst location = resolveMissionStoreLocation({ projectRoot: cwd, ...(config ? { config } : {}) });\n\t\tconst mission = listMissions(location).records.find((record) => record.runs.some((run) => run.runId === runId));\n\t\treturn mission ? { id: mission.id, path: missionRecordPath(location, mission.id) } : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction pathWithin(base: string, candidate: string): boolean {\n\tconst resolvedBase = path.resolve(base);\n\tconst resolvedCandidate = path.resolve(candidate);\n\treturn resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);\n}\n\nfunction isTrustedAsyncDir(asyncDir: string, deps: InspectorDeps): boolean {\n\ttry {\n\t\tif (fs.lstatSync(asyncDir).isSymbolicLink() || !fs.statSync(asyncDir).isDirectory()) return false;\n\t\tconst realDir = fs.realpathSync(asyncDir);\n\t\tconst registered = [...(deps.state?.asyncJobs.values() ?? [])].some((job) => {\n\t\t\ttry {\n\t\t\t\treturn fs.realpathSync(job.asyncDir) === realDir;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t});\n\t\tif (registered) return true;\n\t\tconst root = deps.asyncDirRoot ?? DIRS.async;\n\t\tif (!fs.existsSync(root) || !pathWithin(root, asyncDir)) return false;\n\t\treturn pathWithin(fs.realpathSync(root), realDir);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction resolveAsyncTarget(\n\tparams: InspectorParams,\n\tdeps: InspectorDeps,\n): { runId: string; asyncDir: string } | { error: string } {\n\tconst requestedId = params.id ?? params.runId;\n\tif (params.dir) {\n\t\tconst asyncDir = path.resolve(params.dir);\n\t\tif (!isTrustedAsyncDir(asyncDir, deps))\n\t\t\treturn { error: `Async run directory '${asyncDir}' is outside trusted run roots.` };\n\t\tconst status = readStatus(asyncDir);\n\t\tif (!status) return { error: `No async run status found in '${asyncDir}'.` };\n\t\tif (requestedId && requestedId !== status.runId && !status.runId.startsWith(requestedId))\n\t\t\treturn { error: `Run '${requestedId}' does not match status run '${status.runId}'.` };\n\t\treturn { runId: status.runId, asyncDir };\n\t}\n\tif (!requestedId) return { error: \"Herdr inspector actions require id or dir.\" };\n\ttry {\n\t\tconst resolved = resolveSubagentRunId(requestedId, {\n\t\t\tstate: deps.state,\n\t\t\tasyncDirRoot: deps.asyncDirRoot ?? DIRS.async,\n\t\t\tresultsDir: deps.resultsDir ?? DIRS.results,\n\t\t});\n\t\tif (!resolved) return { error: `No subagent run found for '${requestedId}'.` };\n\t\tif (resolved.kind !== \"async\" || !resolved.location.asyncDir)\n\t\t\treturn { error: `Run '${resolved.id}' is not an inspectable async run with lifecycle artifacts.` };\n\t\treturn { runId: resolved.id, asyncDir: resolved.location.asyncDir };\n\t} catch (cause) {\n\t\treturn { error: cause instanceof Error ? cause.message : String(cause) };\n\t}\n}\n\nasync function paneExists(client: HerdrClient, paneId: string, signal?: AbortSignal): Promise<HerdrResult<unknown>> {\n\treturn client.run([\"pane\", \"get\", paneId], { timeoutMs: 5_000, signal });\n}\n\nexport async function handleHerdrInspectorAction(\n\taction: HerdrInspectorAction,\n\tparams: InspectorParams,\n\tdeps: InspectorDeps,\n): Promise<AgentToolResult<Details>> {\n\tconst target = resolveAsyncTarget(params, deps);\n\tif (\"error\" in target) return result(target.error, true);\n\tconst status = readStatus(target.asyncDir);\n\tif (!status) return result(`No lifecycle status exists for async run '${target.runId}'.`, true);\n\tif (params.index !== undefined && (params.index < 0 || params.index >= (status.steps?.length ?? 0))) {\n\t\treturn result(\n\t\t\t`Async run '${target.runId}' has ${status.steps?.length ?? 0} children. Index ${params.index} is out of range.`,\n\t\t\ttrue,\n\t\t);\n\t}\n\tconst existing = readHerdrInspectorBinding(target.asyncDir, params.index);\n\tconst client = deps.client ?? createHerdrClient();\n\n\tif (action === \"inspector.status\") {\n\t\tif (!existing)\n\t\t\treturn result(\n\t\t\t\t`No Herdr inspector binding exists for async run ${target.runId}${params.index === undefined ? \"\" : ` child ${params.index}`}.`,\n\t\t\t);\n\t\tconst live = await paneExists(client, existing.paneId, deps.signal);\n\t\tif (live.ok === false)\n\t\t\treturn result(\n\t\t\t\t`${formatHerdrError(live.error)}\\nBinding: ${bindingPath(target.asyncDir, params.index)}\\nRun state remains authoritative: ${status.state}.`,\n\t\t\t\ttrue,\n\t\t\t);\n\t\treturn result(\n\t\t\t`Herdr inspector ${existing.paneId} is open for async run ${target.runId}.\\nRun state: ${status.state}\\nBinding: ${bindingPath(target.asyncDir, params.index)}`,\n\t\t);\n\t}\n\n\tif (action === \"inspector.close\") {\n\t\tif (!existing) return result(`No Herdr inspector binding exists for async run ${target.runId}.`);\n\t\tconst closed = await client.run([\"pane\", \"close\", existing.paneId], { timeoutMs: 10_000, signal: deps.signal });\n\t\tif (closed.ok === false && closed.error.code !== \"NOT_FOUND\" && closed.error.code !== \"PANE_GONE\")\n\t\t\treturn result(formatHerdrError(closed.error), true);\n\t\tfs.rmSync(bindingPath(target.asyncDir, params.index), { force: true });\n\t\treturn result(\n\t\t\t`Closed Herdr inspector pane ${existing.paneId} for async run ${target.runId}. The subagent run was not stopped.`,\n\t\t);\n\t}\n\n\tconst detected = await detectHerdr(client, deps.signal);\n\tif (detected.ok === false) return result(formatHerdrError(detected.error), true);\n\tif (existing) {\n\t\tconst live = await paneExists(client, existing.paneId, deps.signal);\n\t\tif (live.ok)\n\t\t\treturn result(\n\t\t\t\t`Herdr inspector pane ${existing.paneId} is already open for async run ${target.runId}.${params.focus ? \" Herdr cannot refocus an arbitrary raw pane id; select it in the Herdr UI.\" : \"\"}`,\n\t\t\t);\n\t}\n\tconst splitArgs = [\"pane\", \"split\", \"--current\", \"--direction\", \"right\", \"--cwd\", status.cwd ?? deps.cwd];\n\tif (params.focus !== false) splitArgs.push(\"--focus\");\n\tconst split = await client.run(splitArgs, { timeoutMs: 15_000, signal: deps.signal });\n\tif (split.ok === false) return result(formatHerdrError(split.error), true);\n\tconst paneId = extractPaneId(split.data);\n\tif (!paneId) return result(\"Herdr inspector error (PANE_GONE): pane split returned no pane id.\", true);\n\tconst mission = missionForRun(target.asyncDir, deps.cwd, deps.missions, target.runId);\n\tconst runnerPath = deps.runnerPath ?? fileURLToPath(new URL(\"../../../inspector-runner.mjs\", import.meta.url));\n\tconst command = inspectorCommand({\n\t\trunnerPath,\n\t\tasyncDir: target.asyncDir,\n\t\trunId: target.runId,\n\t\tindex: params.index,\n\t\tmissionPath: mission?.path,\n\t\tallowSteer: resolveAuthorityDecision({ action: \"steerRun\", policy: deps.authorityPolicy }) === \"auto\",\n\t\tallowStop: resolveAuthorityDecision({ action: \"stopRun\", policy: deps.authorityPolicy }) === \"auto\",\n\t});\n\tconst started = await client.run([\"pane\", \"run\", paneId, command], { timeoutMs: 15_000, signal: deps.signal });\n\tif (started.ok === false) {\n\t\tawait client.run([\"pane\", \"close\", paneId], { timeoutMs: 5_000 });\n\t\treturn result(formatHerdrError(started.error), true);\n\t}\n\tconst now = (deps.now?.() ?? new Date()).toISOString();\n\tconst binding: HerdrInspectorBinding = {\n\t\tschemaVersion: 1,\n\t\tkind: \"herdr-inspector\",\n\t\trunId: target.runId,\n\t\tasyncDir: target.asyncDir,\n\t\t...(params.index !== undefined ? { childIndex: params.index } : {}),\n\t\t...(mission ? { missionId: mission.id, missionPath: mission.path } : {}),\n\t\tpaneId,\n\t\topenedAt: now,\n\t\t...(params.focus !== false ? { lastFocusedAt: now } : {}),\n\t\therdrVersion: detected.data.versionText,\n\t\tcommand,\n\t};\n\twriteAtomicJson(bindingPath(target.asyncDir, params.index), binding);\n\treturn result(\n\t\t`Opened read-only Herdr inspector pane ${paneId} for async run ${target.runId}. Closing the pane does not stop the run.\\nControls inside the pane: steer <message>, stop, status.`,\n\t);\n}\n"]}