/* RFC-0958: mission.resume — resume the last incomplete lifecycle operation for a mission. Do not implement step logic — steps are defined in mission/steps/ and passed to runOperation. Do not manage bordbuch — bordbuch is the business ledger, the journal is technical. RFC-0958: initial mission.resume command handler. RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared RFC-1219: resolveOperationDefinition passes the reconstructed per-kind ctx into runOperation (fixes m000192 "path argument" crash); catch narrowed to the dynamic import so ctx-factory IO errors surface with real messages (REVIEW-CODE-2026-10-07-16 F-1). RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt */ import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, KernelNextStep, } from "@warpgogol/werkstatt-engine/kernel"; import { resolveMissionDir } from "./mission-io.ts"; import { readMissionManifest } from "./mission-io.ts"; import { acquireLock, releaseLock, isLockStale, removeStaleLock, readAllLocks, } from "../werkstatt/index.ts"; import { readJournal, findIncompleteOperation, abandonOperation, runOperation, } from "../journal/index.ts"; import type { OperationDefinition } from "../journal/index.ts"; import path from "node:path"; import { failResult } from "@warpgogol/werkstatt-shared/checks/result-helpers"; export interface MissionResumeData { missionId: string; resumedOp: string | null; opId: string | null; skipped: string[]; executed: string[]; completed: boolean; failedStep?: string; abandoned?: boolean; } function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } function flagBool(input: KernelCommandInput, key: string): boolean { const v = input.flags[key]; return v === true || v === "true"; } export async function runMissionResume( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot } = context; const missionId = flagString(input, "mission"); if (!missionId) { return { exitCode: 1, summary: "[mission.resume] --mission is required", nextSteps: [{ action: "Pass --mission ", kind: "required" }], }; } const missionDir = await resolveMissionDir(workspaceRoot, missionId, context.io); const journalPath = path.join(missionDir, "journal.jsonl"); const manifest = await readMissionManifest(workspaceRoot, missionId); const systemId = manifest.systemId; const operationId = manifest.operationId; const abandon = flagBool(input, "abandon"); const force = flagBool(input, "force"); if (abandon) { if (!force) { return { exitCode: 1, summary: "[mission.resume] --abandon requires --force to confirm destructive action", nextSteps: [{ action: "Re-run with --abandon --force", kind: "required" }], }; } const records = await readJournal(journalPath); const incomplete = findIncompleteOperation(records); if (!incomplete) { return { data: { missionId, resumedOp: null, opId: null, skipped: [], executed: [], completed: false, }, exitCode: 0, summary: `[mission.resume] nothing to abandon — no incomplete operation for ${missionId}`, }; } await abandonOperation(journalPath, incomplete.opId, "manual abandon via --abandon --force"); return { data: { missionId, resumedOp: incomplete.op, opId: incomplete.opId, skipped: [], executed: [], completed: false, abandoned: true, }, exitCode: 0, summary: `[mission.resume] abandoned ${incomplete.op} (${incomplete.opId}) for ${missionId}`, }; } const records = await readJournal(journalPath); const incomplete = findIncompleteOperation(records); if (!incomplete) { return { data: { missionId, resumedOp: null, opId: null, skipped: [], executed: [], completed: false }, exitCode: 0, summary: `[mission.resume] nothing to resume for ${missionId}`, }; } const def = await resolveOperationDefinition( incomplete.op, workspaceRoot, missionId, manifest, context, ); if (!def) { return { exitCode: 1, summary: `[mission.resume] unknown operation kind "${incomplete.op}" — cannot resume`, nextSteps: [ { action: `Run mission.resume --mission ${missionId} --abandon --force`, kind: "required" }, ], }; } const systemLockKey = `system:${systemId}`; const missionLockKey = `mission:${missionId}`; await removeStaleLockIfNeeded(workspaceRoot, systemLockKey); await removeStaleLockIfNeeded(workspaceRoot, missionLockKey); await acquireLock(workspaceRoot, systemLockKey, operationId, "mission.resume", "agent"); await acquireLock(workspaceRoot, missionLockKey, operationId, "mission.resume", "agent"); try { const result = await runOperation(journalPath, def, def.ctx, { resumeOpId: incomplete.opId, missionId, platformVersion: "", }); if (!result.completed) { const nextSteps: KernelNextStep[] = [ { action: `Inspect failing step "${result.failedStep}" and fix root cause`, kind: "required", }, { action: `Re-run mission.resume --mission ${missionId}`, kind: "required" }, ]; return { ...failResult("mission.resume", [ `[mission.resume] failed at step "${result.failedStep}" (${result.skipped.length} skipped, ${result.executed.length} executed)`, ]), data: { missionId, resumedOp: incomplete.op, opId: result.opId, skipped: result.skipped, executed: result.executed, completed: false, failedStep: result.failedStep, }, nextSteps, }; } return { data: { missionId, resumedOp: incomplete.op, opId: result.opId, skipped: result.skipped, executed: result.executed, completed: true, }, exitCode: 0, summary: `[mission.resume] completed ${incomplete.op} from step ${result.executed[0] ?? "n/a"} (${result.skipped.length} skipped, ${result.executed.length} executed)`, }; } finally { await releaseLock(workspaceRoot, missionLockKey); await releaseLock(workspaceRoot, systemLockKey); } } async function removeStaleLockIfNeeded(workspaceRoot: string, lockKey: string): Promise { try { const locks = await readAllLocks(workspaceRoot); const lock = locks.find((l) => l.scope === lockKey); if (lock && isLockStale(lock)) { await removeStaleLock(workspaceRoot, lockKey); } } catch { // No lock file exists — nothing to clean up } } async function resolveOperationDefinition( op: string, workspaceRoot: string, missionId: string, manifest: unknown, context: KernelRuntimeContext, ): Promise<(OperationDefinition & { ctx: unknown }) | null> { // RFC-1219 (REVIEW-CODE-2026-10-07-16 F-1): the catch covers ONLY the // dynamic import — ctx-factory IO errors (readSystemState, resolveMissionDir, // a throwing build*Steps) must propagate with their real message instead of // being masked as "no step resolver for operation kind". let stepsModule: typeof import("./steps/index.ts"); try { stepsModule = await import("./steps/index.ts"); } catch { // Steps module not yet available return null; } if (typeof stepsModule.resolveOperationSteps !== "function") { return null; } const resolved = await stepsModule.resolveOperationSteps( op, workspaceRoot, missionId, manifest, context, ); if (!resolved) { return null; } // ctx carries the reconstructed per-kind step context — passing it to // runOperation lets resumed steps execute against the same shape the // command handler built (fixes the m000192 "path argument must be of type // string" crash). return { op, steps: resolved.steps, ctx: resolved.ctx }; }