{"version":3,"file":"process-mission-executor.d.ts","sourceRoot":"","sources":["../../../src/core/mission-domain/process-mission-executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAIN,KAAK,eAAe,EAEpB,KAAK,aAAa,EAElB,MAAM,qBAAqB,CAAC;AAO7B,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,SAAS,CAAC,EAAE;QACX,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;QACnC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;KACnC,CAAC;CACF;AAED,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,qBAAqB,GAAG,CACnC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,CAAC,EAAE,WAAW,KAChB,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAEpC,MAAM,WAAW,2BAA2B;IAC3C,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,qBAAqB,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,2BAA2B,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC;AA6GjH,MAAM,WAAW,6BAA6B;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,WAAW,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,oBAAoB,CAAC;IAC/D,4DAA4D;IAC5D,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,MAAM,CAAC;CACzD;AAaD,qBAAa,sBAAuB,YAAW,eAAe;IAC7D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoD;IACjF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAyB;IACpD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAsC;IAC1E,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAE5D,YAAY,OAAO,EAAE,6BAA6B,EAMjD;IAEK,MAAM,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,aAAa,CAAC,CAsChG;IAEK,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAyC/D;IAEK,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIlE;YAEa,aAAa;YAMb,SAAS;CAqEvB","sourcesContent":["/**\n * Process Mission Executor (2.3.0).\n *\n * The transitional executor that adapts the current spawned-CLI path to the\n * first-class mission contract. This is the ONLY mission-domain file allowed to\n * depend on process-spawn machinery (and it imports none of the domain's\n * provider/CLI/UI concerns). The default harness uses Node `child_process`;\n * callers may inject a harness for deterministic tests.\n *\n * Invariants:\n *   - A raw exit code of 0 is NEVER classified as SUCCEEDED. Without a verifier\n *     it is PARTIAL (\"execution completed but unverified\").\n *   - The handle exposes `executionId` and `cancel`, never a ChildProcess.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport type { MissionExecutor, MissionLaunchOptions } from \"./mission-executor.js\";\nimport { createMissionHandle, type MissionHandle } from \"./mission-handle.js\";\nimport type { MissionRequest } from \"./mission-request.js\";\nimport { validateMissionRequest } from \"./mission-request.js\";\nimport {\n\tclassifyExecutorOutcome,\n\tcreateMissionResult,\n\ttype ExecutorDiagnostics,\n\ttype ExecutorOutcome,\n\ttype MissionExecutionOutcome,\n\ttype MissionResult,\n\ttype StructuredFailure,\n} from \"./mission-result.js\";\nimport { MissionStateTracker } from \"./mission-state.js\";\n\n// =============================================================================\n// Harness\n// =============================================================================\n\nexport interface ProcessMissionLaunch {\n\tcommand: string;\n\targs: readonly string[];\n\tcwd: string;\n\tenv?: Record<string, string | undefined>;\n\tcallbacks?: {\n\t\tonStdout?: (chunk: string) => void;\n\t\tonStderr?: (chunk: string) => void;\n\t};\n}\n\nexport interface ProcessMissionOutcome extends ExecutorOutcome {\n\tstdout: string;\n\tstderr: string;\n}\n\nexport type ProcessMissionHarness = (\n\tlaunch: ProcessMissionLaunch,\n\tsignal?: AbortSignal,\n) => Promise<ProcessMissionOutcome>;\n\nexport interface ProcessMissionVerifierInput {\n\trequest: MissionRequest;\n\toutcome: ProcessMissionOutcome;\n}\n\nexport interface ProcessMissionVerification {\n\tverified: boolean;\n\tsummary?: string;\n\tcriterionIds?: string[];\n}\n\nexport type ProcessMissionVerifier = (input: ProcessMissionVerifierInput) => Promise<ProcessMissionVerification>;\n\n// =============================================================================\n// Default harness (Node child_process)\n// =============================================================================\n\nconst DEFAULT_TIMEOUT_MS = 600_000;\n\nfunction createDefaultHarness(options: { timeoutMs?: number }): ProcessMissionHarness {\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\treturn (launch, signal) =>\n\t\tnew Promise<ProcessMissionOutcome>((resolve) => {\n\t\t\tconst child = spawn(launch.command, [...launch.args], {\n\t\t\t\tcwd: launch.cwd,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\tenv: { ...process.env, ...(launch.env ?? {}) },\n\t\t\t\t// Detach on POSIX so the child becomes its own process-group\n\t\t\t\t// leader; killing the group then terminates owned descendants\n\t\t\t\t// (matching BackgroundJobRegistry / MCP client conventions).\n\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet launchError: string | undefined;\n\t\t\tlet timedOut = false;\n\t\t\tlet settled = false;\n\t\t\tlet forceTimer: NodeJS.Timeout | undefined;\n\n\t\t\tconst killTree = (signal: NodeJS.Signals) => {\n\t\t\t\tif (child.pid && process.platform !== \"win32\") {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tprocess.kill(-child.pid, signal);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Fall through to the direct child.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tchild.kill(signal);\n\t\t\t\t} catch {\n\t\t\t\t\t// Already gone.\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst killChild = () => {\n\t\t\t\tkillTree(\"SIGTERM\");\n\t\t\t\tforceTimer = setTimeout(() => {\n\t\t\t\t\t// Only force-kill if the process is actually still running.\n\t\t\t\t\tif (child.exitCode === null && child.signalCode === null) {\n\t\t\t\t\t\tkillTree(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t};\n\n\t\t\tconst finish = (outcome: ProcessMissionOutcome) => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tif (forceTimer) clearTimeout(forceTimer);\n\t\t\t\tresolve(outcome);\n\t\t\t};\n\n\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\ttimedOut = true;\n\t\t\t\tkillChild();\n\t\t\t}, timeoutMs);\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\tkillChild();\n\t\t\t\t} else {\n\t\t\t\t\tsignal.addEventListener(\"abort\", killChild, { once: true });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchild.stdout.on(\"data\", (data: Buffer) => {\n\t\t\t\tconst chunk = data.toString();\n\t\t\t\tstdout += chunk;\n\t\t\t\tlaunch.callbacks?.onStdout?.(chunk);\n\t\t\t});\n\t\t\tchild.stderr.on(\"data\", (data: Buffer) => {\n\t\t\t\tconst chunk = data.toString();\n\t\t\t\tstderr += chunk;\n\t\t\t\tlaunch.callbacks?.onStderr?.(chunk);\n\t\t\t});\n\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\tlaunchError = error.message;\n\t\t\t});\n\t\t\tchild.on(\"close\", (code, closeSignal) => {\n\t\t\t\tif (signal) signal.removeEventListener(\"abort\", killChild);\n\t\t\t\tfinish({\n\t\t\t\t\texitCode: code,\n\t\t\t\t\tsignal: closeSignal ?? undefined,\n\t\t\t\t\ttimedOut,\n\t\t\t\t\tlaunchError,\n\t\t\t\t\tstdout,\n\t\t\t\t\tstderr,\n\t\t\t\t});\n\t\t\t});\n\t\t});\n}\n\n// =============================================================================\n// Executor\n// =============================================================================\n\nexport interface ProcessMissionExecutorOptions {\n\texecutorId?: string;\n\t/** Maps a MissionRequest to a concrete process launch (executor detail). */\n\tbuildLaunch: (request: MissionRequest) => ProcessMissionLaunch;\n\t/** Spawn harness override (default: Node child_process). */\n\tharness?: ProcessMissionHarness;\n\t/** Optional verifier that may promote an exit-0 outcome to SUCCEEDED. */\n\tverifier?: ProcessMissionVerifier;\n\ttimeoutMs?: number;\n\t/** Execution id factory (default: UUID). Never a process id. */\n\texecutionIdFactory?: (request: MissionRequest) => string;\n}\n\ninterface ActiveMission {\n\trequest: MissionRequest;\n\texecutionId: string;\n\ttracker: MissionStateTracker;\n\tstartedAtMs: number;\n\tfinishedAtMs?: number;\n\tcontroller: AbortController;\n\toutcomePromise: Promise<ProcessMissionOutcome>;\n\tcancelled: boolean;\n}\n\nexport class ProcessMissionExecutor implements MissionExecutor {\n\treadonly executorId: string;\n\tprivate readonly _buildLaunch: (request: MissionRequest) => ProcessMissionLaunch;\n\tprivate readonly _harness: ProcessMissionHarness;\n\tprivate readonly _verifier?: ProcessMissionVerifier;\n\tprivate readonly _executionIdFactory: (request: MissionRequest) => string;\n\tprivate readonly _active = new Map<string, ActiveMission>();\n\n\tconstructor(options: ProcessMissionExecutorOptions) {\n\t\tthis.executorId = options.executorId ?? \"process\";\n\t\tthis._buildLaunch = options.buildLaunch;\n\t\tthis._harness = options.harness ?? createDefaultHarness({ timeoutMs: options.timeoutMs });\n\t\tthis._verifier = options.verifier;\n\t\tthis._executionIdFactory = options.executionIdFactory ?? (() => `exec_${randomUUID()}`);\n\t}\n\n\tasync launch(request: MissionRequest, options: MissionLaunchOptions = {}): Promise<MissionHandle> {\n\t\tconst validation = validateMissionRequest(request);\n\t\tif (!validation.valid) {\n\t\t\tthrow new Error(`Invalid MissionRequest: ${validation.errors.join(\", \")}`);\n\t\t}\n\n\t\tconst executionId = options.executionId ?? this._executionIdFactory(request);\n\t\tconst controller = new AbortController();\n\t\tif (options.signal) {\n\t\t\tif (options.signal.aborted) controller.abort();\n\t\t\telse options.signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n\t\t}\n\n\t\tconst tracker = new MissionStateTracker(\"CREATED\");\n\t\ttracker.transition(\"QUEUED\");\n\t\ttracker.transition(\"RUNNING\");\n\n\t\tconst active: ActiveMission = {\n\t\t\trequest,\n\t\t\texecutionId,\n\t\t\ttracker,\n\t\t\tstartedAtMs: Date.now(),\n\t\t\tcontroller,\n\t\t\toutcomePromise: this._harness(this._buildLaunch(request), controller.signal),\n\t\t\tcancelled: false,\n\t\t};\n\t\tthis._active.set(request.missionId, active);\n\n\t\treturn createMissionHandle({\n\t\t\tmissionId: request.missionId,\n\t\t\tparentMissionId: request.parentMissionId,\n\t\t\tdepth: request.depth,\n\t\t\texecutionId,\n\t\t\tstate: tracker.state,\n\t\t\tcreatedAtMs: request.createdAtMs,\n\t\t\tstartedAtMs: active.startedAtMs,\n\t\t\tcancel: (reason) => this._cancelActive(active, reason),\n\t\t});\n\t}\n\n\tasync awaitResult(handle: MissionHandle): Promise<MissionResult> {\n\t\tconst active = this._active.get(handle.missionId);\n\t\tif (!active) {\n\t\t\tthrow new Error(`Unknown mission: ${handle.missionId}`);\n\t\t}\n\n\t\tconst outcome = await active.outcomePromise;\n\t\tactive.finishedAtMs = Date.now();\n\n\t\tconst diagnostics: ExecutorDiagnostics = {\n\t\t\texecutorId: this.executorId,\n\t\t\tprocessExitCode: outcome.exitCode,\n\t\t\tsignal: outcome.signal,\n\t\t\ttimedOut: outcome.timedOut,\n\t\t\tlaunchError: outcome.launchError,\n\t\t\tstderr: outcome.stderr.slice(0, 16_000),\n\t\t};\n\n\t\tconst { state, executionOutcome, failure, verification, completionDecision, outputText } = await this._classify(\n\t\t\tactive,\n\t\t\toutcome,\n\t\t);\n\n\t\tconst failures: StructuredFailure[] = [];\n\t\tif (failure) failures.push(failure);\n\n\t\treturn createMissionResult({\n\t\t\tmissionId: active.request.missionId,\n\t\t\tparentMissionId: active.request.parentMissionId,\n\t\t\tdepth: active.request.depth,\n\t\t\tstate,\n\t\t\texecutionOutcome,\n\t\t\toutputText,\n\t\t\tevidenceRefs: [],\n\t\t\tverification,\n\t\t\tcompletionDecision,\n\t\t\tfailures,\n\t\t\texecutorDiagnostics: diagnostics,\n\t\t\tstartedAtMs: active.startedAtMs,\n\t\t\tfinishedAtMs: active.finishedAtMs ?? Date.now(),\n\t\t});\n\t}\n\n\tasync cancel(handle: MissionHandle, reason?: string): Promise<void> {\n\t\tconst active = this._active.get(handle.missionId);\n\t\tif (!active) return;\n\t\tawait this._cancelActive(active, reason);\n\t}\n\n\tprivate async _cancelActive(active: ActiveMission, reason?: string): Promise<void> {\n\t\tactive.cancelled = true;\n\t\tactive.tracker.transition(\"CANCELLED\");\n\t\tactive.controller.abort(reason);\n\t}\n\n\tprivate async _classify(\n\t\tactive: ActiveMission,\n\t\toutcome: ProcessMissionOutcome,\n\t): Promise<{\n\t\tstate: MissionResult[\"state\"];\n\t\texecutionOutcome: MissionExecutionOutcome;\n\t\tfailure?: StructuredFailure;\n\t\tverification: MissionResult[\"verification\"];\n\t\tcompletionDecision: MissionResult[\"completionDecision\"];\n\t\toutputText?: string;\n\t}> {\n\t\tconst outputText = outcome.stdout.trim().slice(0, 16_000) || undefined;\n\n\t\tif (active.cancelled) {\n\t\t\treturn {\n\t\t\t\tstate: \"CANCELLED\",\n\t\t\t\texecutionOutcome: \"CANCELLED\",\n\t\t\t\tfailure: { category: \"CANCELLED\", message: \"mission cancelled by operator\" },\n\t\t\t\tverification: { status: \"unverified\" },\n\t\t\t\tcompletionDecision: \"unavailable\",\n\t\t\t\toutputText,\n\t\t\t};\n\t\t}\n\n\t\tconst base = classifyExecutorOutcome(outcome, { verified: false });\n\n\t\t// A verifier can promote a clean exit-0 execution to verified SUCCEEDED.\n\t\tif (base.state === \"PARTIAL\" && this._verifier) {\n\t\t\tconst verificationResult = await this._verifier({ request: active.request, outcome });\n\t\t\tif (verificationResult.verified) {\n\t\t\t\tactive.tracker.transition(\"SUCCEEDED\");\n\t\t\t\treturn {\n\t\t\t\t\tstate: \"SUCCEEDED\",\n\t\t\t\t\texecutionOutcome: \"COMPLETED\",\n\t\t\t\t\tverification: {\n\t\t\t\t\t\tstatus: \"verified\",\n\t\t\t\t\t\tsummary: verificationResult.summary,\n\t\t\t\t\t\tcriterionIds: verificationResult.criterionIds,\n\t\t\t\t\t},\n\t\t\t\t\tcompletionDecision: \"accepted\",\n\t\t\t\t\toutputText,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstate: \"FAILED\",\n\t\t\t\texecutionOutcome: \"COMPLETED\",\n\t\t\t\tfailure: {\n\t\t\t\t\tcategory: \"VERIFICATION\",\n\t\t\t\t\tmessage: verificationResult.summary ?? \"verification failed\",\n\t\t\t\t},\n\t\t\t\tverification: { status: \"failed\", summary: verificationResult.summary },\n\t\t\t\tcompletionDecision: \"rejected\",\n\t\t\t\toutputText,\n\t\t\t};\n\t\t}\n\n\t\tconst state = base.state;\n\t\tif (state !== \"PARTIAL\") active.tracker.transition(state);\n\t\telse active.tracker.transition(\"PARTIAL\");\n\n\t\treturn {\n\t\t\tstate,\n\t\t\texecutionOutcome: base.executionOutcome,\n\t\t\tfailure: base.failure,\n\t\t\tverification: { status: \"unverified\", summary: base.reason },\n\t\t\tcompletionDecision: state === \"SUCCEEDED\" ? \"accepted\" : \"unavailable\",\n\t\t\toutputText,\n\t\t};\n\t}\n}\n"]}