import { randomUUID } from "node:crypto"; import { readFileSync, writeSync } from "node:fs"; import { join } from "node:path"; import { ExplicitInternalActivationError, isOfficerReviewSeat, runDirectoryFromHostContext, type HostContext, type HostToolResult, type RoleEnvelopeHost, type RoleHost, } from "./host-contracts.ts"; import { Value } from "typebox/value"; import { sitianReport } from "./sitian-facade.ts"; import { createSubmissionLedgerHost, sealAcceptedSubmission } from "./submission-ledger.ts"; import { createCollectorLedger } from "./collector-ledger.ts"; import { activationTraceRecordSchema, namedActivationCause, type ActivationTraceRecord, type ActivationTraceWriter } from "./activation-trace.ts"; import { homeFromRunDirectory } from "./activation-ledger-topology.ts"; import { durableSessionPointer, resolveBookKeyFromGit, } from "./activation-ledger.ts"; import { writeStderrJsonlRecord } from "./stderr-jsonl.ts"; import { createToolExecutionObservationFace, systemToolExecutionObservationMonoNow, writeToolExecutionObservationRecord, type ToolExecutionObservationWriter, } from "./tool-execution-observation.ts"; import { ENGINE_DETOUR_TOOL_NAME, ENGINE_MODEL_FLAG_NAME, resolveEngineModel, resolveEngineName, } from "./engine-detour.ts"; import { engineSessionMaterialFromOptions } from "./package-resources/engine-material.ts"; import { registerEngineDetourTool } from "./engine-detour-tool.ts"; import { readableGateItem } from "./readable-gate-item.ts"; import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts"; import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts"; import type { AnyCanonicalSkillBinding } from "./canonical-skill-binding.ts"; import type { CollectorClock } from "./collector-evidence.ts"; import type { CollectorGitHubTransport } from "./collector-github.ts"; import { COLLECTOR_REQUIRED_TOOLS, COLLECTOR_TRANSPORT_FLAGS, createCollectorRoleRuntime, type CollectorActivation, } from "./collector-role.ts"; import type { ComplianceDecision } from "./compliance-transport.ts"; import { createDoctorRoleRuntime } from "./doctor-role.ts"; import { createNotaryRoleRuntime, NOTARY_SESSION_BOUND_ENTRY, projectNotaryBoundFromFlags, readNotaryTicketFlag, } from "./notary-role.ts"; import { NOTARY_TICKET_FLAG } from "./notary-contracts.ts"; import { COUNTERSIGN_TOOL_SPEC, type CountersignRuntimeDependencies, } from "./countersign-role.ts"; import { COUNTERSIGN_ACCEPTED_TEXT } from "./countersign-contracts.ts"; import { GLEANER_LEFT_TOOL_SPEC, type GleanerLeftRuntimeDependencies, } from "./gleaner-left-role.ts"; import { GLEANER_LEFT_ACCEPTED_TEXT, GLEANER_LEFT_BASE_FLAG } from "./gleaner-left-contracts.ts"; import { INSPECTOR_TOOL_SPEC, type InspectorRuntimeDependencies, } from "./inspector-role.ts"; import { INSPECTOR_ACCEPTED_TEXT, INSPECTOR_SOURCE_RUN_FLAG } from "./inspector-contracts.ts"; import { DIARIST_TOOL_SPEC, type DiaristRuntimeDependencies, } from "./diarist-role.ts"; import { SECRETARIAT_OUTPUT_TOOL_SPEC, SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC, projectSecretariatSummonResult, type SecretariatRuntimeDependencies, type SecretariatSummonCountersignParameters, } from "./secretariat-role.ts"; import { SECRETARIAT_ACCEPTED_TEXT, SECRETARIAT_OUTPUT_TOOL_NAME, SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME, } from "./secretariat-contracts.ts"; import { DIARIST_ACCEPTED_TEXT, projectDiaristSessions, } from "./diarist-contracts.ts"; import { commitDiaristProjection } from "./diarist.ts"; import { TicketProvenanceInputError } from "./ticket-provenance.ts"; import { bindTicketNumberOnRunDirectory } from "./public-cli/invocation.ts"; import { GATEKEEPER_TOOL_SPEC, type GatekeeperRuntimeDependencies, } from "./gatekeeper-role.ts"; import { NAVIGATOR_TOOL_SPEC, type NavigatorRuntimeDependencies, } from "./navigator-role.ts"; import { AUDITOR_TOOL_SPEC, type AuditorRuntimeDependencies, } from "./auditor-role.ts"; import { AUDITOR_ACCEPTED_TEXT } from "./package-contracts/auditor-output.ts"; import { GATEKEEPER_ACCEPTED_TEXT } from "./package-contracts/gatekeeper-output.ts"; import { NAVIGATOR_ACCEPTED_TEXT } from "./package-contracts/navigator-output.ts"; import { formatNavigatorReport, NAVIGATOR_EVENT_TYPE, navigatorSubjectKey, navigatorUnavailableError, subjectPath, type NavigatorAttendance, type NavigatorAttendanceOptions, type NavigatorEvent, type NavigatorPhase, type NavigatorReport, type NavigatorSettlement, type NavigatorSubjectProvenance, type NavigatorTargetRole, type NavigatorWorkContext } from "./navigator-attendance.ts"; import { buildNavigatorInfrastructureFailureFact, classifyPackagedRoleTerminalResult, extractInfrastructureFailureEvidence, NAVIGATOR_INVOCATION_ENTRY, resolveLifecycleInvocationPrincipal, } from "./navigator-invocation-identity.ts"; import { recordTypedProviderHttpStatus } from "./typed-provider-http.ts"; import { NAVIGATOR_POST_ROLE_GRACE_MS, raceNavigatorGrace } from "./public-cli/settlement.ts"; import { PACKAGED_ROLE_REGISTRY, packagedRoleMetadata, packagedRoleOutputTool, packagedRolePhaseFlag, type PackagedRole } from "./packaged-role-registry.ts"; import { isAuditEscalationProjection } from "./audit-escalation.ts"; import { createJudgeRoleRuntime, } from "./judge-role.ts"; import { createReviewerRoleRuntime, type ReviewerActivation, type ReviewerAdmittedInputs, } from "./reviewer-role.ts"; import type { GatekeeperNonPassResult } from "./gatekeeper-role.ts"; /** * Private transport flag names/definitions for Reviewer admitted inputs. * Shared activation envelope owns registration and decoding (ADR 0018). */ const REVIEWER_TRANSPORT_FLAGS = Object.freeze([ Object.freeze({ name: "ak-review-base", definition: Object.freeze({ description: "Fixed base revision for the pinned review target", type: "string" as const, }), }), Object.freeze({ name: "ak-review-lens", definition: Object.freeze({ description: "Single review lens: completeness or correctness", type: "string" as const, }), }), Object.freeze({ name: "ak-review-authority-refs", definition: Object.freeze({ description: "JSON array of durable authority references projected as Skill --authority inputs", type: "string" as const, }), }), Object.freeze({ name: "ak-review-ticket-number", definition: Object.freeze({ description: "Typed ticketNumber for Spec self-fetch primary path", type: "string" as const, }), }), ] as const); /** Notary private transport: optional court-diary ticket (ADR 0018 / 0075 — envelope-owned). */ const NOTARY_TRANSPORT_FLAGS = Object.freeze([ Object.freeze({ name: NOTARY_TICKET_FLAG.name, definition: NOTARY_TICKET_FLAG.definition, }), ] as const); /** Gleaner-left private transport: comparison-base revision (ADR 0018 / #502). */ const GLEANER_LEFT_TRANSPORT_FLAGS = Object.freeze([ Object.freeze({ name: GLEANER_LEFT_BASE_FLAG.name, definition: GLEANER_LEFT_BASE_FLAG.definition, }), ] as const); export const STATION_CHILD_FLAG = Object.freeze({ name: "ak-station-child", definition: Object.freeze({ description: "Station-child role run (omit automatic navigator attendance; #840)", type: "boolean" as const, }), } as const); /** * Decode private transport flags into frozen admitted inputs. * Envelope-owned; necessary JSON decode only (public --authority-ref owns grammar). */ function decodeReviewerAdmittedInputs(getFlag: (name: string) => unknown): ReviewerAdmittedInputs { let authorityRefs: readonly string[] | undefined; const rawAuthorityRefs = getFlag("ak-review-authority-refs"); if (rawAuthorityRefs !== undefined) { if (typeof rawAuthorityRefs !== "string") { throw new Error("Reviewer authority refs transport error: flag value must be a string"); } let parsed: unknown; try { parsed = JSON.parse(rawAuthorityRefs); } catch (error) { throw new Error( `Reviewer authority refs transport error: JSON decode failed: ${error instanceof Error ? error.message : String(error)}`, ); } if (!Array.isArray(parsed) || parsed.some((ref) => typeof ref !== "string")) { throw new Error("Reviewer authority refs transport error: expected a JSON array of strings"); } authorityRefs = Object.freeze(parsed as string[]); } let ticketNumber: number | undefined; const rawTicketNumber = getFlag("ak-review-ticket-number"); // Shape-invalid flag values do not abort: omit typed candidate; branch→commit→degrade continues. if (typeof rawTicketNumber === "string" && /^[1-9]\d*$/.test(rawTicketNumber)) { ticketNumber = Number(rawTicketNumber); } const baseRevision = getFlag("ak-review-base"); if (typeof baseRevision !== "string" || !baseRevision.trim()) { throw new Error("Reviewer role requires --ak-review-base"); } const rawLens = getFlag("ak-review-lens"); if (rawLens !== "completeness" && rawLens !== "correctness") { throw new Error("Reviewer role requires --ak-review-lens completeness|correctness"); } return Object.freeze({ baseRevision, lens: rawLens, ...(authorityRefs === undefined ? {} : { authorityRefs }), ...(ticketNumber === undefined ? {} : { ticketNumber }), }); } /** * Parent system-prompt assembly for the shared activation envelope. * Verification cadence lives in soul/quality-law only (ADR 0073: no machine copy). */ function assembleReviewerParentSystemPrompt(input: { baseSystemPrompt: string; soul: string; }): string { return [ input.baseSystemPrompt, "", "", input.soul, "", ].join("\n"); } import { CODER_OUTPUT_TOOL_NAME, createCoderRoleRuntime, createFixerRoleRuntime, FIXER_FLAG_DEFINITIONS, FIXER_OUTPUT_TOOL_NAME, FIXER_PHASES, type CoderSkillExpansionEvidenceMissingResult, } from "./worker-role.ts"; /** One envelope map for Gatekeeper non-pass and other correct submission rejects (#525). */ type SubmissionNonPassResult = | GatekeeperNonPassResult | CoderSkillExpansionEvidenceMissingResult; import { JUDGE_OUTPUT_TOOL_NAME } from "./package-contracts/judge-output.ts"; import { REVIEWER_OUTPUT_TOOL_NAME } from "./package-contracts/reviewer-output.ts"; import { DOCTOR_OUTPUT_TOOL_NAME } from "./doctor-contracts.ts"; import { MERGER_OUTPUT_TOOL_NAME } from "./merger-contracts.ts"; import { createMergerRoleRuntime, type MergerRoleDependencies } from "./merger-role.ts"; export { buildNavigatorInfrastructureFailureFact, classifyPackagedRoleTerminalResult, extractInfrastructureFailureEvidence, hasNavigatorInfrastructureFailureBase, isAcceptedPackagedRoleTerminalResult, isDurablePackagedRoleTerminalResult, isNavigatorInfrastructureFailureFact, NAVIGATOR_INFRASTRUCTURE_FAILURE_EVIDENCE_KEYS, NAVIGATOR_INFRASTRUCTURE_FAILURE_KIND, type NavigatorInfrastructureFailureFact, type PackagedRoleTerminalClassification, } from "./navigator-invocation-identity.ts"; export { activationTraceRecordSchema, namedActivationCause } from "./activation-trace.ts"; export type { ActivationTraceRecord, ActivationTraceWriter } from "./activation-trace.ts"; export { ActivationGitRepositoryRequiredError, ActivationLedgerError, ActivationSessionFileMissingError, activationBookDirectory, durableSessionPointer, resolveActivationLedgerHome, resolveBookKeyFromGit, } from "./activation-ledger.ts"; export type { ActivationSessionManager, ActivationSessionPointer, } from "./activation-ledger.ts"; export { TOOL_EXECUTION_UPDATE_HEARTBEAT, TOOL_EXECUTION_UPDATE_THROTTLE_MS, createToolExecutionObservationFace, isProducingToolUpdate, systemToolExecutionObservationMonoNow, toolExecutionObservationRecordSchema, validateToolExecutionObservationRecord, writeToolExecutionObservationRecord, } from "./tool-execution-observation.ts"; export type { ToolExecutionObservationRecord, ToolExecutionObservationWriter } from "./tool-execution-observation.ts"; import { NOTARY_OUTPUT_TOOL, INSPECTOR_OUTPUT_TOOL, GatekeeperDecisionError, createGatekeeperOutputTool, runGatekeeper, gateOfficerForSubject, } from "./gatekeeper-role.ts"; export { NOTARY_OUTPUT_TOOL, INSPECTOR_OUTPUT_TOOL, GatekeeperDecisionError, createGatekeeperOutputTool, runGatekeeper, gateOfficerForSubject, }; export type { GatekeeperResult, GatekeeperSubject, GatekeeperNonPassResult, GateOfficer, RunGatekeeperOptions } from "./gatekeeper-role.ts"; import { ParentQueueReaskError } from "./submission-errors.ts"; export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME, } from "./doctor-role.ts"; export type { DoctorCase, DoctorCaseCost, DoctorSubmission, DoctorOutput, DoctorFinding } from "./doctor-contracts.ts"; export { validateDoctorSubmissionShape, validateDoctorOutput, DoctorEvidenceStore } from "./doctor-contracts.ts"; export { loadDoctorCase } from "./doctor-evidence.ts"; export { JUDGE_OUTPUT_TOOL_NAME, type JudgeVerdict, } from "./judge-role.ts"; export { ENGINE_DETOUR_TOOL_NAME, AK_ROLE_ENGINE_ENV } from "./engine-detour.ts"; export { REVIEWER_OUTPUT_TOOL_NAME, type ReviewerIntent, } from "./reviewer-role.ts"; export { CODER_OUTPUT_TOOL_NAME, CODER_SKILL_EXPANSION_EVIDENCE_MISSING_CODE, CoderSkillExpansionEvidenceMissingError, FIXER_FLAG_DEFINITIONS, FIXER_OUTPUT_TOOL_NAME, FIXER_PHASES, type CoderOutput, type CoderSkillExpansionEvidenceMissingResult, type FixerOutput, type WorkerOutput, } from "./worker-role.ts"; export { fixerOutputSchema, validateFixerOutput } from "./package-contracts/fixer-output.ts"; export type { FixerBlocker, FixerClassResult, FixerPhase, FixerTestEvidence } from "./package-contracts/fixer-output.ts"; export { fixerPrerequisiteSchema, fixerPrerequisitesSchema, parseFixerPrerequisites, validateFixerPrerequisites } from "./package-contracts/fixer-packet.ts"; export type { FixerInvocationInput, FixerPrerequisite } from "./package-contracts/fixer-packet.ts"; export { AUDIT_ESCALATION_KIND, buildAuditEscalationResult, disposeComplianceDecision, isAuditEscalationResult, projectAuditEscalation } from "./audit-escalation.ts"; export type { AuditEscalationResult, AuditEscalationToolResult, ComplianceDecisionHandlers } from "./audit-escalation.ts"; export { AUDITOR_SOUL_ROLES, AK_ROLE_AUDITOR_SUBJECT_ENV, loadAuditorSoul, loadAuditorSoulFromSubjectInput, resolveAuditorSubject, } from "./auditor-soul.ts"; export type { AuditorSoulRole } from "./auditor-soul.ts"; export { JUDGE_AUDIT_TOOL_NAME, SOUL_AUDIT_TOOL_NAME } from "./judge-auditor.ts"; export { DOCTOR_AUDIT_TOOL_NAME, createPiDoctorAuditor } from "./doctor-auditor.ts"; export type { ComplianceDecision } from "./compliance-transport.ts"; export { COLLECTOR_OBSERVE_TOOL, COLLECTOR_OUTPUT_TOOL, COLLECTOR_READ_TOOL, COLLECTOR_REQUEST_TOOL, COLLECTOR_WAIT_TOOL, } from "./collector-role.ts"; export type { CollectorReceipt } from "./package-contracts/collector-output.ts"; export type { CollectorGitHubTransport } from "./collector-github.ts"; export type { CollectorClock } from "./collector-evidence.ts"; export * from "./navigator-attendance.ts"; export { MERGER_INPUT_FLAG, createMergerRoleRuntime } from "./merger-role.ts"; export { MERGER_OUTPUT_TOOL_NAME, mergerInputSchema, mergerOutputSchema, validateMergerInput, validateMergerOutput } from "./merger-contracts.ts"; export type { MergerInput, MergerMaterial, MergerOutput } from "./merger-contracts.ts"; export { createProductionMergerGitState } from "./merger-git-state.ts"; export type { MergerGitState, ActiveMergerGitState } from "./merger-git-state.ts"; export type { MergerRoleDependencies } from "./merger-role.ts"; type WorkerArmable = { activate(context?: HostContext): Promise; armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }): void; }; type ActivationRuntime = { event: { reason: string }; context: HostContext; judge: { activate(): Promise }; fixer: WorkerArmable; coder: WorkerArmable; reviewer: { activate( context: HostContext, admitted: ReviewerAdmittedInputs, ): Promise; }; /** Envelope decodes Reviewer transport flags inside the activation stage. */ decodeReviewerAdmitted(): ReviewerAdmittedInputs; /** Envelope stores live Reviewer activation for agent_start prompt assembly. */ bindReviewerParent(activation: ReviewerActivation): void; collector: { activate(context: HostContext, event: { reason: string }): Promise; }; doctor: { activate(): Promise }; notary: { activate(admitted?: import("./notary-role.ts").NotaryAdmittedTicket): Promise; }; /** Envelope decodes Notary ticket flag inside the activation stage (ADR 0018). */ decodeNotaryAdmitted(): import("./notary-role.ts").NotaryAdmittedTicket | undefined; countersign: { activate(): Promise }; gleanerLeft: { activate(): Promise }; inspector: { activate(): Promise }; gatekeeper: { activate(): Promise }; navigator: { activate(): Promise }; auditor: { activate(): Promise }; diarist: { activate(): Promise }; secretariat: { activate(): Promise }; merger(): Promise; }; function activationStage(role: PackagedRole, runtime: ActivationRuntime): { id: string; run(): Promise } { switch (role) { case "judge": return { id: "load-and-install", run: async () => runtime.judge.activate() }; case "fixer": return { id: "load-and-install", run: async () => runtime.fixer.activate() }; case "coder": return { id: "load-and-install", run: async () => runtime.coder.activate(runtime.context) }; case "reviewer": return { id: "load-and-install", run: async () => { const admitted = runtime.decodeReviewerAdmitted(); const activation = await runtime.reviewer.activate(runtime.context, admitted); runtime.bindReviewerParent(activation); } }; case "collector": return { id: "load-and-install", run: async () => runtime.collector.activate(runtime.context, runtime.event) }; case "doctor": return { id: "load-and-install", run: async () => runtime.doctor.activate() }; case "notary": return { id: "load-and-install", run: async () => { // Envelope owns ticket flag read (ADR 0018); role receives admitted value only. await runtime.notary.activate(runtime.decodeNotaryAdmitted()); }, }; case "countersign": return { id: "load-and-install", run: async () => runtime.countersign.activate() }; case "gleaner-left": return { id: "load-and-install", run: async () => runtime.gleanerLeft.activate() }; case "inspector": return { id: "load-and-install", run: async () => runtime.inspector.activate() }; case "gatekeeper": return { id: "load-and-install", run: async () => runtime.gatekeeper.activate() }; case "navigator": return { id: "load-and-install", run: async () => runtime.navigator.activate() }; case "auditor": return { id: "load-and-install", run: async () => runtime.auditor.activate() }; case "diarist": return { id: "load-and-install", run: async () => runtime.diarist.activate() }; case "secretariat": return { id: "load-and-install", run: async () => runtime.secretariat.activate() }; case "merger": return { id: "prepare-git-and-install", run: async () => runtime.merger() }; } } function validateActivationTraceRecord(record: unknown): ActivationTraceRecord { if (!Value.Check(activationTraceRecordSchema, record)) { throw new TypeError("Activation trace record does not match its closed contract"); } return record as ActivationTraceRecord; } async function emitActivationTrace( writeTrace: (record: ActivationTraceRecord) => void | Promise, record: unknown, ): Promise { await writeTrace(validateActivationTraceRecord(record)); } async function executeActivationStage( role: string, stage: { id: string; run(): Promise }, infrastructure: { clock(): string; writeTrace(record: ActivationTraceRecord): void | Promise }, ): Promise { try { await stage.run(); } catch (activationError) { try { await emitActivationTrace(infrastructure.writeTrace, { role, stageId: stage.id, status: "failed", timestamp: infrastructure.clock(), cause: namedActivationCause(activationError), }); } catch (traceError) { throw new AggregateError([activationError, traceError], `Activation stage ${stage.id} failed and its failure trace could not be emitted`); } throw activationError; } } export function writeActivationTraceRecord( record: ActivationTraceRecord, write: typeof writeSync = writeSync, ): void { writeStderrJsonlRecord(record, write); } export class ActivationBarrierError extends Error { readonly code = "AK_ACTIVATION_NOT_ADMITTED"; constructor(role: unknown) { super(`Workflow role ${String(role)} activation did not complete`); this.name = "ActivationBarrierError"; } } export const WORKFLOW_ROLES = PACKAGED_ROLE_REGISTRY.map(({ role }) => role) as Array<(typeof PACKAGED_ROLE_REGISTRY)[number]["role"]>; export const ROLE_FLAG = { name: "ak-role", definition: { description: `Activate a packaged workflow role: ${WORKFLOW_ROLES.slice(0, -1).join(", ")}, or ${WORKFLOW_ROLES.at(-1)}`, type: "string" as const, }, } as const; /** Host-neutral in-process role help for Navigator prepare (Pi and Grok share this). */ export function formatNavigatorRoleHelp(role: NavigatorTargetRole): string { const metadata = packagedRoleMetadata(role); const lines = [ `Usage: ak-role ${role}`, ROLE_FLAG.definition.description, ]; if (metadata?.inputFlag !== undefined) { lines.push(` --${metadata.inputFlag} ${role} input material`); } if (metadata?.phaseFlag !== undefined) { lines.push( ` --${metadata.phaseFlag} ${role} phase: ${(metadata.phases.filter((p) => p !== null) as string[]).join(" | ")}`, ); } lines.push(`Public next-command form: ak-role ${role}`); return lines.join("\n"); } type NavigatorAttendanceDependency = Omit & Partial>; export type RoleRuntimeDependencies = { /** Package root for packaged engine-note resolution (#879). */ packageRoot?: string; /** * Composition-root adapter table for nested gate officer summons (#969). * Production leaves unset (default pi + packaged externals). Tests inject * faux nested hosts so prepareRoleEnvelope.requireGatekeeperPass is bitten * without reimplementing the envelope summon closure. */ hostAdapters?: readonly import("./public-cli/role-turn-host-resolution.ts").NamedRoleTurnHostAdapter[]; /** Non-identity opening materials delivered in the ordinary role brief. */ loadRoleReferenceMaterials?(role: PackagedRole): Promise; loadJudgeSoul(): Promise; loadFixerSoul?(): Promise; loadFixPacket?(path: string): Promise; loadCoderSoul?(): Promise; loadCoderTask?(path: string): Promise; loadReviewerSoul?(): Promise; loadCollectorSoul?(): Promise; /** #677: optional packaged seed for first-use general bot handbook. */ loadCollectorHandbookSeed?(): Promise; createCollectorTransport?(): CollectorGitHubTransport; loadDoctorSoul?(): Promise; loadNotarySoul?(): Promise; loadNotarySourceRun?(path: string): Promise; loadCountersignSoul?(): Promise; loadGleanerLeftSoul?(): Promise; loadInspectorSoul?(): Promise; loadGatekeeperSoul?(): Promise; loadNavigatorSoul?(): Promise; loadAuditorSoul?(): Promise; loadDiaristSoul?(): Promise; loadSecretariatSoul?(): Promise; loadDoctorCase?(path: string): Promise; loadMergerSoul?(): Promise; loadMergerInput?(path: string): Promise; auditDoctorCompliance?(options: { context: HostContext; signal?: AbortSignal }): Promise; createCollectorClock?(): CollectorClock; createNavigatorAttendance?(options: { context: HostContext; role: string; phase: NavigatorPhase; subjectKey: string; subject: string; authority: string; contextError?: unknown; invocationId: string; onEvent: (event: import("./navigator-attendance.ts").NavigatorEvent, report: import("./navigator-attendance.ts").NavigatorReport) => void | Promise }): NavigatorAttendanceDependency | Promise; loadNavigatorWorkContext?(options: { context: HostContext; role: string; phase: NavigatorPhase; getFlag?: (name: string) => unknown }): Promise; loadCanonicalSkillBinding?( name: "tdd" | "ak-cross-m-review", ): Promise; activationClock?(): string; activationTraceWriter?: (record: ActivationTraceRecord) => void | Promise; /** Wall-clock ISO timestamps for tool-execution observation records; defaults to activationClock/Date. */ toolExecutionObservationClock?(): string; /** Monotonic ms clock for update throttling; defaults to performance.now (not Date.now). */ toolExecutionObservationMonoNow?(): number; toolExecutionObservationWriter?: ToolExecutionObservationWriter; }; function abortContext(ctx: { abort(): void }): void { ctx.abort(); } function failInfrastructure(error: unknown, ctx: { mode: string; abort(): void }): never { abortContext(ctx); if (ctx.mode === "print" || ctx.mode === "json") process.exitCode = 1; throw error; } /** * Envelope-owned pending infrastructure failure: closed fact + original Error evidence. * tool_result projects once from this record; settlement consumes durable details as-is. */ type PendingInfrastructureFailure = { readonly details: Record; }; function buildPendingInfrastructureFailure(error: unknown): PendingInfrastructureFailure { return { details: { ...buildNavigatorInfrastructureFailureFact(), ...extractInfrastructureFailureEvidence(error), }, }; } function navigatorPhase(roleHost: RoleHost, role: string): NavigatorPhase { const metadata = packagedRoleMetadata(role); if (metadata === undefined || metadata.phases[0] === null) return null; const phaseFlag = packagedRolePhaseFlag(role); const requested = phaseFlag === undefined ? undefined : roleHost.getFlag(phaseFlag); return requested === "apply" ? "apply" : "plan"; } function navigatorOutputTool(role: string): string | undefined { return packagedRoleOutputTool(role); } export function publicNavigatorSettlement(role: string, phase: NavigatorPhase, event: { toolName: string; isError?: unknown; details: unknown }): NavigatorSettlement | undefined { // One shared classifier owns terminal discriminant (lifecycle + settlement + extractors). if (event.toolName !== navigatorOutputTool(role)) return undefined; const classification = classifyPackagedRoleTerminalResult(event); if (classification.kind === "nonterminal") return undefined; if (classification.kind === "infrastructure") { return { kind: "role_infrastructure_failure", role, phase }; } // accepted/human — project role/phase status; classifier already rejected infra/contradiction. const details = typeof event.details === "object" && event.details !== null && !Array.isArray(event.details) ? event.details as Record : {}; // Live Navigator consumes only the audit-owned projection; persisted/replayed // records are re-authenticated by settlement against retained audit evidence. if (isAuditEscalationProjection(event.details)) { return { kind: "human_decision", role, phase, status: "audit_escalation" }; } const status = typeof details.status === "string" ? details.status : typeof details.judgeStatus === "string" ? details.judgeStatus : typeof details.countersignStatus === "string" ? details.countersignStatus : typeof details.secretariatStatus === "string" ? details.secretariatStatus : undefined; if (status !== undefined && status === "escalate") { return { kind: "human_decision", role, phase, status }; } return { kind: "accepted", role, phase, ...(status === undefined ? {} : { status }) }; } export async function projectClosedSubmissionLifecycle( closed: import("./submission-ledger.ts").ClosedSubmission, context: HostContext, phase: NavigatorPhase, recordAccepted: () => void, settle: (settlement: NavigatorSettlement | undefined) => Promise, ): Promise { recordAccepted(); const closure = { toolName: navigatorOutputTool(closed.role)!, isError: false, details: closed.accepted, }; context.sessionManager.appendCustomEntry?.("ak-role-submission-closure", closure); await settle(publicNavigatorSettlement(closed.role, phase, closure)); } /** * Optional pre-accept hook on the shared filed-officer envelope (ADR 0075). * May return a details projection (envelope-owned machine facts recorded next to * the submitted parameters); undefined keeps the parameters as submitted. */ type FiledOfficerBeforeAccept = (input: { readonly toolCallId: string; readonly parameters: unknown; readonly signal: AbortSignal | undefined; readonly ctx: HostContext; }) => Promise; /** * Shared registration envelope for filed officers (ADR 0018 / #572): * activate, tool register, before_agent_start prompt, inventory check. * Role module keeps label/soul/spec shape only; sole-final barrier is ledger-owned. * Optional beforeAccept is the sole extension seam (e.g. countersign Notary gate). */ function createFiledOfficerRuntime( roleHost: RoleHost, spec: { role: string; tool: { name: string; label: string; description: string; promptSnippet: string; parameters: unknown }; acceptedText: string; soulTag: string; beforeAccept?: FiledOfficerBeforeAccept; }, dependencies: { loadSoul(): Promise }, ) { let soul: string | undefined; let registered = false; return { async activate() { const loaded = (await dependencies.loadSoul()).trim(); if (loaded.length === 0) throw new Error(`${spec.role} soul is empty`); soul = loaded; if (!registered) { registered = true; roleHost.registerTool({ name: spec.tool.name, label: spec.tool.label, description: spec.tool.description, promptSnippet: spec.tool.promptSnippet, parameters: spec.tool.parameters as never, async execute(toolCallId, parameters, signal, _onUpdate, ctx): Promise> { if (soul === undefined) throw new Error(`${spec.role} 职分未装载`); const projected = spec.beforeAccept === undefined ? undefined : await spec.beforeAccept({ toolCallId, parameters, signal, ctx }); // Accept-as-is + terminate only. Shape is not an admission gate // (第 0 条 / ADR 0055); sole-final barrier is ledger-owned (#575). return { content: [{ type: "text" as const, text: spec.acceptedText }], details: projected === undefined ? parameters : projected, terminate: true as const, }; }, }); roleHost.on("before_agent_start", (event) => { if (soul === undefined) throw new Error(`${spec.role} 职分未装载`); const tail = `\n\n<${spec.soulTag}_soul>\n${soul}\n`; return { systemPrompt: `${event.systemPrompt}${tail}` }; }); } const all = roleHost.getAllTools().map((tool) => tool.name); if (all.filter((name) => name === spec.tool.name).length !== 1) { throw new Error(`${spec.role} required tool collision or missing: ${spec.tool.name}`); } }, }; } export function createGleanerLeftRoleRuntime( roleHost: RoleHost, dependencies: GleanerLeftRuntimeDependencies, ) { return createFiledOfficerRuntime( roleHost, { role: "gleaner-left", tool: GLEANER_LEFT_TOOL_SPEC, acceptedText: GLEANER_LEFT_ACCEPTED_TEXT, soulTag: "gleaner-left", }, dependencies, ); } function decodeGleanerLeftBase(getFlag: (name: string) => unknown): string { const baseRevision = getFlag(GLEANER_LEFT_BASE_FLAG.name); if (typeof baseRevision !== "string" || !baseRevision.trim()) { throw new Error("Gleaner-left role requires --ak-gleaner-left-base"); } return baseRevision; } export function createInspectorRoleRuntime( roleHost: RoleHost, dependencies: InspectorRuntimeDependencies, ) { return createFiledOfficerRuntime( roleHost, { role: "inspector", tool: INSPECTOR_TOOL_SPEC, acceptedText: INSPECTOR_ACCEPTED_TEXT, soulTag: "inspector", }, dependencies, ); } /** #639: direct Gatekeeper public seat on the shared filed-officer envelope. */ export function createGatekeeperRoleRuntime( roleHost: RoleHost, dependencies: GatekeeperRuntimeDependencies, ) { return createFiledOfficerRuntime( roleHost, { role: "gatekeeper", tool: GATEKEEPER_TOOL_SPEC, acceptedText: GATEKEEPER_ACCEPTED_TEXT, soulTag: "gatekeeper", }, dependencies, ); } /** #639: direct Navigator public seat on the shared filed-officer envelope. */ export function createNavigatorRoleRuntime( roleHost: RoleHost, dependencies: NavigatorRuntimeDependencies, ) { return createFiledOfficerRuntime( roleHost, { role: "navigator", tool: NAVIGATOR_TOOL_SPEC, acceptedText: NAVIGATOR_ACCEPTED_TEXT, soulTag: "navigator", }, dependencies, ); } /** #675: public 审刑院 seat on the shared filed-officer envelope. */ export function createAuditorRoleRuntime( roleHost: RoleHost, dependencies: AuditorRuntimeDependencies, ) { const base = createFiledOfficerRuntime( roleHost, { role: "auditor", tool: AUDITOR_TOOL_SPEC, acceptedText: AUDITOR_ACCEPTED_TEXT, soulTag: "auditor", }, dependencies, ); return { async activate() { await base.activate(); // Same tools whether nested or direct (#675): dossier tool always registered. // Source run: only the shared --source-run input face (never own-run fallback). const { createAuditorDossierTool, AUDITOR_DOSSIER_TOOL_NAME } = await import("./auditor-dossier-tool.ts"); const { AK_ROLE_AUDITOR_SOURCE_RUN_ENV } = await import("./auditor-soul.ts"); const already = roleHost.getAllTools().some((tool) => tool.name === AUDITOR_DOSSIER_TOOL_NAME); if (already) return; const sourceRun = typeof process.env[AK_ROLE_AUDITOR_SOURCE_RUN_ENV] === "string" && process.env[AK_ROLE_AUDITOR_SOURCE_RUN_ENV].trim() !== "" ? process.env[AK_ROLE_AUDITOR_SOURCE_RUN_ENV].trim() : undefined; roleHost.registerTool(createAuditorDossierTool(sourceRun) as never); }, }; } /** * LLM typed court-target assertion from diarist output (ADR 0075 / #779). * null/absent = true-unbound; positive safe integer = ticket N. * Shape-only read of the typed field — not content judgment of free text. * Any other shape fails honestly — never washes into unbound. */ function readDiaristTicketAssertion( submitted: Record | undefined, ): { kind: "true-unbound" } | { kind: "ticket"; ticketNumber: number } | { kind: "invalid" } { if (submitted === undefined || !("ticketNumber" in submitted)) { return { kind: "true-unbound" }; } const raw = submitted.ticketNumber; if (raw === null) return { kind: "true-unbound" }; if (typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 1) { return { kind: "ticket", ticketNumber: raw }; } if (typeof raw === "string" && /^[1-9]\d*$/.test(raw)) { const n = Number(raw); if (Number.isSafeInteger(n) && n >= 1) { return { kind: "ticket", ticketNumber: n }; } } return { kind: "invalid" }; } /** Shared durable coordinates for role tools that summon another public role. */ function readRoleRunCoordinates(ctx: HostContext, label: string): { readonly runDirectory: string; readonly projectRoot: string; readonly home: string; readonly admitted: Record; } { const runDirectory = runDirectoryFromHostContext(ctx); if (runDirectory === undefined) throw new Error(`${label} requires AK_ROLE_RUN_DIR`); const admittedPath = join(runDirectory, "admitted-request.json"); const admitted = JSON.parse(readFileSync(admittedPath, "utf8")) as Record; if (typeof admitted.projectRoot !== "string" || admitted.projectRoot.trim() === "") { throw new Error(`${label} admitted-request missing projectRoot (${admittedPath})`); } return { runDirectory, projectRoot: admitted.projectRoot, home: homeFromRunDirectory(runDirectory), admitted, }; } /** Run coordinates + optional pre-bound ticket from durable pages (#779). */ function readDiaristRunCoordinates(ctx: HostContext): { readonly runDirectory: string; readonly projectRoot: string; readonly home: string; readonly boundTicketNumber?: number; } { const coordinates = readRoleRunCoordinates(ctx, "diarist accept"); const bound = typeof coordinates.admitted.ticketNumber === "number" && Number.isSafeInteger(coordinates.admitted.ticketNumber) && coordinates.admitted.ticketNumber >= 1 ? coordinates.admitted.ticketNumber : undefined; return { runDirectory: coordinates.runDirectory, projectRoot: coordinates.projectRoot, home: coordinates.home, ...(bound === undefined ? {} : { boundTicketNumber: bound }), }; } /** Plain-language re-ask when diarist bounds cannot be used (#901 / reask-not-explode). */ const DIARIST_BOUNDS_REASK = "边界无法使用。请重交 sessions:每卷 path + ranges,每端以原生 id 或本轮行号二选一指名。" as const; /** * #708 / #779 / #901: 起居郎 public seat on the shared filed-officer envelope. * LLM judges ticket + dialogue bounds; mechanical layer reprojects the unique * records.jsonl. Unusable bounds reask via ParentQueueReaskError. Machine * facts never come from model self-report (锚定宪法). */ export function createDiaristRoleRuntime( roleHost: RoleHost, dependencies: DiaristRuntimeDependencies, ) { return createFiledOfficerRuntime( roleHost, { role: "diarist", tool: DIARIST_TOOL_SPEC, acceptedText: DIARIST_ACCEPTED_TEXT, soulTag: "diarist", beforeAccept: async ({ parameters, ctx }) => { const submitted = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters) ? (parameters as Record) : undefined; const assertion = readDiaristTicketAssertion(submitted); const coords = readDiaristRunCoordinates(ctx); // #836 7.3: pre-bound ticket is material for the LLM, not an override. const ticketNumber = assertion.kind === "ticket" ? assertion.ticketNumber : undefined; if (ticketNumber !== undefined) { if (coords.boundTicketNumber === undefined) { await bindTicketNumberOnRunDirectory(coords.runDirectory, ticketNumber); } const sessions = projectDiaristSessions(parameters); if (sessions === undefined) { throw new ParentQueueReaskError(DIARIST_BOUNDS_REASK); } let facts; try { facts = await commitDiaristProjection({ ticketNumber, cwd: coords.projectRoot, home: coords.home, sessions, }); } catch (error) { // Bound/session input failures → reask via typed identity (not message prefix). // Unexpected infrastructure keeps its own identity (do not wash). if (error instanceof ParentQueueReaskError) throw error; if (error instanceof TicketProvenanceInputError) { throw new ParentQueueReaskError( `${DIARIST_BOUNDS_REASK}\n${error.message}`, ); } throw error; } } return parameters; }, }, dependencies, ); } /** Countersign status words the queue reads (#753). */ const COUNTERSIGN_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]); /** * Plain-language re-ask when countersignStatus is not a known queue word. * Back to countersign itself — notary is not summoned (#753). */ const COUNTERSIGN_STATUS_REASK = "countersignStatus 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。" as const; /** * #969 authorized non-pi hosts: Secretariat converged is a candidate ticket — * AK-side submission gate summons 给事中. pi keeps mid-turn summon tool only. */ const SECRETARIAT_SUBMISSION_GATE_HOSTS = new Set(["codex", "claude", "grok-build"]); /** Secretariat status words the non-pi submission gate reads (#969). */ const SECRETARIAT_QUEUE_STATUSES = new Set(["converged", "escalate"]); /** * Plain-language re-ask when non-pi secretariatStatus is not a known queue word. * Back to secretariat itself — 给事中 is not summoned (#969 / ADR 0055). */ const SECRETARIAT_STATUS_REASK = "secretariatStatus 不是 converged、escalate 之一。请重新交卷,status 写明其一。" as const; /** * #924 Secretariat on the shared filed-officer envelope + non-terminating * summon-countersign tool (auditor dossier extension pattern). * Nested countersign lifecycle stays on summonPublicRole (ADR 0018). * #924 / 第 0 条 (pi): output tool records the receipt as submitted — no status * shape/value reject or reask; legality is content-layer / downstream. * #969 (codex/claude/grok-build): converged arms 既有交卷闸 → 给事中; escalate * skips the gate; other values reask secretariat (ADR 0055). */ export function createSecretariatRoleRuntime( roleHost: RoleHost, dependencies: SecretariatRuntimeDependencies, hostActions?: import("./host-contracts.ts").HostGatekeeperActions, ) { let parentInstruction = ""; // #969: non-pi submission gate only — pi mid-turn summon path stays untouched. const beforeAccept: FiledOfficerBeforeAccept | undefined = hostActions !== undefined && roleHost.requireGatekeeperPass !== undefined ? async ({ toolCallId, parameters, signal, ctx }) => { const host = typeof ctx.host === "string" && ctx.host.trim() !== "" ? ctx.host.trim() : undefined; if (host === undefined || !SECRETARIAT_SUBMISSION_GATE_HOSTS.has(host)) { return undefined; } const record = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters) ? (parameters as Record) : undefined; const status = record !== undefined && typeof record.secretariatStatus === "string" ? record.secretariatStatus : undefined; if (status === undefined || !SECRETARIAT_QUEUE_STATUSES.has(status)) { throw new ParentQueueReaskError(SECRETARIAT_STATUS_REASK); } if (status === "escalate") { // Parent escalate → throw to caller as-is; 给事中 does not attend (#969). return undefined; } try { const pass = await roleHost.requireGatekeeperPass!({ context: ctx, subject: { kind: "secretariat_verdict" }, ...(signal === undefined ? {} : { signal }), hostActions, toolCallId, // #879: this-turn typed payload — identity-bound at submit site. submission: parameters, }); // #969: book 给事中 署 snapshot for seat settlement projection (receipt + runId). // Ledger accepted stays LLM params (#836); public terminal reads this entry. if ( pass !== undefined && pass !== null && typeof pass === "object" && "receipt" in pass ) { const { SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, } = await import("./secretariat-contracts.ts"); const runId = typeof pass.runId === "string" && pass.runId.trim() !== "" ? pass.runId : undefined; ctx.sessionManager.appendCustomEntry?.(SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, { officer: "countersign", receipt: pass.receipt, ...(runId === undefined ? {} : { runId }), }); } return undefined; } catch (error) { // #969: 给事中上呈 ends parent with officer receipt (no retry / 不擅改). if ( error instanceof GatekeeperDecisionError && error.result.status === "escalate" ) { const receipt = error.result.receipt; const receiptRecord = receipt !== null && typeof receipt === "object" && !Array.isArray(receipt) ? (receipt as Record) : undefined; const runId = typeof error.result.runId === "string" && error.result.runId.trim() !== "" ? error.result.runId : undefined; // Durable on every host: envelope persists custom entries only // (toolResult rows are memory-only on headless/ACP — #617/#959). const { SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, } = await import("./secretariat-contracts.ts"); ctx.sessionManager.appendCustomEntry?.(SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, { officer: "countersign", receipt, ...(runId === undefined ? {} : { runId }), }); const { buildAuditEscalationResult } = await import("./audit-escalation.ts"); return buildAuditEscalationResult( { status: "escalate", officer: "countersign", ...(receiptRecord !== undefined && Object.prototype.hasOwnProperty.call(receiptRecord, "decisionGate") ? { decisionGate: receiptRecord.decisionGate } : {}), }, receipt, ); } throw error; } } : undefined; const base = createFiledOfficerRuntime( roleHost, { role: "secretariat", tool: SECRETARIAT_OUTPUT_TOOL_SPEC, acceptedText: SECRETARIAT_ACCEPTED_TEXT, soulTag: "secretariat", ...(beforeAccept === undefined ? {} : { beforeAccept }), }, dependencies, ); let summonRegistered = false; return { async activate() { await base.activate(); if (!summonRegistered) { summonRegistered = true; // Capture parent prompt for default summon instruction (envelope already // owns soul inject; this handler only records the assignment text). roleHost.on("before_agent_start", (event) => { if (typeof event.prompt === "string" && event.prompt.trim() !== "") { parentInstruction = event.prompt; } }); roleHost.registerTool({ name: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME, label: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC.label, description: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC.description, promptSnippet: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC.promptSnippet, parameters: SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_SPEC.parameters as never, async execute( _toolCallId, parameters: SecretariatSummonCountersignParameters, signal, _onUpdate, ctx, ): Promise> { const fromArgs = typeof parameters?.instruction === "string" ? parameters.instruction.trim() : ""; // #924: instruction comes from tool args or parent prompt only — no // unauthored default summons prose (票号写在传召里; no fabricated fallback). const instruction = fromArgs !== "" ? fromArgs : parentInstruction.trim(); const correlationId = (() => { const runDirectory = runDirectoryFromHostContext(ctx); if (runDirectory === undefined) return undefined; return runIdFromRunDirectory(runDirectory); })(); const summon = dependencies.summonCountersign; const summoned = summon === undefined ? await (async () => { // #987 Result 7 / #747: mid-turn summon resumes by parentRunPath // (secretariat run dir), same key as summonGateOfficer countersign. // Board ticket is bind-only — never the resume lookup. const coordinates = readRoleRunCoordinates(ctx, "secretariat summons"); const { summonPublicRole } = await import("./public-role-summons.ts"); const { readBoardTicketNumber } = await import("./run-ticket-number.ts"); const parentTicket = await readBoardTicketNumber(coordinates.runDirectory); return summonPublicRole({ role: "countersign", argv: ["--project", coordinates.projectRoot, "--", instruction], cwd: coordinates.projectRoot, home: coordinates.home, parentRunPath: coordinates.runDirectory, ...(parentTicket === undefined ? {} : { boundTicketNumber: parentTicket }), ...(signal === undefined ? {} : { signal }), ...(correlationId === undefined ? {} : { correlationId }), ...(dependencies.packageRoot === undefined ? {} : { packageRoot: dependencies.packageRoot }), ...(dependencies.hostAdapters === undefined ? {} : { hostAdapters: dependencies.hostAdapters }), }); })() : await summon({ instruction, cwd: ctx.cwd, ...(signal === undefined ? {} : { signal }), ...(correlationId === undefined ? {} : { correlationId }), ...(dependencies.home === undefined ? {} : { home: dependencies.home }), ...(dependencies.packageRoot === undefined ? {} : { packageRoot: dependencies.packageRoot }), }); const details = projectSecretariatSummonResult(summoned); // #953 / #775: parent-visible text from typed details via readableGateItem // sole source — payloads + receipt (latest) already carry 传话 facts. return { content: [ { type: "text" as const, text: readableGateItem(details), }, ], details, }; }, }); } const packageRequired = [ SECRETARIAT_OUTPUT_TOOL_NAME, SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME, ] as const; const all = roleHost.getAllTools().map((tool) => tool.name); for (const name of packageRequired) { if (all.filter((item) => item === name).length !== 1) { throw new Error(`secretariat required tool collision or missing: ${name}`); } } // Host-neutral minimum package surface (#924 G6): declare package tools via // setActiveTools without hardcoding host builtin names. Preserve any host // surface already visible on getAllTools/getActiveTools so body rewrite // (public gh path) stays reachable on hosts that expose it. const priorActive = roleHost.getActiveTools(); const hostSurface = priorActive.length > 0 ? priorActive : all; const nextActive = [ ...new Set([...hostSurface, ...packageRequired]), ]; roleHost.setActiveTools(nextActive); const active = roleHost.getActiveTools(); for (const name of packageRequired) { if (!active.includes(name)) { throw new Error(`secretariat failed to activate required tool ${name}`); } } }, }; } export function createCountersignRoleRuntime( roleHost: RoleHost, dependencies: CountersignRuntimeDependencies, hostActions?: import("./host-contracts.ts").HostGatekeeperActions, ) { // Notary inner gate difference only — lifecycle stays on the shared envelope. // Pointer-only summons: officer self-fetches from run dossier (#632 / ADR 0079). // #753 queue: read countersignStatus only — escalate skips gate (thrown to caller); // unreadable status returns to countersign; else notary inner gate. const beforeAccept: FiledOfficerBeforeAccept | undefined = hostActions !== undefined && roleHost.requireGatekeeperPass !== undefined ? async ({ toolCallId, parameters, signal, ctx }) => { const record = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters) ? (parameters as Record) : undefined; const status = record !== undefined && typeof record.countersignStatus === "string" ? record.countersignStatus : undefined; if (status === undefined || !COUNTERSIGN_QUEUE_STATUSES.has(status)) { // Parent status unreadable → back to countersign itself; do not summon notary, // do not forge an officer bounce face (#753). throw new ParentQueueReaskError(COUNTERSIGN_STATUS_REASK); } if (status === "escalate") { // Parent escalate → throw to caller as-is; notary does not attend (#753). return undefined; } await roleHost.requireGatekeeperPass!({ context: ctx, subject: { kind: "countersign_verdict" }, ...(signal === undefined ? {} : { signal }), hostActions, toolCallId, // #879: this-turn typed payload — identity-bound at submit site. submission: parameters, }); } : undefined; return createFiledOfficerRuntime( roleHost, { role: "countersign", tool: COUNTERSIGN_TOOL_SPEC, acceptedText: COUNTERSIGN_ACCEPTED_TEXT, soulTag: "countersign", ...(beforeAccept === undefined ? {} : { beforeAccept }), }, dependencies, ); } /** * #959: last assistant text parts as navigator prose exit. * Tool-call-only messages yield undefined — those already ride the tool path. */ function lastAssistantProse( messages: readonly { role: string; content?: readonly { type: string; text?: string }[] }[], ): string | undefined { for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (message?.role !== "assistant" || !Array.isArray(message.content)) continue; const texts: string[] = []; for (const part of message.content) { if (part.type === "text" && typeof part.text === "string" && part.text.length > 0) { texts.push(part.text); } } if (texts.length === 0) continue; return texts.join(""); } return undefined; } export function createRoleRuntimeExtension( dependencies: RoleRuntimeDependencies, ): (envelopeHost: RoleEnvelopeHost) => void { return (envelopeHost) => { let projectClosedSubmission: (closed: import("./submission-ledger.ts").ClosedSubmission, context: HostContext) => Promise = async () => { throw new Error("角色终局投射接缝尚未初始化"); }; const roleHost = createSubmissionLedgerHost( envelopeHost.host, new Map(PACKAGED_ROLE_REGISTRY.map(({ role, outputTool }) => [outputTool, role])), failInfrastructure, async (closed, context) => projectClosedSubmission(closed, context), ); roleHost.registerFlag(ROLE_FLAG.name, ROLE_FLAG.definition); // Reviewer transport flags: shared envelope owns registration (ADR 0018). for (const flag of REVIEWER_TRANSPORT_FLAGS) { roleHost.registerFlag(flag.name, flag.definition); } for (const flag of NOTARY_TRANSPORT_FLAGS) { roleHost.registerFlag(flag.name, flag.definition); } roleHost.registerFlag( INSPECTOR_SOURCE_RUN_FLAG.name, INSPECTOR_SOURCE_RUN_FLAG.definition, ); for (const flag of GLEANER_LEFT_TRANSPORT_FLAGS) { roleHost.registerFlag(flag.name, flag.definition); } // Collector transport flags: shared envelope owns registration (ADR 0018 / #676 E). for (const flag of COLLECTOR_TRANSPORT_FLAGS) { roleHost.registerFlag(flag.name, flag.definition); } // Station-child identity (#840): omit navigator attendance. One flag. roleHost.registerFlag(STATION_CHILD_FLAG.name, STATION_CHILD_FLAG.definition); // Register model only. Pi never sets ak-engine — resolveEngineName must // fall through to child-process env. An empty default would block that. roleHost.registerFlag(ENGINE_MODEL_FLAG_NAME, { description: "本次劳务引擎模型", type: "string", default: "", }); let admitted = false; let selectedRole: PackagedRole | undefined; let roleReferenceMaterials = ""; /** Live Reviewer parent activation for envelope agent_start prompt assembly. */ let activeReviewerParent: ReviewerActivation | undefined; /** Envelope-owned Reviewer Skill expansion state (ADR 0018 — not a role-module facade). */ let reviewerOriginalRequest: string | undefined; let reviewerExpansionCaptured = false; let navigatorAttendance: NavigatorAttendanceDependency | undefined; // #351: session-lifecycle owner for periodic OAuth refresh (orthogonal to role admission). let pendingNavigatorPresentation: { event: import("./navigator-attendance.ts").NavigatorEvent; report: import("./navigator-attendance.ts").NavigatorReport } | undefined; let pendingNavigatorSettlement: Promise | undefined; let navigatorWorkContext: NavigatorWorkContext | undefined; let navigatorSessionParent: string | undefined; let navigatorCwd: string | undefined; /** toolCallId → fact+evidence; one-shot projected onto durable tool_result (#475). */ const pendingInfrastructureFailures = new Map(); // Envelope-owned execute→tool_result bridge for submission non-pass (ADR 0018 / #525). const pendingSubmissionNonPassByToolCallId = new Map(); let engineDetourRegistered = false; // #288 primary-session thin adapter. The policy is the sole budget owner; // terminating-tool rejections and mechanical delivery requests share two turns. let receiptDelivery = createReceiptDeliveryPolicy(); let noReceiptRecorded = false; // Public-run fetch observation (in-process-session statusAwareFetch face). let priorFetch: typeof globalThis.fetch | undefined; let fetchWrapped = false; /** Envelope-owned: abort/teardown attendance without re-blocking the parent court (#959). */ const disposeNavigatorAttendanceNonBlocking = ( attendance: NavigatorAttendanceDependency | undefined, ): void => { if (attendance === undefined) return; const recordDisposeFailure = (error: unknown): void => { const diagnostic = error instanceof Error ? error.message : String(error); try { sitianReport({ level: "event", kind: "navigator-dispose-failure", cwd: navigatorCwd, sessionParent: navigatorSessionParent, payload: { diagnostic }, source: "role-runtime", }); } catch (recordError) { try { envelopeHost.appendEntry?.("ak-navigator-dispose-failure", { diagnostic, recordFailure: recordError instanceof Error ? recordError.message : String(recordError), }); } catch { // Failure already diagnosed; recording must not create unhandled rejection (#959). } } }; // Evaluate dispose inside try: Promise.resolve(dispose()) throws sync before .then attaches. let pending: void | Promise; try { pending = attendance.dispose(); } catch (error) { recordDisposeFailure(error); return; } void Promise.resolve(pending).then(undefined, recordDisposeFailure); }; const settleNavigatorProjection = async (settlement: NavigatorSettlement | undefined) => { const attendance = navigatorAttendance; if (settlement === undefined || attendance === undefined) return; const workContext = navigatorWorkContext; const pending = (async () => { // ADR 0052 / #959: every settlement that starts a post-role feed host round // (accepted, human_decision, role_infrastructure_failure) shares one grace // and the same honest unavailable projection — no unbounded parallel branch. const settlePromise = attendance.settle(settlement); // Attach catch immediately so a late rejection after grace timeout cannot // surface as unhandledRejection / stale-ctx after session dispose (#675). void settlePromise.catch(() => undefined); const raced = await raceNavigatorGrace(settlePromise, NAVIGATOR_POST_ROLE_GRACE_MS); if (raced.status !== "timeout") return; if (pendingNavigatorPresentation === undefined) { const routePlaybookReadFailure = attendance.knownRoutePlaybookReadFailure?.(); const report: NavigatorReport = { disposition: "unavailable", unavailableReason: "Navigator exceeded post-role delivery grace", unavailableSource: "unknown", unavailableCause: "unknown", ...(routePlaybookReadFailure === undefined ? {} : { routePlaybookReadFailure }), }; const event: NavigatorEvent = { version: 1, disposition: "unavailable", invocationId: "post-role-grace-timeout", role: settlement.role, phase: settlement.phase, subjectKey: workContext?.subjectKey ?? "", unavailableReason: "Navigator exceeded post-role delivery grace", unavailableSource: "unknown", unavailableCause: "unknown", ...(routePlaybookReadFailure === undefined ? {} : { routePlaybookReadFailure }), }; pendingNavigatorPresentation = { event, report }; } // 过时不候: do not await sidecar teardown — parent court must close. disposeNavigatorAttendanceNonBlocking(attendance); })(); pendingNavigatorSettlement = pending; await pending; }; projectClosedSubmission = async (closed, context) => projectClosedSubmissionLifecycle( closed, context, navigatorPhase(roleHost, closed.role), () => receiptDelivery.recordAccepted(), settleNavigatorProjection, ); roleHost.on("input", (event) => { const text = event.text; const role = roleHost.getFlag(ROLE_FLAG.name); if (role !== undefined && !admitted) return { action: "handled" as const }; // Reviewer: recover original request; Pi argv may already carry native form. if ( role === "reviewer" && admitted && activeReviewerParent !== undefined && reviewerOriginalRequest === undefined ) { reviewerOriginalRequest = roleHost.capabilities?.skillOriginalRequest?.( activeReviewerParent.skillBinding.name, text, ) ?? text; } return { action: "continue" as const }; }); // Reference law/guides use the existing typed reading-material channel; // they are not rewritten into operator dialogue or the identity Soul. roleHost.on("before_agent_start", () => roleReferenceMaterials === "" ? undefined : { readingMaterial: { kind: "role-reference-materials", content: roleReferenceMaterials } }); roleHost.on("before_agent_start", async (event, ctx) => { const role = roleHost.getFlag(ROLE_FLAG.name); const prompt = event.prompt; if (role === undefined) return; if (!admitted || selectedRole !== role) { failInfrastructure(new ActivationBarrierError(role), ctx); } // Notary session bound: envelope-owned lifecycle write (ADR 0018 / #582). // Ticket flag register/read + session entry live here; role projects admitted bound only. if (role === "notary") { const bound = projectNotaryBoundFromFlags((name) => roleHost.getFlag(name)); if (bound !== undefined) { ctx.sessionManager.appendCustomEntry?.(NOTARY_SESSION_BOUND_ENTRY, bound); } } if (navigatorAttendance !== undefined && navigatorWorkContext !== undefined && navigatorWorkContext.contextError === undefined) { // Flagged roles already have a concrete packet/task/case/review input. // A bare Judge (and other bare packaged entrypoint) gets its concrete // user task at this seam; do not copy the assembled system prompt. // Replacement is keyed by typed subject provenance, never prose prefixes. // Soft session_start placeholders (no materials yet) recover here; hard // contextError from true loader failures stays poisoned and honest. if (navigatorWorkContext.subjectProvenance === "placeholder") { // Navigator's own input bootstrap, not a rewrite of another role's // output payload (#836 targets the latter): the human's own raw // prompt bytes, copied verbatim into Navigator's work context when // nothing else has supplied a subject yet. const subject = prompt.trim(); if (subject !== "") { const root = subjectPath(ctx.sessionManager.getSessionDir(), ctx.cwd); const subjectProvenance = "user_prompt" satisfies NavigatorSubjectProvenance; const priorAuthority = navigatorWorkContext.authority; const authority = typeof priorAuthority === "string" && priorAuthority.trim() !== "" ? priorAuthority : subject; navigatorWorkContext = { subjectKey: navigatorSubjectKey(root, subject, subjectProvenance), subject, authority, subjectProvenance, }; await navigatorAttendance.setWorkContext(navigatorWorkContext); } } } navigatorAttendance?.prepare(); // Envelope-owned Reviewer expansion capture + parent prompt assembly (no role-module callback). if (role === "reviewer" && activeReviewerParent !== undefined) { if (!reviewerExpansionCaptured) { if (reviewerOriginalRequest !== undefined) { activeReviewerParent.skillBinding.captureExpansion( roleHost.capabilities?.skillExpansion(prompt), reviewerOriginalRequest, ); } reviewerExpansionCaptured = true; } return { systemPrompt: assembleReviewerParentSystemPrompt({ baseSystemPrompt: event.systemPrompt, soul: activeReviewerParent.soul, }), }; } // #676 E / J1: collector materials + drift gates share this envelope hook (no parallel register). if (role === "collector" && activeCollector !== undefined) { if (!collectorFirstDispatchDone) { collectorFirstDispatchDone = true; activeCollector.ledger.recordActivation(activeCollector.clock); } return { systemPrompt: collectorBusiness.assembleMaterials(activeCollector, event.systemPrompt), }; } }); // #879: engine coordinates ride readingMaterial only when station-child // officer dialogue replaced the ordinary transport prompt (no double fold). roleHost.on("before_agent_start", () => { const role = selectedRole ?? roleHost.getFlag(ROLE_FLAG.name); if (typeof role !== "string" || !isOfficerReviewSeat(role)) return; if (roleHost.getFlag(STATION_CHILD_FLAG.name) !== true) return; const engine = resolveEngineName((name) => roleHost.getFlag(name)); if (engine === undefined || dependencies.packageRoot === undefined) return; const engineModel = resolveEngineModel((name) => roleHost.getFlag(name)); const material = engineSessionMaterialFromOptions({ engine, ...(engineModel === undefined ? {} : { engineModel }), packageRoot: dependencies.packageRoot, }); if (material === undefined) return; return { readingMaterial: { kind: "engine-session-material" as const, name: material.name, ...(material.model === undefined ? {} : { model: material.model }), ...(material.materialPath === undefined ? {} : { materialPath: material.materialPath }), }, }; }); roleHost.on("before_agent_start", () => { const path = roleHost.getFlag(INSPECTOR_SOURCE_RUN_FLAG.name); if (typeof path !== "string" || path.trim() === "") return; return { readingMaterial: { kind: "inspector-parent-binding" as const, sourceRunPath: path, }, }; }); // #879: single shared owner of station-child 0081 case-dossier → readingMaterial fold. // post-admission freezes under run/attachments/case-dossier/; this handler alone // projects it onto the existing agent-start materials face (Pi + envelope collect). // Role modules must not re-read the freeze (ADR 0018 lifecycle; no duplicate fold). roleHost.on("before_agent_start", async (_event, ctx) => { const runDir = runDirectoryFromHostContext(ctx); if (runDir === undefined) return; const { loadCaseDossierReadingMaterial } = await import( "./public-cli/case-dossier-delivery.ts" ); const caseDossier = await loadCaseDossierReadingMaterial(runDir); if (caseDossier === undefined) return; return { readingMaterial: caseDossier }; }); roleHost.on("tool_result", async (event) => { const role = selectedRole; if (role === undefined) return; // #676 E / J1: collector operational bookkeeping on the shared tool_result seam. if (role === "collector" && activeCollector !== undefined) { collectorBusiness.onToolResult(activeCollector, event); } const pendingInfra = pendingInfrastructureFailures.get(event.toolCallId); const isRoleInfrastructureFailure = pendingInfra !== undefined; if (pendingInfra !== undefined) pendingInfrastructureFailures.delete(event.toolCallId); // One-shot project fact + typed evidence so live settlement and durable session agree. const infrastructureDetails = pendingInfra?.details; const classified = infrastructureDetails === undefined ? event : { ...event, details: infrastructureDetails }; const isOutputTool = event.toolName === navigatorOutputTool(role); const outputClassification = isOutputTool ? classifyPackagedRoleTerminalResult(classified) : undefined; if (isRoleInfrastructureFailure || outputClassification?.kind === "infrastructure") { receiptDelivery.stopForInfrastructure(); } else if (isOutputTool && outputClassification?.kind === "nonterminal" && event.isError) { const reason = (event.content ?? []) .map((part) => part.type === "text" && "text" in part ? part.text : "") .join("") .trim(); receiptDelivery.recordRejected(reason); } // Accepted/human terminal projection belongs exclusively to typed ledger // closure. tool_result retains only infrastructure settlement. const settlement = isRoleInfrastructureFailure || outputClassification?.kind === "infrastructure" ? publicNavigatorSettlement(role, navigatorPhase(roleHost, role), classified) : undefined; await settleNavigatorProjection(settlement); // Persist typed infrastructure-failure fact onto the role session toolResult so // exact-session restart shares the same durable completion classification. if (infrastructureDetails !== undefined) { return { isError: true }; } // Submission non-pass: throw kept message text for the model; project the // envelope-bound structured result onto session details at this tool_result seam. const submissionNonPass = pendingSubmissionNonPassByToolCallId.get(event.toolCallId); if (submissionNonPass !== undefined) { pendingSubmissionNonPassByToolCallId.delete(event.toolCallId); return { details: submissionNonPass, isError: true }; } }); // Queue receipt delivery before `agent_settled`: that event means Pi has // already decided no queued continuation will run, so a triggerTurn there is // too late for print/json sessions. `agent_end` is the last production seam // whose queued next turn is consumed before settlement. roleHost.on("agent_end", async (event, ctx) => { const lastMessage = event.messages.at(-1); if (lastMessage?.role === "assistant" && (lastMessage.stopReason === "error" || lastMessage.stopReason === "aborted")) { // Abort after an already-recorded receipt must not un-accept or催交. if (receiptDelivery.nextAction() !== "accepted") { receiptDelivery.stopForInfrastructure(); } return; } const role = selectedRole ?? roleHost.getFlag(ROLE_FLAG.name); // #959: navigator prose exit — final assistant text is the receipt. // No typed-tool 催交; no JSON required. Tool path still wins when already accepted. if (role === "navigator" && receiptDelivery.nextAction() !== "accepted") { const prose = lastAssistantProse(event.messages); if (prose !== undefined && prose.trim() !== "") { const accepted = { prose }; await sealAcceptedSubmission({ context: ctx, role: "navigator", accepted, toolCallId: `navigator-prose-exit:${randomUUID()}`, }); await projectClosedSubmission( { role: "navigator", kind: "accepted", accepted }, ctx, ); return; } // Attended with neither tool nor prose → honest no_receipt (not typed 催交). // Exhaust delivery budget without sending the typed prompt so facts() stays lawful. if (!noReceiptRecorded) { const runPointer = runDirectoryFromHostContext(ctx); if (runPointer !== undefined) { noReceiptRecorded = true; while (receiptDelivery.nextAction() === "request-delivery") { receiptDelivery.recordDeliveryRequest(); } const facts = receiptDelivery.facts({ runPointer, attemptPointer: `current:${runPointer}` }); envelopeHost.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts); try { sitianReport({ level: "event", kind: "no-receipt-lifecycle", cwd: ctx.cwd, sessionParent: ctx.sessionManager.getSessionFile(), payload: facts, source: "role-runtime", }); } catch {} } } return; } if (receiptDelivery.nextAction() === "request-delivery") { receiptDelivery.recordDeliveryRequest(); // Keep the package-owned continuation off the public input lifecycle: // receipt delivery must not be mistaken for later caller input. envelopeHost.appendEntry("ak-receipt-delivery-request"); try { sitianReport({ level: "event", kind: "receipt-delivery", cwd: ctx.cwd, sessionParent: ctx.sessionManager.getSessionFile(), payload: { type: "ak-receipt-delivery-request" }, source: "role-runtime", }); } catch {} envelopeHost.sendMessage({ customType: "ak-receipt-delivery-prompt", content: RECEIPT_DELIVERY_PROMPT, display: false, }, { triggerTurn: true, deliverAs: "followUp" }); } else if (receiptDelivery.nextAction() === "no-receipt" && !noReceiptRecorded) { const runPointer = runDirectoryFromHostContext(ctx); if (runPointer !== undefined) { noReceiptRecorded = true; const facts = receiptDelivery.facts({ runPointer, attemptPointer: `current:${runPointer}` }); envelopeHost.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts); try { sitianReport({ level: "event", kind: "no-receipt-lifecycle", cwd: ctx.cwd, sessionParent: ctx.sessionManager.getSessionFile(), payload: facts, source: "role-runtime", }); } catch {} } } }); roleHost.on("agent_settled", async () => { if (pendingNavigatorSettlement !== undefined) { await pendingNavigatorSettlement; } // Do not auto-drain Navigator on ordinary mid-turn agent_settled. // Multi-turn roles (#162 coder) fire agent_settled after every tool turn; // discarding a healthy/in-flight prepare there forces a cold prepare on the // final accepted terminal and races the post-role grace. Keep preparation // until an accepted/human/infrastructure tool_result settlement (or dispose). // Provider death without any role tool_result leaves preparation held; the // next explicit settlement or session end owns it — not mid-turn churn. pendingNavigatorSettlement = undefined; const presentation = pendingNavigatorPresentation; pendingNavigatorPresentation = undefined; if (presentation === undefined) return; await envelopeHost.sendMessage({ customType: NAVIGATOR_EVENT_TYPE, content: formatNavigatorReport(presentation.report), display: true, details: presentation.event, }, { triggerTurn: false }); }); roleHost.on("session_shutdown", async () => { // #351: stop OAuth keepalive first so shutdown yields zero further ticks. envelopeHost.stopKeepalive(); if (fetchWrapped && priorFetch !== undefined) { globalThis.fetch = priorFetch; priorFetch = undefined; fetchWrapped = false; } // #676 J4: collector fatal latch must surface nonzero exit on shutdown (envelope-owned). if ( selectedRole === "collector" && activeCollector !== undefined && activeCollector.ledger.fatal ) { if (process.exitCode === undefined || process.exitCode === 0) { process.exitCode = 1; } } // Flush any still-pending affirmative attendance before teardown. // Grace-timeout paths normally emit on agent_settled; abort can skip that hook. const presentation = pendingNavigatorPresentation; pendingNavigatorPresentation = undefined; if (presentation !== undefined) { try { await envelopeHost.sendMessage({ customType: NAVIGATOR_EVENT_TYPE, content: formatNavigatorReport(presentation.report), display: true, details: presentation.event, }, { triggerTurn: false }); } catch { // Teardown must not mask the original role failure cause. } } // Same non-blocking dispose as post-role grace: awaiting here re-blocked the // parent court for 43–270s after unavailable was already projected (#959 reopen). const attendanceToDispose = navigatorAttendance; navigatorAttendance = undefined; pendingNavigatorSettlement = undefined; disposeNavigatorAttendanceNonBlocking(attendanceToDispose); pendingInfrastructureFailures.clear(); pendingSubmissionNonPassByToolCallId.clear(); observationFace.reset(); }); const hostActions = { failInfrastructure(error: unknown, ctx: HostContext, toolCallId?: string): never { if (toolCallId !== undefined) { pendingInfrastructureFailures.set(toolCallId, buildPendingInfrastructureFailure(error)); } failInfrastructure(error, ctx); }, bindSubmissionNonPass(toolCallId: string, result: SubmissionNonPassResult): void { pendingSubmissionNonPassByToolCallId.set(toolCallId, result); }, }; const judge = createJudgeRoleRuntime( roleHost, { loadSoul: dependencies.loadJudgeSoul, }, hostActions, ); const fixer = createFixerRoleRuntime( roleHost, { async loadSoul() { if (dependencies.loadFixerSoul === undefined) { throw new Error("fixer soul loader is not configured"); } return dependencies.loadFixerSoul(); }, async loadPacket(path) { if (dependencies.loadFixPacket === undefined) { throw new Error("Fixer packet loader is not configured"); } return dependencies.loadFixPacket(path); }, }, hostActions, ); const coder = createCoderRoleRuntime( roleHost, { async loadSoul() { if (dependencies.loadCoderSoul === undefined) { throw new Error("coder soul loader is not configured"); } return dependencies.loadCoderSoul(); }, async loadTask(path) { if (dependencies.loadCoderTask === undefined) { throw new Error("Coder task loader is not configured"); } return dependencies.loadCoderTask(path); }, ...(dependencies.loadCanonicalSkillBinding === undefined ? {} : { loadCanonicalSkillBinding: (name: "tdd") => dependencies.loadCanonicalSkillBinding!(name), }), }, hostActions, ); const reviewer = createReviewerRoleRuntime( roleHost, { async loadSoul() { if (dependencies.loadReviewerSoul === undefined) { throw new Error("reviewer soul loader is not configured"); } return dependencies.loadReviewerSoul(); }, async loadCanonicalSkillBinding(name) { if (dependencies.loadCanonicalSkillBinding === undefined) { throw new Error("Reviewer runtime dependencies are not configured"); } return dependencies.loadCanonicalSkillBinding(name); }, }, hostActions, ); const doctor = createDoctorRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadDoctorSoul) throw new Error("Doctor runtime dependencies are not configured"); return dependencies.loadDoctorSoul(); }, async loadCase(path) { if (!dependencies.loadDoctorCase) throw new Error("Doctor runtime dependencies are not configured"); return dependencies.loadDoctorCase(path); }, async auditCompliance(options) { if (!dependencies.auditDoctorCompliance) throw new Error("Doctor runtime dependencies are not configured"); return dependencies.auditDoctorCompliance(options); }, }, hostActions); const notary = createNotaryRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadNotarySoul) throw new Error("Notary runtime dependencies are not configured"); return dependencies.loadNotarySoul(); }, async loadSourceRunLocator(path) { if (!dependencies.loadNotarySourceRun) throw new Error("Notary runtime dependencies are not configured"); return dependencies.loadNotarySourceRun(path); }, }, hostActions); const countersign = createCountersignRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadCountersignSoul) throw new Error("Countersign runtime dependencies are not configured"); return dependencies.loadCountersignSoul(); }, }, hostActions); const gleanerLeftRuntime = createGleanerLeftRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadGleanerLeftSoul) throw new Error("Gleaner-left runtime dependencies are not configured"); return dependencies.loadGleanerLeftSoul(); }, }); const gleanerLeft = { async activate() { decodeGleanerLeftBase((name) => envelopeHost.host.getFlag(name)); return gleanerLeftRuntime.activate(); }, }; const inspector = createInspectorRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadInspectorSoul) throw new Error("Inspector runtime dependencies are not configured"); return dependencies.loadInspectorSoul(); }, }); const gatekeeper = createGatekeeperRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadGatekeeperSoul) throw new Error("Gatekeeper runtime dependencies are not configured"); return dependencies.loadGatekeeperSoul(); }, }); const navigator = createNavigatorRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadNavigatorSoul) throw new Error("Navigator runtime dependencies are not configured"); return dependencies.loadNavigatorSoul(); }, }); const auditor = createAuditorRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadAuditorSoul) throw new Error("Auditor runtime dependencies are not configured"); return dependencies.loadAuditorSoul(); }, }); const diarist = createDiaristRoleRuntime( roleHost, { async loadSoul() { if (!dependencies.loadDiaristSoul) throw new Error("Diarist runtime dependencies are not configured"); return dependencies.loadDiaristSoul(); }, }, ); const secretariat = createSecretariatRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadSecretariatSoul) throw new Error("Secretariat runtime dependencies are not configured"); return dependencies.loadSecretariatSoul(); }, ...(dependencies.packageRoot === undefined ? {} : { packageRoot: dependencies.packageRoot }), ...(dependencies.hostAdapters === undefined ? {} : { hostAdapters: dependencies.hostAdapters }), }, hostActions); const merger = createMergerRoleRuntime(roleHost, { async loadSoul() { if (!dependencies.loadMergerSoul) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerSoul(); }, async loadInput(path) { if (!dependencies.loadMergerInput) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerInput(path); }, }); // #676 E: shared envelope owns collector lifecycle (mode/fork, tool surface, // event gates). Role module supplies business activate/tools/materials only. let activeCollector: CollectorActivation | undefined; let collectorFirstDispatchDone = false; const collectorBusiness = createCollectorRoleRuntime( roleHost, { async loadSoul() { if (dependencies.loadCollectorSoul === undefined) { throw new Error("collector soul loader is not configured"); } return dependencies.loadCollectorSoul(); }, createTransport() { if (dependencies.createCollectorTransport === undefined) { throw new Error("Collector GitHub transport is not configured"); } return dependencies.createCollectorTransport(); }, createLedger(config, collectorClock, context) { const append = context.sessionManager?.appendCustomEntry; return createCollectorLedger(config, { clock: collectorClock, ...(append === undefined ? {} : { journal: { append(customType, data) { context.sessionManager?.appendCustomEntry?.(customType, data); }, }, }), dossierEntries: context.sessionManager?.getEntries?.() ?? [], }); }, ...(dependencies.createCollectorClock === undefined ? {} : { createClock: dependencies.createCollectorClock }), ...(dependencies.loadCollectorHandbookSeed === undefined ? {} : { loadHandbookSeed: dependencies.loadCollectorHandbookSeed }), }, hostActions, ); // #676 J1: business tools + tool_call gate register only behind admission/activation. // before_agent_start / tool_result collector branches live on the shared envelope hooks above. // // #676 J4 dispositions for guards removed from the role-private collector module: // - skills/contextFiles/appendSystemPrompt fail-closed → shared before_agent_start (above). // - ambient skill/prompt/template commands → activate (below). // - session_shutdown fatal exitCode → shared session_shutdown (above). // - subsequent-input latchFatal + fixed-kickoff rewrite → intentionally not migrated: // #676 materials-first multi-turn abolished the single-shot fixed kickoff; multi-turn // observe/request/wait requires later inputs. Judge r1: fixed-kickoff equality delete is authorized. // - tool sourceInfo path override check → intentionally not migrated: depended on // packageExtensionPath deleted under ADR 0018 / #676 E envelope ownership; uniqueness // + setActiveTools inventory checks remain on activate. let collectorToolCallRegistered = false; const collector = { async activate(context: HostContext, event: { reason: string }) { activeCollector = undefined; collectorFirstDispatchDone = false; // Envelope-owned mode / fork-reload gates (not role-private lifecycle). if (context.mode !== "print" && context.mode !== "json") { throw new Error( `Collector supports only print or json mode (got ${context.mode})`, ); } if (event.reason === "fork" || event.reason === "reload") { throw new Error( `Collector does not support session_start reason ${event.reason}`, ); } // Business tools behind admission barrier (inert-without-role invariant). // First activation: fail closed if a required name is already occupied. // Later activations reuse the once-registered tools (registerBusinessTools is idempotent). const preExisting = roleHost.getAllTools(); const alreadyRegistered = COLLECTOR_REQUIRED_TOOLS.every((required) => preExisting.some((tool) => tool.name === required), ); if (!alreadyRegistered) { for (const required of COLLECTOR_REQUIRED_TOOLS) { const prior = preExisting.filter((tool) => tool.name === required); if (prior.length > 0) { throw new Error(`Collector required tool name collision: ${required}`); } } } collectorBusiness.registerBusinessTools(() => activeCollector); const allTools = roleHost.getAllTools(); for (const required of COLLECTOR_REQUIRED_TOOLS) { const matches = allTools.filter((tool) => tool.name === required); if (matches.length === 0) { throw new Error(`Collector required tool missing: ${required}`); } if (matches.length > 1) { throw new Error(`Collector required tool name collision: ${required}`); } } roleHost.setActiveTools([...COLLECTOR_REQUIRED_TOOLS]); const active = new Set(roleHost.getActiveTools()); for (const required of COLLECTOR_REQUIRED_TOOLS) { if (!active.has(required)) { throw new Error(`Collector failed to activate required tool ${required}`); } } for (const name of active) { if (!(COLLECTOR_REQUIRED_TOOLS as readonly string[]).includes(name)) { throw new Error(`Collector active tool surface includes unexpected ${name}`); } } // Seat-scoped tool_call gate — registered only after collector admission (no install-time tool_call). if (!collectorToolCallRegistered) { collectorToolCallRegistered = true; roleHost.on("tool_call", (toolEvent) => { if (activeCollector === undefined || selectedRole !== "collector") return; if (!(COLLECTOR_REQUIRED_TOOLS as readonly string[]).includes(toolEvent.toolName)) { return { block: true, reason: `通进司禁用工具 ${toolEvent.toolName}`, }; } return collectorBusiness.onToolCall(activeCollector, toolEvent); }); } const activation = await collectorBusiness.activate(context); if (activation.ledger.activationRecorded) { collectorFirstDispatchDone = true; } activeCollector = activation; }, }; const clock = dependencies.activationClock ?? (() => new Date().toISOString()); const writeTrace = dependencies.activationTraceWriter ?? writeActivationTraceRecord; const observationFace = createToolExecutionObservationFace({ role: () => selectedRole, admitted: () => admitted, clock: dependencies.toolExecutionObservationClock ?? clock, monoNow: dependencies.toolExecutionObservationMonoNow ?? systemToolExecutionObservationMonoNow, write: dependencies.toolExecutionObservationWriter ?? writeToolExecutionObservationRecord, }); // ExtensionRunner.emit catches ordinary handler throws, emits extension error, and continues. // Observation plane failures must still hit the shared infrastructure termination path // (abort + nonzero print/json exit) with the original cause before that swallow. const observe = async ( run: () => void | Promise, ctx: HostContext, ): Promise => { try { await run(); } catch (error) { failInfrastructure(error, ctx); } }; roleHost.on("tool_execution_start", async (event, ctx) => { await observe(() => observationFace.onStart(event), ctx); }); roleHost.on("tool_execution_update", async (event, ctx) => { await observe(() => observationFace.onUpdate(event), ctx); }); roleHost.on("tool_execution_end", async (event, ctx) => { await observe(() => observationFace.onEnd(event), ctx); }); // Public Role run: record typed non-success HTTP for error evidence + v1 resume. // Same observation owner as in-process-session statusAwareFetch → typed-provider-http // sidecar (settlement already merges observation.httpStatus into knownFailure). // after_provider_response covers the success-path onResponse face; fetch wrap covers // non-2xx Responses where openai-completions throws before onResponse (#675). const recordHttpObservation = async ( status: number, provider: string, ctx: HostContext, ): Promise => { const runDir = runDirectoryFromHostContext(ctx); if (runDir === undefined) return; try { await recordTypedProviderHttpStatus(runDir, { httpStatus: status, provider }); } catch (error) { if ( status >= 200 && status < 300 && error instanceof Error && "code" in error && (error as { code?: unknown }).code === "ENOENT" ) { return; } failInfrastructure(error, ctx); } }; roleHost.on("after_provider_response", async (event, ctx) => { const status = event.status; if (typeof status !== "number") return; const fromCtx = ctx.model?.provider; const provider = typeof fromCtx === "string" && fromCtx.trim() !== "" ? fromCtx : "unknown"; await recordHttpObservation(status, provider, ctx); }); roleHost.on("session_start", async (event, ctx) => { // Scope fetch observation to this public run (in-process-session statusAwareFetch face). if (!fetchWrapped && typeof globalThis.fetch === "function") { priorFetch = globalThis.fetch.bind(globalThis); const underlying = priorFetch; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const response = await underlying(input, init); const runDir = runDirectoryFromHostContext(ctx); if ( runDir !== undefined && typeof response?.status === "number" && (response.status < 200 || response.status >= 300) ) { const provider = typeof ctx.model?.provider === "string" && ctx.model.provider.trim() !== "" ? ctx.model.provider : "unknown"; try { await recordTypedProviderHttpStatus(runDir, { httpStatus: response.status, provider, }); } catch { // Observation must not break the provider stream (same as in-process-session). } } return response; }) as typeof globalThis.fetch; fetchWrapped = true; } admitted = false; selectedRole = undefined; roleReferenceMaterials = ""; activeReviewerParent = undefined; reviewerOriginalRequest = undefined; reviewerExpansionCaptured = false; receiptDelivery = createReceiptDeliveryPolicy(); noReceiptRecorded = false; observationFace.reset(); pendingNavigatorPresentation = undefined; pendingNavigatorSettlement = undefined; pendingInfrastructureFailures.clear(); pendingSubmissionNonPassByToolCallId.clear(); navigatorWorkContext = undefined; // #351: OAuth keepalive is orthogonal to --ak-role; start before role early-return // so role-less sessions (and reload after shutdown stop) still keep tokens alive. envelopeHost.startKeepalive(ctx); const rawRole = roleHost.getFlag(ROLE_FLAG.name); if (rawRole === undefined) return; const entry = PACKAGED_ROLE_REGISTRY.find(({ role }) => role === rawRole); if (entry === undefined) { failInfrastructure(new Error(`Unsupported workflow role: ${String(rawRole)}`), ctx); } selectedRole = entry.role; await navigatorAttendance?.dispose(); navigatorAttendance = undefined; const runtime: ActivationRuntime = { event, context: ctx, judge, fixer, coder, reviewer, decodeReviewerAdmitted() { return decodeReviewerAdmittedInputs((name) => roleHost.getFlag(name)); }, bindReviewerParent(activation) { activeReviewerParent = activation; }, decodeNotaryAdmitted() { const ticketNumber = readNotaryTicketFlag( roleHost.getFlag(NOTARY_TICKET_FLAG.name), ); return ticketNumber === undefined ? undefined : { ticketNumber }; }, collector, doctor, notary, countersign, gleanerLeft, inspector, gatekeeper, navigator, auditor, diarist, secretariat, merger: async () => { await merger.activate(); }, }; try { // Production topology only (ADR 0048/0049): no test-only ledger hooks. // Admit durable session file first so lifecycle getEntries/appendEntry are truthful: // deferred SM materialization must not wipe an in-memory principal marker. // #855: two-face waiting.jsonl write removed — fail-closed book-key + session only. resolveBookKeyFromGit(ctx.cwd); durableSessionPointer(ctx.sessionManager); // Station children (court diarist, inner-gate summons) omit navigator sidecar (#840). // Top-level public entry legs still attend automatically. // Navigator seat never re-attaches itself — prepare turns already ARE the navigator // public activation (prevents summonPublicRole navigator ↔ attendance recursion). const isStationChild = roleHost.getFlag(STATION_CHILD_FLAG.name) === true; if ( dependencies.createNavigatorAttendance !== undefined && entry.role !== "navigator" && !isStationChild ) { navigatorSessionParent = ctx.sessionManager.getSessionFile(); navigatorCwd = ctx.cwd; let work: NavigatorWorkContext; let contextError: unknown; if (dependencies.loadNavigatorWorkContext === undefined) { const fallbackSubjectKey = subjectPath(ctx.sessionManager.getSessionDir(), ctx.cwd); contextError = new Error("Navigator work context loader is not configured"); work = { subjectKey: fallbackSubjectKey, subject: `work subject: ${fallbackSubjectKey}`, authority: "", subjectProvenance: "placeholder" }; } else { try { work = await dependencies.loadNavigatorWorkContext({ context: ctx, role: entry.role, phase: navigatorPhase(roleHost, entry.role), getFlag: (name) => roleHost.getFlag(name), }); contextError = work.contextError; } catch (error) { // Contract: README.md#Navigator-attendance — a failed context load continues with a typed placeholder work context; the original cause is retained in contextError for the typed unavailable report. contextError = navigatorUnavailableError("context", error); const fallbackSubjectKey = subjectPath(ctx.sessionManager.getSessionDir(), ctx.cwd); work = { subjectKey: fallbackSubjectKey, subject: `work subject: ${fallbackSubjectKey}`, authority: "", subjectProvenance: "placeholder" }; } } navigatorWorkContext = { ...work, ...(contextError === undefined ? {} : { contextError }) }; // Shared envelope owns exact invocation principal from admitted session lifecycle. // session_start is process activation: resume unfinished principal only when marker // role/phase/subjectKey still match; mint for contradictory marker, malformed nearest, // missing marker, or terminal already completed. const sessionEntries = [...ctx.sessionManager.getEntries()]; const invocationPhase = navigatorPhase(roleHost, entry.role); const lifecyclePrincipal = resolveLifecycleInvocationPrincipal(sessionEntries, { role: entry.role, phase: invocationPhase, subjectKey: work.subjectKey, }); const invocationId = lifecyclePrincipal.invocationId; if (!lifecyclePrincipal.resume) { const data = { invocationId, role: entry.role, phase: invocationPhase, subjectKey: work.subjectKey, }; envelopeHost.appendEntry(NAVIGATOR_INVOCATION_ENTRY, data); try { sitianReport({ level: "event", kind: "attendance", cwd: ctx.cwd, sessionParent: ctx.sessionManager.getSessionFile(), payload: { type: NAVIGATOR_INVOCATION_ENTRY, ...data }, source: "role-runtime", }); } catch {} } navigatorAttendance = await dependencies.createNavigatorAttendance({ context: ctx, role: entry.role, phase: invocationPhase, subjectKey: work.subjectKey, subject: work.subject, authority: work.authority, invocationId, ...(contextError === undefined ? {} : { contextError }), onEvent: (navigatorEvent, report) => { pendingNavigatorPresentation = { event: navigatorEvent, report }; }, }); // Warm live help during activation so prepare is not help-bound under load. // Concrete work context also starts full preparation so session create // overlaps the role run. Placeholder subjects wait for before_agent_start // (user prompt may replace the subject key) but still inherit warm help. navigatorAttendance.warmHelp?.(); if ( navigatorWorkContext.contextError === undefined && navigatorWorkContext.subjectProvenance !== "placeholder" ) { navigatorAttendance.prepare(); } } await executeActivationStage(entry.role, activationStage(entry.role, runtime), { clock, writeTrace }); roleReferenceMaterials = await dependencies.loadRoleReferenceMaterials?.(entry.role) ?? ""; // #357 T2 / #378 / #380 / #391 / #818: any role+engine activation registers the package detour tool once. // Gate is resolveEngineName (RoleHost flag → env fallback) — no per-engine execute branch; no role-module spawn. if (!engineDetourRegistered) { engineDetourRegistered = registerEngineDetourTool(roleHost, hostActions); } // Secretariat owns a declared active surface. The shared engine detour is // registered after role activation, so include it here rather than leave // a newly registered optional tool unreachable until a later reload. if (entry.role === "secretariat" && engineDetourRegistered) { roleHost.setActiveTools([ ...new Set([...roleHost.getActiveTools(), ENGINE_DETOUR_TOOL_NAME]), ]); } // Worker gates ①②: arm records baseline and runs private one-shot hook uninstall (ADR 0070). // Parent session feeds #216 createRecordSession so baseline/bounce survive resume. if (entry.role === "coder" || entry.role === "fixer") { if (entry.role === "coder") coder.armSubmissionGate(ctx.cwd, ctx.sessionManager); else fixer.armSubmissionGate(ctx.cwd, ctx.sessionManager); } admitted = true; } catch (error) { failInfrastructure(error, ctx); } }); }; }