{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../../src/core/mission-control/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAyLH,wBAAgB,wBAAwB,IAAI,MAAM,CAajD;AAED,wBAAsB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAyIlF","sourcesContent":["/**\n * Mission Control Plane — CLI (2.9.0).\n *\n * `jensen mission list|show|tree|attempts|ownership|result|evidence|resume|cancel|recover`\n *\n * This is the durable-mission operator surface. It routes through\n * MissionControlService rather than touching stores/session files directly, and\n * supports `--json` for stable machine-readable DTO output. Human output is a\n * compact key: value block, never a raw JSON dump by default.\n */\n\nimport chalk from \"chalk\";\nimport { getAgentDir } from \"../../config.js\";\nimport { defaultChildSessionDir } from \"../durable-child-session/index.js\";\nimport { isMissionState, type MissionState } from \"../mission-domain/mission-state.js\";\nimport { createFileDurableMissionStore } from \"../mission-durable/index.js\";\nimport { MissionControlService } from \"./mission-control-service.js\";\nimport type {\n\tMissionCancellationView,\n\tMissionControlResumeOutcome,\n\tMissionDetail,\n\tMissionListResult,\n\tMissionOwnershipView,\n\tMissionResultView,\n\tMissionTreeNode,\n} from \"./mission-control-types.js\";\n\nconst CONTROL_SUBCOMMANDS = new Set([\n\t\"list\",\n\t\"show\",\n\t\"tree\",\n\t\"attempts\",\n\t\"ownership\",\n\t\"result\",\n\t\"evidence\",\n\t\"resume\",\n\t\"cancel\",\n\t\"recover\",\n]);\n\nfunction buildService(): MissionControlService {\n\tconst store = createFileDurableMissionStore();\n\treturn new MissionControlService({ store, sessionDir: defaultChildSessionDir(getAgentDir()) });\n}\n\nfunction valueArgs(args: string[]): string[] {\n\treturn args.filter((a) => !a.startsWith(\"--\"));\n}\n\nfunction flag(args: string[], name: string): boolean {\n\treturn args.includes(name);\n}\n\nfunction flagValue(args: string[], name: string): string | undefined {\n\tconst idx = args.indexOf(name);\n\tif (idx === -1) return undefined;\n\treturn args[idx + 1];\n}\n\nfunction printJson(payload: unknown): void {\n\tprocess.stdout.write(`${JSON.stringify(payload, null, 2)}\\n`);\n}\n\nfunction codeOf(error: unknown): string | undefined {\n\tif (typeof error === \"object\" && error !== null && \"code\" in error) {\n\t\treturn String((error as { code: unknown }).code);\n\t}\n\treturn undefined;\n}\n\nfunction renderError(error: unknown): void {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tconst code = codeOf(error);\n\tprocess.stderr.write(`${chalk.red(code ? `${code}: ${message}` : message)}\\n`);\n}\n\n// =============================================================================\n// Human renderers\n// =============================================================================\n\nfunction renderList(result: MissionListResult): void {\n\tif (result.entries.length === 0 && result.corrupt.length === 0) {\n\t\tprocess.stdout.write(\"(no missions)\\n\");\n\t\treturn;\n\t}\n\tfor (const entry of result.entries) {\n\t\tprocess.stdout.write(\n\t\t\t[\n\t\t\t\t`${entry.missionId}  [${entry.state}]`,\n\t\t\t\tentry.parentMissionId ? ` parent=${entry.parentMissionId}` : \"\",\n\t\t\t\tentry.childSessionId ? ` session=${entry.childSessionId}` : \"\",\n\t\t\t\t` attempts=${entry.attemptCount}`,\n\t\t\t\t` fence=${entry.fencingToken}`,\n\t\t\t\tentry.owned ? ` owned (${entry.leaseStatus})` : \"\",\n\t\t\t\tentry.resumable ? \" resumable\" : \"\",\n\t\t\t].join(\"\"),\n\t\t);\n\t\tprocess.stdout.write(\"\\n\");\n\t}\n\tfor (const corrupt of result.corrupt) {\n\t\tprocess.stdout.write(chalk.yellow(`${corrupt.missionId}  [CORRUPT] ${corrupt.diagnostic}\\n`));\n\t}\n}\n\nfunction renderDetail(detail: MissionDetail): void {\n\tconst s = detail.summary;\n\tprocess.stdout.write(`MISSION\\n`);\n\tprocess.stdout.write(`  id: ${s.missionId}\\n`);\n\tprocess.stdout.write(`  state: ${s.state}\\n`);\n\tif (s.parentMissionId) process.stdout.write(`  parent: ${s.parentMissionId}\\n`);\n\tif (s.childSessionId) process.stdout.write(`  session: ${s.childSessionId}\\n`);\n\tprocess.stdout.write(`  depth: ${s.depth}\\n`);\n\tprocess.stdout.write(`  attempts: ${s.attemptCount}\\n`);\n\tprocess.stdout.write(`  fence: ${s.fencingToken}\\n`);\n\tprocess.stdout.write(`  terminal: ${s.terminal ? \"yes\" : \"no\"}\\n`);\n\tprocess.stdout.write(`  resumable: ${s.resumable ? \"yes\" : \"no\"}\\n`);\n\tif (s.verificationStatus) process.stdout.write(`  verification: ${s.verificationStatus}\\n`);\n\tprocess.stdout.write(`OWNERSHIP\\n`);\n\tprocess.stdout.write(`  owned: ${detail.ownership.owned ? \"yes\" : \"no\"}\\n`);\n\tprocess.stdout.write(`  fence: ${detail.ownership.fencingToken}\\n`);\n\tif (detail.ownership.ownerId) process.stdout.write(`  owner: ${detail.ownership.ownerId}\\n`);\n\tif (detail.ownership.expiresAtMs) process.stdout.write(`  expires: ${detail.ownership.expiresAtMs}\\n`);\n\tprocess.stdout.write(`  lease: ${detail.ownership.leaseStatus}\\n`);\n\tif (detail.result.available) process.stdout.write(`RESULT\\n  state: ${detail.result.result?.state}\\n`);\n\tif (detail.children.length > 0) process.stdout.write(`CHILDREN\\n  ${detail.children.join(\", \")}\\n`);\n}\n\nfunction renderTree(node: MissionTreeNode, prefix = \"\"): void {\n\tprocess.stdout.write(\n\t\t`${prefix}${node.missionId}  [${node.state}]  fence=${node.ownership.fencingToken}${node.ownership.owned ? \" (owned)\" : \"\"}\\n`,\n\t);\n\tfor (let i = 0; i < node.children.length; i++) {\n\t\tconst last = i === node.children.length - 1;\n\t\trenderTree(node.children[i], `${prefix}${last ? \"  └── \" : \"  ├── \"}`);\n\t}\n}\n\nfunction renderOwnership(view: MissionOwnershipView): void {\n\tprocess.stdout.write(`OWNERSHIP\\n`);\n\tprocess.stdout.write(`  owned: ${view.owned ? \"yes\" : \"no\"}\\n`);\n\tprocess.stdout.write(`  lease: ${view.leaseStatus}\\n`);\n\tprocess.stdout.write(`  fence: ${view.fencingToken}\\n`);\n\tif (view.ownerId) process.stdout.write(`  owner: ${view.ownerId}\\n`);\n\tif (view.leaseId) process.stdout.write(`  leaseId: ${view.leaseId}\\n`);\n\tif (view.expiresAtMs) process.stdout.write(`  expires: ${view.expiresAtMs}\\n`);\n\tif (view.remainingMs !== undefined) process.stdout.write(`  remaining: ${view.remainingMs}ms\\n`);\n\tif (view.localRuntime?.heartbeatTelemetry) {\n\t\tprocess.stdout.write(`  local heartbeat: renewals=${view.localRuntime.heartbeatTelemetry.renewalCount}\\n`);\n\t}\n}\n\nfunction renderResult(view: MissionResultView): void {\n\tif (!view.available || !view.result) {\n\t\tprocess.stdout.write(\"no terminal result\\n\");\n\t\treturn;\n\t}\n\tprocess.stdout.write(`RESULT\\n`);\n\tprocess.stdout.write(`  state: ${view.result.state}\\n`);\n\tprocess.stdout.write(`  execution: ${view.result.executionOutcome}\\n`);\n\tprocess.stdout.write(`  verification: ${view.result.verification.status}\\n`);\n\tif (view.resultExecutionId) process.stdout.write(`  executionId: ${view.resultExecutionId}\\n`);\n}\n\nfunction renderResume(outcome: MissionControlResumeOutcome): void {\n\tprocess.stdout.write(\n\t\t[\n\t\t\t`resumed ${outcome.missionId}`,\n\t\t\t`state=${outcome.missionState}`,\n\t\t\t`attempt=${outcome.attemptId}`,\n\t\t\t`fence=${outcome.fencingToken}`,\n\t\t\t`success=${outcome.success}`,\n\t\t].join(\" \"),\n\t);\n\tprocess.stdout.write(\"\\n\");\n}\n\nfunction renderCancel(view: MissionCancellationView): void {\n\tprocess.stdout.write(\n\t\t[\n\t\t\t`cancel ${view.missionId}`,\n\t\t\t`status=${view.status}`,\n\t\t\t`executorConfirmedStopped=${view.executorConfirmedStopped}`,\n\t\t\tview.reason ? `reason=${view.reason}` : \"\",\n\t\t]\n\t\t\t.filter(Boolean)\n\t\t\t.join(\" \"),\n\t);\n\tprocess.stdout.write(\"\\n\");\n}\n\n// =============================================================================\n// Handler\n// =============================================================================\n\nexport function printMissionControlUsage(): string {\n\treturn [\n\t\t\"  mission list [--json] [--state S] [--parent ID] [--terminal|--nonterminal] [--interrupted] [--owned|--unowned] [--resumable|--not-resumable] [--limit N]\",\n\t\t\"  mission show <MISSION_ID> [--json]\",\n\t\t\"  mission tree <MISSION_ID> [--json]\",\n\t\t\"  mission attempts <MISSION_ID> [--json]\",\n\t\t\"  mission ownership <MISSION_ID> [--json]\",\n\t\t\"  mission result <MISSION_ID> [--json]\",\n\t\t\"  mission evidence <MISSION_ID> [--json]\",\n\t\t\"  mission resume <MISSION_ID> [--json]\",\n\t\t\"  mission cancel <MISSION_ID> [--json]\",\n\t\t\"  mission recover [--json]\",\n\t].join(\"\\n\");\n}\n\nexport async function handleMissionControlCommand(args: string[]): Promise<boolean> {\n\tif (args[0] !== \"mission\") return false;\n\tconst sub = args[1];\n\tif (!sub || !CONTROL_SUBCOMMANDS.has(sub)) return false;\n\n\tconst json = flag(args, \"--json\");\n\tconst service = buildService();\n\n\ttry {\n\t\tswitch (sub) {\n\t\t\tcase \"list\": {\n\t\t\t\tconst limitArg = flagValue(args, \"--limit\");\n\t\t\t\tconst depthArg = flagValue(args, \"--depth\");\n\t\t\t\tconst stateArg = flagValue(args, \"--state\");\n\t\t\t\tconst state: MissionState | undefined = stateArg && isMissionState(stateArg) ? stateArg : undefined;\n\t\t\t\tconst depth = depthArg !== undefined ? Number(depthArg) : undefined;\n\t\t\t\tconst result = await service.listMissions({\n\t\t\t\t\tfilter: {\n\t\t\t\t\t\tstate,\n\t\t\t\t\t\tparentMissionId: flagValue(args, \"--parent\"),\n\t\t\t\t\t\tterminal: flag(args, \"--terminal\") ? true : flag(args, \"--nonterminal\") ? false : undefined,\n\t\t\t\t\t\tinterrupted: flag(args, \"--interrupted\") ? true : undefined,\n\t\t\t\t\t\towned: flag(args, \"--owned\") ? true : flag(args, \"--unowned\") ? false : undefined,\n\t\t\t\t\t\tresumable: flag(args, \"--resumable\") ? true : flag(args, \"--not-resumable\") ? false : undefined,\n\t\t\t\t\t\tdepth: depth !== undefined && Number.isSafeInteger(depth) ? depth : undefined,\n\t\t\t\t\t},\n\t\t\t\t\tlimit: limitArg !== undefined ? Number(limitArg) : undefined,\n\t\t\t\t});\n\t\t\t\tif (json) printJson(result);\n\t\t\t\telse renderList(result);\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \"show\": {\n\t\t\t\tconst missionId = valueArgs(args)[2];\n\t\t\t\tif (!missionId) return missingMissionId();\n\t\t\t\tconst detail = await service.getMission(missionId);\n\t\t\t\tif (json) printJson(detail);\n\t\t\t\telse renderDetail(detail);\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \"tree\": {\n\t\t\t\tconst missionId = valueArgs(args)[2];\n\t\t\t\tif (!missionId) return missingMissionId();\n\t\t\t\tconst tree = await service.getMissionTree(missionId);\n\t\t\t\tif (json) printJson(tree);\n\t\t\t\telse renderTree(tree);\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \"attempts\": {\n\t\t\t\tconst missionId = valueArgs(args)[2];\n\t\t\t\tif (!missionId) return missingMissionId();\n\t\t\t\tconst attempts = await service.getAttempts(missionId);\n\t\t\t\tif (json) printJson(attempts);\n\t\t\t\telse {\n\t\t\t\t\tfor (const attempt of attempts.attempts) {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`${attempt.attemptId}${attempt.executionId ? ` exec=${attempt.executionId}` : \"\"} started=${attempt.startedAtMs}${attempt.endReason ? ` end=${attempt.endReason}` : \"\"}\\n`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (attempts.currentAttemptId) process.stdout.write(`current=${attempts.currentAttemptId}\\n`);\n\t\t\t\t\tif (attempts.resultExecutionId) process.stdout.write(`resultExecution=${attempts.resultExecutionId}\\n`);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \"ownership\": {\n\t\t\t\tconst missionId = valueArgs(args)[2];\n\t\t\t\tif (!missionId) return missingMissionId();\n\t\t\t\tconst ownership = await service.getOwnership(missionId);\n\t\t\t\tif (json) printJson(ownership);\n\t\t\t\telse renderOwnership(ownership);\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \"result\": {\n\t\t\t\tconst missionId = valueArgs(args)[2];\n\t\t\t\tif (!missionId) return missingMissionId();\n\t\t\t\tconst result = await service.getResult(missionId);\n\t\t\t\tif (json) printJson(result);\n\t\t\t\telse renderResult(result);\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \"evidence\": {\n\t\t\t\tconst missionId = valueArgs(args)[2];\n\t\t\t\tif (!missionId) return missingMissionId();\n\t\t\t\tconst refs = await service.getEvidenceRefs(missionId);\n\t\t\t\tif (json) printJson(refs);\n\t\t\t\telse {\n\t\t\t\t\tif (refs.length === 0) process.stdout.write(\"(no evidence references)\\n\");\n\t\t\t\t\tfor (const ref of refs) {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`${ref.evidenceId}${ref.available === undefined ? \"\" : ref.available ? \" available\" : \" missing\"}\\n`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \"resume\": {\n\t\t\t\tconst missionId = valueArgs(args)[2];\n\t\t\t\tif (!missionId) return missingMissionId();\n\t\t\t\tconst outcome = await resumeViaControl(service, missionId);\n\t\t\t\tif (json) printJson(outcome);\n\t\t\t\telse renderResume(outcome);\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \"cancel\": {\n\t\t\t\tconst missionId = valueArgs(args)[2];\n\t\t\t\tif (!missionId) return missingMissionId();\n\t\t\t\tconst view = await service.cancelMission(missionId);\n\t\t\t\tif (json) printJson(view);\n\t\t\t\telse renderCancel(view);\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \"recover\": {\n\t\t\t\tconst report = await service.recoverMission();\n\t\t\t\tif (json) printJson(report);\n\t\t\t\telse {\n\t\t\t\t\tfor (const action of report.actions) process.stdout.write(`${action}\\n`);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t} catch (error) {\n\t\trenderError(error);\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n}\n\nfunction missingMissionId(): boolean {\n\tprocess.stderr.write(\"missing mission id\\n\");\n\tprocess.exitCode = 1;\n\treturn true;\n}\n\n/** Build the concrete child CLI launch and resume through Mission Control. */\nasync function resumeViaControl(\n\tservice: MissionControlService,\n\tmissionId: string,\n): Promise<MissionControlResumeOutcome> {\n\tconst cliEntry = process.argv[1];\n\tconst command = process.execPath;\n\tconst prefixArgs = [...process.execArgv, cliEntry];\n\tconst sessionDir = defaultChildSessionDir(getAgentDir());\n\n\treturn service.resumeMission(missionId, {\n\t\tbuildResumeLaunch: ({ request, resumePrompt, childSessionId }) => {\n\t\t\tconst launchArgs = [\n\t\t\t\t...prefixArgs,\n\t\t\t\t\"--mode\",\n\t\t\t\t\"json\",\n\t\t\t\t\"-p\",\n\t\t\t\t\"--child-mission\",\n\t\t\t\tmissionId,\n\t\t\t\t\"--session-id\",\n\t\t\t\tchildSessionId,\n\t\t\t\t\"--session-dir\",\n\t\t\t\tsessionDir,\n\t\t\t];\n\t\t\tif (request.modelPolicy) {\n\t\t\t\tlaunchArgs.push(\"--provider\", request.modelPolicy.provider, \"--model\", request.modelPolicy.model);\n\t\t\t}\n\t\t\tif (request.capabilities && request.capabilities.length > 0) {\n\t\t\t\tlaunchArgs.push(\"--tools\", request.capabilities.join(\",\"));\n\t\t\t}\n\t\t\tlaunchArgs.push(resumePrompt);\n\t\t\treturn {\n\t\t\t\tcommand,\n\t\t\t\targs: launchArgs,\n\t\t\t\tcwd: request.workspaceScope?.cwd ?? process.cwd(),\n\t\t\t\tenv: { JENSEN_MISSION_ID: request.missionId },\n\t\t\t};\n\t\t},\n\t});\n}\n"]}