/** * Public Inspector Role run: admit instruction/attachments → shared post-admission * coordinator → settle Terminal result (#568 / ADR 0074). Lawful releases: * pass/bounce/escalate. #633: manual resume continues the exact session. Dual path * with gate-province dispatch; this module is the direct command face. * #637 / #747: same-parent (卷宗指针) re-summons resume the seat's previous run. */ import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts"; import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.ts"; import { CliUsageError } from "./cli-errors.ts"; import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts"; import { admitInspectorInvocation, buildInspectorTransportPrompt, persistAdmittedSourceRunPath, type AdmittedInspectorInvocation, type ParseInspectorArgvResult, } from "./invocation.ts"; import { prepareSummonsResumeMaterials, runPostAdmissionOneShot, type PostAdmissionAdapters, type PostAdmissionEnv, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.ts"; import { loadResumableInspectorRun, markRunAdmitted, parentRunPathFromGatePointerInstruction, type PublicResumeRequest, type SameTicketSummonsMaterials, } from "./run-lifecycle.ts"; import { presentStructuralRejection, trySettleInspectorTerminalResult, } from "./settlement.ts"; import type { CliIo } from "./cli-io.ts"; import type { TerminalResult } from "./terminal.ts"; import { projectRoleTurnRequest, type RoleTurnRequestProjectionOptions, } from "./turn-request.ts"; export type InspectorRunEnv = PostAdmissionEnv & { principalAuthority: DurablePrincipalAuthority; createRunId?: () => string; /** * #753 nested gate re-ask: plain-language instruction on same-ticket resume. * Only set by summonPublicRole when the prior officer reply was not three-state. * Never a public CLI argv — must not pollute the 卷宗指针 parentRunPath key (#747). */ reviewReask?: string; /** * #879 gate summons: verbatim parent-submission body for officer dialogue content. * Rides summons.instruction / first-mint prompt when reviewReask is absent. * Argv 卷宗指针 stays the binding/lookup key only. */ gateReviewInstruction?: string; }; function inspectorParentRunPath( admitted: AdmittedInspectorInvocation, ): string | undefined { if (typeof admitted.sourceRunPath === "string" && admitted.sourceRunPath.trim() !== "") { return admitted.sourceRunPath; } return parentRunPathFromGatePointerInstruction(admitted.instruction); } /** Project admitted invocation onto the host-neutral turn request. */ export function buildInspectorTurnRequest( admitted: AdmittedInspectorInvocation, options: RoleTurnRequestProjectionOptions, ): RoleTurnRequest { const sourceRun = inspectorParentRunPath(admitted); return projectRoleTurnRequest( admitted, { activation: { role: "inspector" as const, ...(sourceRun === undefined ? {} : { sourceRun }), }, }, options, ); } export async function runPublicInspector( argv: readonly string[], env: InspectorRunEnv, io: CliIo, parseInspectorArgv: (args: readonly string[]) => ParseInspectorArgvResult, ): Promise<{ exitCode: number; admitted?: AdmittedInspectorInvocation; terminal?: TerminalResult; }> { let parsed: ParseInspectorArgvResult; try { parsed = parseInspectorArgv(argv); } catch (error) { if (error instanceof CliUsageError) { presentStructuralRejection(error, io); return { exitCode: 2 }; } throw error; } // #747: same parent (卷宗指针) → resume prior inspector run with this summons' materials. // #771: no mechanical ticket match against instruction text; parent-run path only. // No bare catch→fresh: lookup/resume failures surface; only true absence mints new. const projectRoot = parsed.project ?? env.cwd; // #747: parentRunPath is the pure 卷宗指针 path only — never reask/materials text. // #753/#786: gate re-ask / verbatim submission body ride summons.instruction on resume. const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction); if (parentRunPath !== undefined) { const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction; const summons: SameTicketSummonsMaterials = { sourceRunPath: parentRunPath, ...(resumeInstruction === undefined ? { instruction: parsed.instruction, instructionEmpty: parsed.instruction.trim() === "", } : { instruction: resumeInstruction, instructionEmpty: false }), attachmentPaths: parsed.attachmentPaths, }; const resumed = await tryResumeSameTicketSeatRun({ home: env.home, projectRoot, role: "inspector", parentRunPath, freshSummons: env.freshSummons, summons, resume: (runId, materials) => runPublicInspectorResume( { runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io, ), }); if (resumed !== undefined) return resumed; } let admitted: AdmittedInspectorInvocation; try { admitted = await admitInspectorInvocation({ home: env.home, principalAuthority: env.principalAuthority, cwd: env.cwd, instruction: parsed.instruction, attachmentPaths: parsed.attachmentPaths, ...(parsed.project === undefined ? {} : { project: parsed.project }), ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }), ...(env.model === undefined ? {} : { model: env.model }), ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }), }); } catch (error) { if (error instanceof CliUsageError) { presentStructuralRejection(error, io); return { exitCode: 2 }; } throw error; } await markRunAdmitted(admitted, env.principalAuthority); if (parentRunPath !== undefined) { await persistAdmittedSourceRunPath(admitted, parentRunPath); admitted = { ...admitted, sourceRunPath: parentRunPath }; } const engineMaterial = engineSessionMaterialFromOptions({ ...pickEngineAxis(env), packageRoot: env.packageRoot, }); const turnRequest = buildInspectorTurnRequest(admitted, { packageRoot: env.packageRoot, home: env.home, agentDir: env.agentDir, ...(env.model === undefined ? {} : { model: env.model }), ...pickEngineAxis(env), ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }), ...(env.correlationId === undefined || env.correlationId.trim() === "" ? {} : { correlationId: env.correlationId }), continuation: { kind: "initial", // #879: gate first mint uses parent payload as content; binding stays typed activation. prompt: (env.reviewReask ?? env.gateReviewInstruction) ?? buildInspectorTransportPrompt(admitted, engineMaterial), }, }); return await runPostAdmissionOneShot({ admitted, env, io, request: turnRequest, adapters: inspectorAdapters(), ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }), }); } function inspectorAdapters(options?: { beforeDispatch?: ( admitted: AdmittedInspectorInvocation, ) => void | Promise; }): PostAdmissionAdapters { return { trySettle: (admitted, authority, scope) => trySettleInspectorTerminalResult(admitted, authority, scope), shouldPresentSettled: () => true, ...(options?.beforeDispatch === undefined ? {} : { beforeDispatch: options.beforeDispatch }), }; } /** * Resume a previously admitted Inspector run (#633 / #637); the session principal reopens. * Same-ticket summons deliver this turn's instruction + frozen attachments; manual * resume forwards only caller-supplied message bytes. */ export async function runPublicInspectorResume( request: PublicResumeRequest, env: InspectorRunEnv, io: CliIo, ): Promise<{ exitCode: number; admitted?: AdmittedInspectorInvocation; terminal?: TerminalResult; }> { return await runPostAdmissionSeatResume({ request, env, io, load: (effective) => loadResumableInspectorRun( env.home, effective.runId, env.principalAuthority, ), buildTurnRequest: async (admitted, effective) => { const summonsPrepared = await prepareSummonsResumeMaterials( admitted.runDirectory, effective.summons, ); return buildInspectorTurnRequest( admitted, resumeTurnRequestProjectionOptions( admitted, effective, env, summonsPrepared, ), ); }, adapters: inspectorAdapters(), ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }), }); }