@akagilnc/pi-workflow-roles 0.1.3572 → 0.1.3621

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/acp-host/production-host.js +20239 -10556
  2. package/dist/analyst-gate-cycles-read.js +387 -0
  3. package/dist/archivist-record-entry.js +22 -1
  4. package/dist/audit-escalation.js +6 -0
  5. package/dist/auditor-soul.js +74 -0
  6. package/dist/collector-config.js +86 -0
  7. package/dist/collector-evidence.js +316 -0
  8. package/dist/collector-github.js +527 -0
  9. package/dist/collector-ledger.js +853 -0
  10. package/dist/collector-tool-schemas.js +33 -0
  11. package/dist/compliance-transport.js +104 -52
  12. package/dist/diarist-mechanical.js +411 -0
  13. package/dist/diarist-ticket-resolution.js +177 -0
  14. package/dist/diarist.js +311 -0
  15. package/dist/doctor-auditor.js +25 -0
  16. package/dist/doctor-contracts.js +2 -0
  17. package/dist/doctor-evidence.js +142 -0
  18. package/dist/gatekeeper-role.js +104 -81
  19. package/dist/host-transition-prior-native.js +78 -0
  20. package/dist/institutional-resolution.js +1 -95
  21. package/dist/judge-auditor.js +26 -0
  22. package/dist/ledger-session-read.js +227 -0
  23. package/dist/merger-git-state.js +78 -0
  24. package/dist/navigator-attendance.js +15 -7
  25. package/dist/navigator-public-session.js +170 -0
  26. package/dist/navigator-session-contracts.js +36 -40
  27. package/dist/notary-source-run.js +122 -0
  28. package/dist/package-contracts/auditor-output.js +66 -0
  29. package/dist/package-contracts/evidence-child-output.js +34 -0
  30. package/dist/package-contracts/judge-output.js +2 -0
  31. package/dist/package-contracts/terminating-tools.js +25 -2
  32. package/dist/package-resources/method-skill.js +254 -0
  33. package/dist/packaged-role-registry.js +36 -0
  34. package/dist/pi/durable-principal.js +61 -0
  35. package/dist/pi/in-process-session.js +25 -6
  36. package/dist/pi/known-failure.js +52 -0
  37. package/dist/pi/role-turn-host.js +430 -0
  38. package/dist/public-cli/auto-resume.js +414 -0
  39. package/dist/public-cli/cli-errors.js +8 -0
  40. package/dist/public-cli/cli-io.js +1 -0
  41. package/dist/public-cli/command-renderer.js +5 -0
  42. package/dist/public-cli/doctor-run.js +84 -0
  43. package/dist/public-cli/inspector-run.js +136 -0
  44. package/dist/public-cli/instruction-seat-run.js +256 -0
  45. package/dist/public-cli/invocation.js +2225 -0
  46. package/dist/public-cli/judge-run.js +118 -0
  47. package/dist/public-cli/load-production-acp-host.js +40 -0
  48. package/dist/public-cli/main.js +1140 -652
  49. package/dist/public-cli/notary-run.js +161 -0
  50. package/dist/public-cli/option-definitions.js +1192 -0
  51. package/dist/public-cli/post-admission.js +667 -0
  52. package/dist/public-cli/public-run-credentials.js +49 -0
  53. package/dist/public-cli/registry.js +4 -1
  54. package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
  55. package/dist/public-cli/run-lifecycle.js +1344 -0
  56. package/dist/public-cli/seat-ticket-binding.js +113 -0
  57. package/dist/public-cli/settlement.js +3441 -0
  58. package/dist/public-cli/terminal.js +135 -0
  59. package/dist/public-cli/turn-request.js +25 -0
  60. package/dist/public-role-summons.js +301 -0
  61. package/dist/receipt-delivery-policy.js +10 -0
  62. package/dist/reviewer-child-executor.js +80 -8
  63. package/dist/reviewer-execution-ledger.js +2 -1
  64. package/dist/run-terminal-artifacts.js +195 -0
  65. package/dist/run-ticket-number.js +40 -0
  66. package/dist/session-assistant-usage.js +107 -0
  67. package/dist/shape-unreadable-failure.js +34 -0
  68. package/dist/submission-errors.js +3 -0
  69. package/dist/submission-ledger.js +388 -0
  70. package/dist/ticket-provenance-contracts.js +115 -0
  71. package/dist/ticket-provenance.js +340 -0
  72. package/extensions/role-runtime.ts +7 -0
  73. package/package.json +1 -1
  74. package/scripts/build-package.mjs +2 -1
  75. package/souls/doctor-auditor.md +1 -0
  76. package/souls/evidence-child.md +1 -0
  77. package/src/acp-host/production-host.ts +3 -0
  78. package/src/acp-host/role-envelope.ts +2 -2
  79. package/src/acp-host/role-turn-host.ts +9 -1
  80. package/src/analyst-gate-cycles-read.ts +37 -2
  81. package/src/archivist-record-entry.ts +45 -1
  82. package/src/audit-escalation.ts +17 -0
  83. package/src/auditor-role.ts +22 -0
  84. package/src/auditor-soul.ts +42 -0
  85. package/src/compliance-transport.ts +177 -66
  86. package/src/doctor-auditor.ts +10 -14
  87. package/src/doctor-contracts.ts +1 -0
  88. package/src/doctor-role.ts +2 -2
  89. package/src/evidence-child-role.ts +22 -0
  90. package/src/gatekeeper-pass-envelope.ts +109 -0
  91. package/src/gatekeeper-role.ts +145 -104
  92. package/src/host-contracts.ts +9 -1
  93. package/src/institutional-resolution.ts +8 -163
  94. package/src/judge-auditor.ts +10 -15
  95. package/src/judge-role.ts +8 -0
  96. package/src/navigator-attendance.ts +25 -9
  97. package/src/navigator-public-session.ts +217 -0
  98. package/src/navigator-session-contracts.ts +61 -54
  99. package/src/notary-source-run.ts +3 -1
  100. package/src/package-contracts/auditor-output.ts +82 -0
  101. package/src/package-contracts/evidence-child-output.ts +51 -0
  102. package/src/package-contracts/judge-output.ts +1 -0
  103. package/src/package-contracts/reviewer-output.ts +2 -2
  104. package/src/package-contracts/terminating-tools.ts +31 -1
  105. package/src/packaged-role-registry.ts +43 -0
  106. package/src/pi/adapter.ts +2 -1
  107. package/src/pi/in-process-session.ts +38 -12
  108. package/src/pi/role-turn-host.ts +34 -0
  109. package/src/public-cli/cli.ts +14 -6
  110. package/src/public-cli/instruction-seat-run.ts +289 -50
  111. package/src/public-cli/invocation.ts +66 -25
  112. package/src/public-cli/option-definitions.ts +61 -0
  113. package/src/public-cli/post-admission.ts +7 -1
  114. package/src/public-cli/registry.ts +4 -1
  115. package/src/public-cli/run-lifecycle.ts +22 -3
  116. package/src/public-cli/settlement.ts +120 -5
  117. package/src/public-cli/terminal.ts +2 -0
  118. package/src/public-role-summons.ts +431 -0
  119. package/src/receipt-delivery-policy.ts +10 -0
  120. package/src/reviewer-agent.ts +1 -1
  121. package/src/reviewer-child-executor.ts +114 -12
  122. package/src/reviewer-execution-ledger.ts +3 -2
  123. package/src/role-runtime.ts +158 -17
  124. package/src/session-assistant-usage.ts +119 -0
  125. package/src/session-opening-materials.ts +1 -1
  126. package/src/shape-unreadable-failure.ts +49 -0
  127. package/src/submission-errors.ts +3 -0
  128. package/dist/evidence-child-executor.js +0 -829
  129. package/src/evidence-child-executor.ts +0 -1140
@@ -0,0 +1,3441 @@
1
+ /**
2
+ * Shared settlement for public Role runs: role outcome + Navigator fact + artifacts
3
+ * into one Terminal result (ADR 0052 / #106 / #107 / #101).
4
+ * Controlled failures and audit human decisions settle here without washing causes.
5
+ */
6
+ import { randomUUID } from "node:crypto";
7
+ import { appendFile, lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
8
+ import { dirname, join } from "node:path";
9
+ import { readAnalystGateCyclesFromAuditorRoles, } from "../analyst-gate-cycles-read.js";
10
+ import { readSitianRecords, resolveSitianRecordPath, sitianReport } from "../sitian-facade.js";
11
+ import { readAuditEscalationSubmission, readLatestSubmissionOutcome, readSealedSubmission, } from "../submission-ledger.js";
12
+ import { isAuditEscalationResult } from "../audit-escalation.js";
13
+ import { AUDITOR_SOUL_ROLES } from "../auditor-soul.js";
14
+ import { DOCTOR_AUDIT_TOOL_NAME } from "../doctor-auditor.js";
15
+ import { JUDGE_AUDIT_TOOL_NAME } from "../judge-auditor.js";
16
+ import { stampShapeUnreadableDetails } from "../shape-unreadable-failure.js";
17
+ import { knownFailureFromProviderStop } from "../pi/known-failure.js";
18
+ import { readReviewerDispatchRejection } from "./reviewer-dispatch-rejection.js";
19
+ import { RESUME_TRANSPORT_ENVELOPE, isV1ResumableProvider, readLatestTypedProviderHttpObservation, readTypedHttp429Observation, } from "./run-lifecycle.js";
20
+ import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, COMPLIANCE_RESPONSE_ENTRY_TYPE, readComplianceCandidate, } from "../compliance-transport.js";
21
+ // COMPLIANCE_RESPONSE_ENTRY_TYPE remains for boundRetainedAuditResponse (call/result
22
+ // interval binding on historical session bytes). Provider-stop retain authority is Sitian.
23
+ import { COLLECTOR_OBSERVE_TOOL, COLLECTOR_READ_TOOL, COLLECTOR_REQUEST_TOOL, COLLECTOR_WAIT_TOOL, } from "../collector-ledger.js";
24
+ import { ENGINE_DETOUR_TOOL_NAME } from "../engine-detour.js";
25
+ import { JUDGE_OUTPUT_TOOL_NAME, } from "../package-contracts/judge-output.js";
26
+ import { COLLECTOR_OUTPUT_TOOL, validateAcceptedCollectorReceipt, } from "../package-contracts/collector-output.js";
27
+ import { CODER_OUTPUT_TOOL_NAME, FIXER_OUTPUT_TOOL_NAME, validateAcceptedCoderDetails, validateFixerOutput, } from "../package-contracts/worker-output.js";
28
+ import { validateAcceptedDetails } from "../package-contracts/terminating-tools.js";
29
+ import { DOCTOR_OUTPUT_TOOL_NAME, validateRecordedDoctorOutput, } from "../doctor-contracts.js";
30
+ import { REVIEWER_OUTPUT_TOOL_NAME, validateRuntimeReviewerReceipt, } from "../package-contracts/reviewer-output.js";
31
+ import { MERGER_OUTPUT_TOOL_NAME, validateMergerOutput, } from "../merger-contracts.js";
32
+ import { NOTARY_OUTPUT_TOOL_NAME, notaryDecisiveFacts, validateRecordedNotaryOutput, } from "../notary-contracts.js";
33
+ import { COUNTERSIGN_OUTPUT_TOOL_NAME, validateRecordedCountersignOutput, } from "../countersign-contracts.js";
34
+ import { GLEANER_LEFT_OUTPUT_TOOL_NAME, gleanerLeftDecisiveFacts, validateRecordedGleanerLeftOutput, } from "../gleaner-left-contracts.js";
35
+ import { DIARIST_OUTPUT_TOOL_NAME, diaristDecisiveFacts, validateRecordedDiaristOutput, } from "../diarist-contracts.js";
36
+ import { INSPECTOR_OUTPUT_TOOL_NAME, inspectorDecisiveFacts, validateRecordedInspectorOutput, } from "../inspector-contracts.js";
37
+ import { GATEKEEPER_OUTPUT_TOOL_NAME, gatekeeperDecisiveFacts, validateRecordedGatekeeperOutput, } from "../package-contracts/gatekeeper-output.js";
38
+ import { NAVIGATOR_OUTPUT_TOOL_NAME, navigatorDecisiveFacts, validateRecordedNavigatorOutput, } from "../package-contracts/navigator-output.js";
39
+ import { AUDITOR_OUTPUT_TOOL_NAME, auditorDecisiveFacts, projectLawfulAuditorOutput, validateRecordedAuditorOutput, } from "../package-contracts/auditor-output.js";
40
+ import { EVIDENCE_CHILD_OUTPUT_TOOL_NAME, evidenceChildDecisiveFacts, validateRecordedEvidenceChildOutput, } from "../package-contracts/evidence-child-output.js";
41
+ import { observePackagedMethodSkillInvocation, } from "../package-resources/method-skill.js";
42
+ import { classifyPackagedRoleTerminalResult, findLatestDurablePackagedRoleTerminal, hasNavigatorInfrastructureFailureBase, isAcceptedPackagedRoleTerminalResult, isReceiptSettlementBindingClear, NAVIGATOR_INVOCATION_ENTRY, parseInvocationMarkerIdentity, } from "../navigator-invocation-identity.js";
43
+ import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, parseNoReceiptLifecycleFacts } from "../receipt-delivery-policy.js";
44
+ import { ensureRunArtifactsDir, homeFromRunDirectory, } from "./invocation.js";
45
+ /** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
46
+ function sealedLedgerHome(admitted) {
47
+ return homeFromRunDirectory(admitted.runDirectory);
48
+ }
49
+ function ledgerReadScope(admitted, scope) {
50
+ return {
51
+ home: sealedLedgerHome(admitted),
52
+ ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
53
+ ? {}
54
+ : { attemptId: scope.courtAttemptId }),
55
+ };
56
+ }
57
+ async function sealedLedgerOutcome(admitted, scope) {
58
+ return readSealedSubmission(admitted.projectRoot, admitted.runId, ledgerReadScope(admitted, scope));
59
+ }
60
+ export async function sealedAcceptanceRedispatchDisposition(admitted) {
61
+ try {
62
+ if ((await sealedLedgerOutcome(admitted)) !== undefined) {
63
+ return { kind: "block", reason: "sealed-accepted" };
64
+ }
65
+ return { kind: "allow" };
66
+ }
67
+ catch (cause) {
68
+ // Ledger authority must not wash read failure into "unsealed" (#648).
69
+ return { kind: "block", reason: "authority-failed", cause };
70
+ }
71
+ }
72
+ async function auditEscalationLedgerOutcome(admitted, role, scope) {
73
+ const projection = await readAuditEscalationSubmission(admitted.projectRoot, admitted.runId, ledgerReadScope(admitted, scope));
74
+ if (projection?.role !== role)
75
+ return undefined;
76
+ return projection;
77
+ }
78
+ async function closedLedgerOutcome(admitted, role, scope) {
79
+ const sealed = await sealedLedgerOutcome(admitted, scope);
80
+ return sealed?.role === role
81
+ ? sealed
82
+ : auditEscalationLedgerOutcome(admitted, role, scope);
83
+ }
84
+ /** Transitional host-session reads remain only for non-sealed failure and audit evidence. */
85
+ function coordinatesFromAdmitted(authority, admitted) {
86
+ return authority.decode(admitted.principal);
87
+ }
88
+ import { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, recommendationNavigatorFact, buildResidualIncompleteTerminalOutcome, redactExactRunId, } from "./terminal.js";
89
+ export { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, };
90
+ /** Presentation bound for one stderr diagnostic line (durable artifact keeps full text). */
91
+ export const CONCISE_DIAGNOSTIC_MAX_CHARS = 480;
92
+ /**
93
+ * True when a stderr line is observation/event/token/stack flood rather than a diagnostic.
94
+ * Recognizes both `event:` prefixes and real JSONL records with an `event` key
95
+ * (tool_execution_* observation face).
96
+ */
97
+ export function isChildDiagnosticFloodLine(line) {
98
+ if (/^at\s+/.test(line))
99
+ return true;
100
+ if (line.startsWith("event:"))
101
+ return true;
102
+ if (/\btokens?=/.test(line))
103
+ return true;
104
+ if (/\btool_calls?=/.test(line))
105
+ return true;
106
+ if (line.startsWith("{")) {
107
+ try {
108
+ const parsed = JSON.parse(line);
109
+ if (typeof parsed === "object" &&
110
+ parsed !== null &&
111
+ !Array.isArray(parsed) &&
112
+ typeof parsed.event === "string") {
113
+ return true;
114
+ }
115
+ }
116
+ catch {
117
+ // Not JSON — may still be a real diagnostic.
118
+ }
119
+ }
120
+ return false;
121
+ }
122
+ /**
123
+ * True when a stderr line is Pi auth/model help scaffolding rather than the failure identity.
124
+ * Real counterexample: multi-line "No API key…" guidance ends with docs/*.md path lines;
125
+ * those footers must not displace the primary diagnostic.
126
+ */
127
+ export function isChildDiagnosticHelpFooterLine(line) {
128
+ const trimmed = line.trim();
129
+ if (trimmed.length === 0)
130
+ return false;
131
+ // Path-only doc references (indented or bare).
132
+ if (/^\S+\.(md|txt)$/i.test(trimmed))
133
+ return true;
134
+ // Auth guidance continuations from Pi formatNoApiKeyFoundMessage / getProviderLoginHelp.
135
+ if (/^Use \//i.test(trimmed))
136
+ return true;
137
+ if (/^Then use \//i.test(trimmed))
138
+ return true;
139
+ if (/^See:\s*$/i.test(trimmed))
140
+ return true;
141
+ return false;
142
+ }
143
+ /** Bound one diagnostic for human stderr presentation; durable evidence stays full. */
144
+ export function boundConciseDiagnostic(diagnostic, maxChars = CONCISE_DIAGNOSTIC_MAX_CHARS) {
145
+ if (diagnostic.length <= maxChars)
146
+ return diagnostic;
147
+ if (maxChars <= 1)
148
+ return "…";
149
+ return `${diagnostic.slice(0, maxChars - 1)}…`;
150
+ }
151
+ /**
152
+ * Pick one concise diagnostic line from child stderr without stacks/events/tokens/help footers.
153
+ * Prefers the last nonblank line that is not a frame, observation flood, or docs-path footer.
154
+ * Returns the full selected diagnostic (bound only at presentation).
155
+ */
156
+ export function conciseChildDiagnostic(stderr, fallback) {
157
+ const lines = stderr
158
+ .split(/\r?\n/)
159
+ .map((line) => line.trim())
160
+ .filter((line) => line.length > 0);
161
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
162
+ const line = lines[i];
163
+ if (isChildDiagnosticFloodLine(line))
164
+ continue;
165
+ if (isChildDiagnosticHelpFooterLine(line))
166
+ continue;
167
+ // Strip a leading "Error:" label but keep the message identity.
168
+ return line.replace(/^Error:\s*/i, "").trim() || fallback;
169
+ }
170
+ return fallback;
171
+ }
172
+ export function formatCliDiagnostic(message) {
173
+ return `ak-role: ${message}\n`;
174
+ }
175
+ /**
176
+ * One concise stderr line for humans. Durable Error Artifact / Terminal keep the
177
+ * full original diagnostic — presentation collapses newlines and flood frames.
178
+ */
179
+ export function formatFailureStderrDiagnostic(failure) {
180
+ const selected = conciseChildDiagnostic(failure.diagnostic, "failure");
181
+ // conciseChildDiagnostic already returns one split line; defend fallback paths.
182
+ const oneLine = selected
183
+ .split(/\r?\n/)
184
+ .map((line) => line.trim())
185
+ .find((line) => line.length > 0) ?? "failure";
186
+ return formatCliDiagnostic(boundConciseDiagnostic(oneLine));
187
+ }
188
+ /** Pre-admission structural rejection: stderr only, no run, no Terminal. */
189
+ export function presentStructuralRejection(error, io) {
190
+ io.stderr(formatCliDiagnostic(error.message));
191
+ }
192
+ /** ControlledFailure face without admitted-run Terminal (stdout body + stderr line). */
193
+ export function presentControlledFailure(failure, io) {
194
+ io.stdout(`${JSON.stringify(failure, null, 2)}\n`);
195
+ io.stderr(formatFailureStderrDiagnostic(failure));
196
+ }
197
+ export async function inspectJudgeSession(sessionFile) {
198
+ try {
199
+ await readFile(sessionFile, "utf8");
200
+ return { state: "present" };
201
+ }
202
+ catch (error) {
203
+ if (isMissingPathError(error))
204
+ return { state: "missing" };
205
+ return {
206
+ state: "unreadable",
207
+ diagnostic: error instanceof Error
208
+ ? error.message || error.name
209
+ : String(error),
210
+ };
211
+ }
212
+ }
213
+ function thrownIdentity(error) {
214
+ const identity = {
215
+ name: error.name,
216
+ };
217
+ const code = error.code;
218
+ if (typeof code === "string" || typeof code === "number") {
219
+ identity.code = code;
220
+ }
221
+ return identity;
222
+ }
223
+ /** Production-owned typed thrown failure (explicit-internal channel). */
224
+ function isTypedActivationError(error) {
225
+ if (!(error instanceof Error))
226
+ return false;
227
+ const cause = error.knownCause;
228
+ return (cause === "provider" ||
229
+ cause === "activation" ||
230
+ cause === "session" ||
231
+ cause === "output" ||
232
+ cause === "timeout" ||
233
+ cause === "unrecognized");
234
+ }
235
+ /** Flatten nested AggregateError leaves; non-aggregate values stay as one fact. */
236
+ function flattenThrownFailureLeaves(error) {
237
+ if (!(error instanceof AggregateError)) {
238
+ return [error];
239
+ }
240
+ const leaves = [];
241
+ for (const item of error.errors) {
242
+ leaves.push(...flattenThrownFailureLeaves(item));
243
+ }
244
+ return leaves;
245
+ }
246
+ /**
247
+ * Project one thrown value into a ControlledFailure leaf.
248
+ * Sole owner for thrown-leaf identity/diagnostic mapping.
249
+ * AggregateError nesting is handled by classifyThrownFailure.
250
+ */
251
+ export function projectThrownFailureLeaf(error) {
252
+ if (isTypedActivationError(error)) {
253
+ const identity = thrownIdentity(error);
254
+ if (error.failureCode !== undefined && identity.code === undefined) {
255
+ identity.code = error.failureCode;
256
+ }
257
+ return {
258
+ cause: error.knownCause,
259
+ diagnostic: error.message || error.name || "unrecognized exception",
260
+ identity,
261
+ ...(error.details === undefined ? {} : { details: error.details }),
262
+ };
263
+ }
264
+ if (error instanceof Error) {
265
+ const identity = thrownIdentity(error);
266
+ return {
267
+ cause: "unrecognized",
268
+ diagnostic: error.message || error.name || "unrecognized exception",
269
+ identity,
270
+ };
271
+ }
272
+ return {
273
+ cause: "unrecognized",
274
+ diagnostic: String(error),
275
+ };
276
+ }
277
+ /**
278
+ * Concurrent thrown failures (host + cleanup, etc.):
279
+ * primary leaf owns cause/diagnostic/identity; remaining leaves stay as
280
+ * details.concurrentFailures so neither fact covers the other.
281
+ */
282
+ function classifyThrownFailure(error) {
283
+ if (!(error instanceof AggregateError)) {
284
+ return projectThrownFailureLeaf(error);
285
+ }
286
+ const leaves = flattenThrownFailureLeaves(error);
287
+ if (leaves.length === 0) {
288
+ // Empty aggregate — retain the aggregate shell rather than invent a cause.
289
+ return projectThrownFailureLeaf(error);
290
+ }
291
+ const primary = projectThrownFailureLeaf(leaves[0]);
292
+ if (leaves.length === 1) {
293
+ return primary;
294
+ }
295
+ const priorConcurrent = Array.isArray(primary.details?.concurrentFailures)
296
+ ? primary.details.concurrentFailures
297
+ : [];
298
+ const concurrentFailures = [
299
+ ...priorConcurrent,
300
+ ...leaves.slice(1).map((leaf) => {
301
+ const secondary = projectThrownFailureLeaf(leaf);
302
+ return {
303
+ cause: secondary.cause,
304
+ diagnostic: secondary.diagnostic,
305
+ ...(secondary.identity === undefined ? {} : { identity: secondary.identity }),
306
+ ...(secondary.details === undefined ? {} : { details: secondary.details }),
307
+ };
308
+ }),
309
+ ];
310
+ return {
311
+ cause: primary.cause,
312
+ diagnostic: primary.diagnostic,
313
+ ...(primary.identity === undefined ? {} : { identity: primary.identity }),
314
+ details: {
315
+ ...(primary.details ?? {}),
316
+ concurrentFailures,
317
+ },
318
+ };
319
+ }
320
+ /** Merge caller-owned secondary evidence into a classified failure without washing path facts. */
321
+ function withKnownDetails(failure, knownDetails) {
322
+ if (knownDetails === undefined)
323
+ return failure;
324
+ const { timedOut: _knownTimedOut, ...rest } = knownDetails;
325
+ return {
326
+ ...failure,
327
+ details: {
328
+ ...rest,
329
+ ...(failure.details ?? {}),
330
+ },
331
+ };
332
+ }
333
+ /**
334
+ * Classify a controlled post-admission failure without washing unrecognized identities.
335
+ * Cause classes are closed; diagnostic text retains the original identity when known.
336
+ *
337
+ * Order: thrown → knownCause → timeout → activation (nonzero) → session → output.
338
+ * knownCause precedes timeout so a co-present typed provider/session identity is not
339
+ * washed when the child also timed out. Cause is never inferred from stderr wording.
340
+ * AggregateError concurrent leaves keep primary identity and secondary facts in details.
341
+ */
342
+ export function classifyPostAdmissionFailure(input) {
343
+ // Own-key presence, not value: `throw undefined` is a real caught exception.
344
+ if (Object.hasOwn(input, "thrown")) {
345
+ return classifyThrownFailure(input.thrown);
346
+ }
347
+ if (input.knownCause !== undefined) {
348
+ const fallback = input.knownCause === "provider"
349
+ ? "provider failure"
350
+ : input.knownCause === "session"
351
+ ? "session unreadable"
352
+ : input.knownCause === "output"
353
+ ? "role run completed without a lawful typed terminal result"
354
+ : `role run failed (${input.knownCause})`;
355
+ const diagnostic = input.knownDiagnostic !== undefined && input.knownDiagnostic.trim() !== ""
356
+ ? input.knownDiagnostic
357
+ : conciseChildDiagnostic(input.stderr, fallback);
358
+ const { timedOut: _knownTimedOut, ...knownDetails } = input.knownDetails ?? {};
359
+ const remoteCode = knownDetails.code;
360
+ return {
361
+ cause: input.knownCause,
362
+ diagnostic,
363
+ details: {
364
+ ...knownDetails,
365
+ ...(remoteCode === undefined ? {} : { code: remoteCode }),
366
+ exitCode: input.code,
367
+ ...(input.timedOut ? { timedOut: true } : {}),
368
+ },
369
+ ...(input.knownIdentity === undefined
370
+ ? {}
371
+ : { identity: input.knownIdentity }),
372
+ };
373
+ }
374
+ if (input.timedOut) {
375
+ return withKnownDetails({
376
+ cause: "timeout",
377
+ diagnostic: "role run timed out",
378
+ details: { timedOut: true, exitCode: input.code },
379
+ }, input.knownDetails);
380
+ }
381
+ if (input.code !== 0) {
382
+ const fallback = `role run failed with exit ${input.code ?? "null"}`;
383
+ return withKnownDetails({
384
+ cause: "activation",
385
+ diagnostic: conciseChildDiagnostic(input.stderr, fallback),
386
+ details: { exitCode: input.code },
387
+ }, input.knownDetails);
388
+ }
389
+ if (input.session?.state === "missing") {
390
+ return withKnownDetails({
391
+ cause: "session",
392
+ diagnostic: "role run left no readable session transcript",
393
+ details: { exitCode: input.code, session: "missing" },
394
+ }, input.knownDetails);
395
+ }
396
+ if (input.session?.state === "unreadable") {
397
+ return withKnownDetails({
398
+ cause: "session",
399
+ diagnostic: input.session.diagnostic,
400
+ details: { exitCode: input.code, session: "unreadable" },
401
+ }, input.knownDetails);
402
+ }
403
+ return withKnownDetails({
404
+ cause: "output",
405
+ diagnostic: "role run completed without a lawful typed terminal result",
406
+ details: { exitCode: input.code },
407
+ }, input.knownDetails);
408
+ }
409
+ /** One projection owner for the four audited public runners. */
410
+ export function explicitInternalKnownFailureClassificationInput(failure) {
411
+ if (failure === undefined)
412
+ return {};
413
+ return {
414
+ knownCause: failure.cause,
415
+ ...(failure.identity === undefined ? {} : { knownIdentity: failure.identity }),
416
+ ...(failure.diagnostic === undefined ? {} : { knownDiagnostic: failure.diagnostic }),
417
+ ...(failure.details === undefined ? {} : { knownDetails: failure.details }),
418
+ };
419
+ }
420
+ /** Post-role Navigator delivery grace (Issue #11 / #101 / #106 / #159). */
421
+ export const NAVIGATOR_POST_ROLE_GRACE_MS = 10_000;
422
+ function isMissingPathError(error) {
423
+ return (error instanceof Error &&
424
+ "code" in error &&
425
+ error.code === "ENOENT");
426
+ }
427
+ /**
428
+ * Preserve session-read failure identity as a typed session cause.
429
+ * SyntaxError keeps its name so durable settlement does not wash malformed JSONL
430
+ * into generic output absence.
431
+ */
432
+ function sessionReadFailure(error, fallbackMessage) {
433
+ if (error instanceof SyntaxError) {
434
+ const failed = new SyntaxError(error.message || fallbackMessage);
435
+ failed.knownCause = "session";
436
+ return failed;
437
+ }
438
+ if (error instanceof Error) {
439
+ const failed = new Error(error.message || error.name || fallbackMessage);
440
+ failed.name = error.name || "Error";
441
+ failed.knownCause = "session";
442
+ const code = error.code;
443
+ if (typeof code === "string" || typeof code === "number") {
444
+ failed.failureCode = code;
445
+ failed.code = code;
446
+ }
447
+ return failed;
448
+ }
449
+ const failed = new Error(String(error));
450
+ failed.knownCause = "session";
451
+ return failed;
452
+ }
453
+ /**
454
+ * Read the exact bound Pi session file principal.
455
+ * Does not scan the session directory for "latest" — resume identity is the file.
456
+ */
457
+ async function readBoundSessionEntries(sessionFile) {
458
+ const text = await readFile(sessionFile, "utf8");
459
+ const entries = [];
460
+ for (const line of text.trim().split("\n").filter(Boolean)) {
461
+ try {
462
+ entries.push(JSON.parse(line));
463
+ }
464
+ catch (error) {
465
+ throw sessionReadFailure(error, "malformed session JSONL");
466
+ }
467
+ }
468
+ return entries;
469
+ }
470
+ function typedHttpStatusFromMessage(message) {
471
+ for (const candidate of [message.httpStatus, message.statusCode, message.status]) {
472
+ if (typeof candidate === "number" && (candidate < 200 || candidate >= 300))
473
+ return candidate;
474
+ }
475
+ return undefined;
476
+ }
477
+ function sessionProviderStopFromAssistant(message) {
478
+ if (message?.role !== "assistant")
479
+ return undefined;
480
+ if (message.stopReason !== "error" && message.stopReason !== "aborted")
481
+ return undefined;
482
+ const httpStatus = typedHttpStatusFromMessage(message);
483
+ return {
484
+ stopReason: message.stopReason,
485
+ // Preserve held errorMessage bytes — emptiness check must not rewrite.
486
+ ...(typeof message.errorMessage === "string" && message.errorMessage.trim() !== ""
487
+ ? { errorMessage: message.errorMessage }
488
+ : {}),
489
+ ...(typeof message.provider === "string" && message.provider.trim() !== ""
490
+ ? { provider: message.provider }
491
+ : {}),
492
+ ...(typeof message.model === "string" && message.model.trim() !== ""
493
+ ? { model: message.model }
494
+ : {}),
495
+ ...(typeof message.api === "string" && message.api.trim() !== ""
496
+ ? { api: message.api }
497
+ : {}),
498
+ ...(typeof message.rawStopReason === "string" && message.rawStopReason.trim() !== ""
499
+ ? { rawStopReason: message.rawStopReason }
500
+ : {}),
501
+ ...(message.diagnostics === undefined ? {} : { diagnostics: message.diagnostics }),
502
+ ...(httpStatus === undefined ? {} : { httpStatus }),
503
+ ...(message.body === undefined ? {} : { body: message.body }),
504
+ ...(message.code === undefined ? {} : { code: message.code }),
505
+ ...(message.errno === undefined ? {} : { errno: message.errno }),
506
+ };
507
+ }
508
+ export function extractSessionProviderStop(entries) {
509
+ // A resumed dispatch appends a typed top-level user turn to the same session.
510
+ // Older attempt native stops must not replace the newer attempt's stop.
511
+ // Sessions without a user turn are the initial attempt.
512
+ // Auditor retained responses live in Sitian (kind=auditor); see readSessionProviderStop.
513
+ let attemptStart = 0;
514
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
515
+ const entry = entries[i];
516
+ if (entry?.type === "message" && entry.message?.role === "user") {
517
+ attemptStart = i;
518
+ break;
519
+ }
520
+ }
521
+ for (let i = entries.length - 1; i >= attemptStart; i -= 1) {
522
+ const entry = entries[i];
523
+ if (entry?.type !== "message")
524
+ continue;
525
+ const message = entry.message;
526
+ if (message?.role !== "assistant")
527
+ continue;
528
+ // Latest assistant in the current attempt only (reviewer-child-executor lastAssistant pattern).
529
+ return sessionProviderStopFromAssistant(message);
530
+ }
531
+ return undefined;
532
+ }
533
+ /**
534
+ * Latest Sitian-retained auditor response stop for this parent session principal.
535
+ * Writer: retainComplianceResponse → sitianReport(kind=auditor, payload={version,response}).
536
+ * Payload stopReason is preserved as retained — aborted stays aborted; no 500/error wash here.
537
+ */
538
+ async function readSitianRetainedAuditorProviderStop(sessionFile) {
539
+ try {
540
+ const { recordFile } = resolveSitianRecordPath({
541
+ level: "event",
542
+ kind: "auditor",
543
+ sessionParent: sessionFile,
544
+ // Path is driven by sessionParent when under ledger home; cwd is a fallback only.
545
+ cwd: dirname(sessionFile),
546
+ });
547
+ const { records } = await readSitianRecords(recordFile);
548
+ for (let i = records.length - 1; i >= 0; i -= 1) {
549
+ const payload = records[i]?.payload;
550
+ if (!isRecord(payload) || !isRecord(payload.response))
551
+ continue;
552
+ // Lifecycle events carry `type` (binding / compliance_failure); retain does not.
553
+ if (typeof payload.type === "string")
554
+ continue;
555
+ const stop = sessionProviderStopFromAssistant(payload.response);
556
+ if (stop !== undefined)
557
+ return stop;
558
+ // Latest retain exists but is not a provider-stop — do not scan older retains
559
+ // (mirrors former session COMPLIANCE_RESPONSE preference break).
560
+ break;
561
+ }
562
+ }
563
+ catch {
564
+ // Missing volume or unreadable path is absence, not a settlement failure.
565
+ }
566
+ return undefined;
567
+ }
568
+ /** Read retained auditor stop (Sitian) then native session assistant stop, if any. */
569
+ export async function readSessionProviderStop(sessionFile) {
570
+ const retained = await readSitianRetainedAuditorProviderStop(sessionFile);
571
+ if (retained !== undefined)
572
+ return retained;
573
+ try {
574
+ const entries = await readBoundSessionEntries(sessionFile);
575
+ return extractSessionProviderStop(entries);
576
+ }
577
+ catch {
578
+ return undefined;
579
+ }
580
+ }
581
+ /**
582
+ * Recover a provider stop from Reviewer fixed-axis evidence children bound to this parent.
583
+ * Dispatch runs during activation before the parent model turn; leg failures leave durable
584
+ * stops under session/evidence-children/ and must not wash into generic activation.
585
+ */
586
+ export async function readBoundEvidenceChildKnownFailure(sessionFile) {
587
+ const childDirectory = join(dirname(sessionFile), "evidence-children");
588
+ let names;
589
+ try {
590
+ names = await readdir(childDirectory);
591
+ }
592
+ catch (error) {
593
+ if (isMissingPathError(error))
594
+ return undefined;
595
+ throw sessionReadFailure(error, "failed to read bound evidence-child session directory");
596
+ }
597
+ for (const file of names.filter((name) => name.endsWith(".jsonl")).sort().reverse()) {
598
+ let entries;
599
+ try {
600
+ entries = await readBoundSessionEntries(join(childDirectory, file));
601
+ }
602
+ catch (error) {
603
+ throw sessionReadFailure(error, "failed to read discovered evidence-child session");
604
+ }
605
+ const header = entries.find((entry) => entry.type === "session");
606
+ if (!isRecord(header) || header.parentSession !== sessionFile)
607
+ continue;
608
+ const stop = extractSessionProviderStop(entries);
609
+ if (stop === undefined)
610
+ continue;
611
+ const primary = knownFailureFromProviderStop(stop);
612
+ return {
613
+ ...primary,
614
+ details: {
615
+ ...(primary.details ?? {}),
616
+ secondaryEvidence: "evidence-child",
617
+ },
618
+ };
619
+ }
620
+ return undefined;
621
+ }
622
+ async function loadBoundAuditorVolumes(sessionFile) {
623
+ let parentEntries;
624
+ try {
625
+ parentEntries = await readBoundSessionEntries(sessionFile);
626
+ }
627
+ catch (error) {
628
+ if (isMissingPathError(error))
629
+ return undefined;
630
+ throw sessionReadFailure(error, "failed to read parent session for auditor binding");
631
+ }
632
+ const parentId = parentEntries.find((entry) => entry.type === "session")?.id;
633
+ if (parentId === undefined)
634
+ return undefined;
635
+ const RESUME_ENVELOPE = RESUME_TRANSPORT_ENVELOPE;
636
+ // Keyed prefix on the transport token line only (#600 / 8e767152). Resume may
637
+ // append engine handbook presentation after the token; settlement must not
638
+ // treat those prose lines as a real user turn or key on their shape.
639
+ const isResumeEnvelopeBytes = (value) => {
640
+ if (typeof value !== "string")
641
+ return false;
642
+ const nl = value.indexOf("\n");
643
+ const firstLine = nl === -1 ? value : value.slice(0, nl);
644
+ return firstLine === RESUME_ENVELOPE;
645
+ };
646
+ const isResumeEnvelope = (msg) => {
647
+ if (!isRecord(msg) || msg.role !== "user")
648
+ return false;
649
+ const text = typeof msg.text === "string" ? msg.text : typeof msg.content === "string" ? msg.content : undefined;
650
+ if (isResumeEnvelopeBytes(text))
651
+ return true;
652
+ const content = msg.content;
653
+ if (Array.isArray(content)) {
654
+ return content.some((p) => isRecord(p) && (isResumeEnvelopeBytes(p.text) || isResumeEnvelopeBytes(p.content)));
655
+ }
656
+ return false;
657
+ };
658
+ let latestParentUserIndex = -1;
659
+ for (let i = parentEntries.length - 1; i >= 0; i -= 1) {
660
+ const entry = parentEntries[i];
661
+ if (entry?.type !== "message" || entry.message?.role !== "user")
662
+ continue;
663
+ if (isResumeEnvelope(entry.message))
664
+ continue;
665
+ latestParentUserIndex = i;
666
+ break;
667
+ }
668
+ const childDirectories = [join(dirname(sessionFile), "auditor-roles")];
669
+ // Auto-resume seam (owner A): stale check must ignore resume envelope and
670
+ // prioritize retention. Previous `attemptEntryIndex < latest` discarded the
671
+ // first attempt's child after resume advanced latest, losing retentionFailure
672
+ // when retry had no compliance entry. Fix: ignore envelope for staleness and
673
+ // prefer any valid compliance failure before falling back to primary.
674
+ const valid = [];
675
+ let sawAnyDirectory = false;
676
+ for (const childDirectory of childDirectories) {
677
+ let names;
678
+ try {
679
+ names = await readdir(childDirectory);
680
+ sawAnyDirectory = true;
681
+ }
682
+ catch (error) {
683
+ if (isMissingPathError(error))
684
+ continue;
685
+ throw sessionReadFailure(error, "failed to read bound auditor session directory");
686
+ }
687
+ for (const file of names.filter((name) => name.endsWith(".jsonl")).sort().reverse()) {
688
+ let entries;
689
+ try {
690
+ entries = await readBoundSessionEntries(join(childDirectory, file));
691
+ }
692
+ catch (error) {
693
+ throw sessionReadFailure(error, "failed to read discovered auditor session");
694
+ }
695
+ const header = entries.find((entry) => entry.type === "session");
696
+ if (!isRecord(header))
697
+ continue;
698
+ // Parent-attempt binding owns its interval on multi-attempt volumes
699
+ // (never whole-volume provider/compliance).
700
+ const bindingIndexes = [];
701
+ for (let i = 0; i < entries.length; i += 1) {
702
+ const entry = entries[i];
703
+ if (entry?.type === "custom" && entry.customType === AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE) {
704
+ bindingIndexes.push(i);
705
+ }
706
+ }
707
+ const bindingPasses = bindingIndexes.length > 0
708
+ ? bindingIndexes.map((start, idx) => ({
709
+ entry: entries[start],
710
+ start,
711
+ end: idx + 1 < bindingIndexes.length ? bindingIndexes[idx + 1] : entries.length,
712
+ }))
713
+ : [{ entry: undefined, start: 0, end: entries.length }];
714
+ for (const { entry: bindingEntry, start, end } of bindingPasses) {
715
+ const bindingParent = bindingEntry !== undefined &&
716
+ isRecord(bindingEntry.data) &&
717
+ isRecord(bindingEntry.data.parent)
718
+ ? bindingEntry.data.parent
719
+ : undefined;
720
+ const attemptEntryId = typeof bindingParent?.attemptEntryId === "string"
721
+ ? bindingParent.attemptEntryId
722
+ : undefined;
723
+ const attemptEntryIndex = attemptEntryId === undefined
724
+ ? -1
725
+ : parentEntries.findIndex((entry) => entry.id === attemptEntryId);
726
+ const boundSessionFile = typeof bindingParent?.sessionFile === "string"
727
+ ? bindingParent.sessionFile
728
+ : typeof header.parentSession === "string"
729
+ ? header.parentSession
730
+ : undefined;
731
+ if (boundSessionFile !== sessionFile)
732
+ continue;
733
+ if (bindingParent !== undefined &&
734
+ (bindingParent.sessionId !== parentId || attemptEntryIndex < latestParentUserIndex)) {
735
+ continue;
736
+ }
737
+ if (bindingParent === undefined && header.parentSession !== sessionFile)
738
+ continue;
739
+ valid.push({
740
+ entries: entries.slice(start, end),
741
+ parentId,
742
+ sessionFile,
743
+ ...(attemptEntryId === undefined ? {} : { attemptEntryId }),
744
+ });
745
+ // Keep every qualifying interval in the current parent-user range.
746
+ // A single first-match break drops later same-user summons failures (#636).
747
+ }
748
+ }
749
+ }
750
+ if (!sawAnyDirectory && valid.length === 0)
751
+ return undefined;
752
+ return valid;
753
+ }
754
+ function complianceFailureFromAuditorVolumes(volumes) {
755
+ for (const { entries, attemptEntryId, parentId, sessionFile } of volumes) {
756
+ const stop = extractSessionProviderStop(entries);
757
+ if (stop === undefined)
758
+ continue;
759
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
760
+ const entry = entries[i];
761
+ if (entry?.type !== "custom" || entry.customType !== AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE || !isRecord(entry.data))
762
+ continue;
763
+ const parent = isRecord(entry.data.parent) ? entry.data.parent : undefined;
764
+ const failure = isRecord(entry.data.failure) ? entry.data.failure : undefined;
765
+ if (parent?.sessionId !== parentId || parent.sessionFile !== sessionFile || parent.attemptEntryId !== attemptEntryId || (failure?.cause !== "provider" && failure?.cause !== "unrecognized"))
766
+ continue;
767
+ const identity = isRecord(failure.identity) ? failure.identity : undefined;
768
+ return {
769
+ cause: failure.cause === "provider" ? "provider" : "unrecognized",
770
+ ...(identity === undefined ? {} : { identity: {
771
+ ...(typeof identity.name === "string" ? { name: identity.name } : {}),
772
+ ...(typeof identity.code === "string" || typeof identity.code === "number" ? { code: identity.code } : {}),
773
+ } }),
774
+ ...(typeof failure.diagnostic === "string" ? { diagnostic: failure.diagnostic } : {}),
775
+ ...(isRecord(failure.details) ? { details: failure.details } : {}),
776
+ };
777
+ }
778
+ }
779
+ return undefined;
780
+ }
781
+ function providerStopFallbackFromAuditorVolumes(volumes) {
782
+ for (const { entries } of volumes) {
783
+ const stop = extractSessionProviderStop(entries);
784
+ if (stop === undefined)
785
+ continue;
786
+ const primary = knownFailureFromProviderStop(stop);
787
+ return {
788
+ ...primary,
789
+ details: {
790
+ ...(primary.details ?? {}),
791
+ secondaryEvidence: "unavailable",
792
+ },
793
+ };
794
+ }
795
+ return undefined;
796
+ }
797
+ /** Recover a provider stop from the auditor child bound to the current parent attempt. */
798
+ export async function readBoundAuditorKnownFailure(sessionFile) {
799
+ const volumes = await loadBoundAuditorVolumes(sessionFile);
800
+ if (volumes === undefined)
801
+ return undefined;
802
+ return complianceFailureFromAuditorVolumes(volumes)
803
+ ?? providerStopFallbackFromAuditorVolumes(volumes);
804
+ }
805
+ /** Strong auditor tier only — retained compliance-failure entries, no provider-stop fallback. */
806
+ async function readBoundAuditorComplianceFailure(sessionFile) {
807
+ const volumes = await loadBoundAuditorVolumes(sessionFile);
808
+ if (volumes === undefined)
809
+ return undefined;
810
+ return complianceFailureFromAuditorVolumes(volumes);
811
+ }
812
+ /** Weaker auditor tier: provider stop without a retained compliance-failure entry. */
813
+ async function readBoundAuditorProviderStopFallback(sessionFile) {
814
+ const volumes = await loadBoundAuditorVolumes(sessionFile);
815
+ if (volumes === undefined)
816
+ return undefined;
817
+ return providerStopFallbackFromAuditorVolumes(volumes);
818
+ }
819
+ function typedFailedTerminatingToolKnownFailure(entries) {
820
+ let attemptStart = 0;
821
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
822
+ if (entries[i]?.type === "message" && entries[i]?.message?.role === "user") {
823
+ attemptStart = i;
824
+ break;
825
+ }
826
+ }
827
+ const attemptEntries = entries.slice(attemptStart);
828
+ for (let i = attemptEntries.length - 1; i >= 0; i -= 1) {
829
+ const message = attemptEntries[i]?.message;
830
+ if (attemptEntries[i]?.type !== "message" || message?.role !== "toolResult")
831
+ continue;
832
+ const classification = classifyPackagedRoleTerminalResult(message);
833
+ if (classification.kind !== "infrastructure")
834
+ continue;
835
+ if (typeof message.toolCallId !== "string" || typeof message.toolName !== "string")
836
+ continue;
837
+ if (boundRoleToolCallForResult(attemptEntries, i, message, message.toolName) === undefined)
838
+ continue;
839
+ const textPart = Array.isArray(message.content)
840
+ ? message.content.find((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
841
+ : undefined;
842
+ const diagnostic = isRecord(textPart) ? textPart.text : undefined;
843
+ // Durable details already carry fact + typed evidence from envelope one-shot projection (#475).
844
+ // Do not re-parse retained compliance responses here.
845
+ // Host infrastructure must NOT map to cause=output — that cause is reserved for
846
+ // shape-unreadable retained candidates (ADR 0055 / #675 producer→consumer diversion).
847
+ const details = isRecord(message.details) ? message.details : classification.fact;
848
+ return {
849
+ cause: "activation",
850
+ identity: { name: message.toolName, code: message.toolCallId },
851
+ ...(typeof diagnostic === "string" && diagnostic.trim() !== "" ? { diagnostic } : {}),
852
+ details,
853
+ };
854
+ }
855
+ return undefined;
856
+ }
857
+ function resolutionOf(knownFailure, typedHttp = { settled: false }) {
858
+ return {
859
+ ...(knownFailure === undefined ? {} : { knownFailure }),
860
+ ...(typedHttp.observation === undefined ? {} : { typedHttpObservation: typedHttp.observation }),
861
+ typedHttpObservationSettled: typedHttp.settled,
862
+ };
863
+ }
864
+ /** Sole evidence-priority owner for public runners with Soul auditors. */
865
+ export async function resolveAuditedRunnerFailureResolution(input) {
866
+ if (input.runner !== undefined)
867
+ return resolutionOf(input.runner);
868
+ if (input.runDirectory !== undefined) {
869
+ try {
870
+ const rejection = await readReviewerDispatchRejection(input.runDirectory);
871
+ if (rejection !== undefined)
872
+ return resolutionOf(rejection);
873
+ }
874
+ catch (error) {
875
+ const failure = error instanceof Error ? error : new Error(String(error));
876
+ return resolutionOf({
877
+ cause: "activation",
878
+ identity: thrownIdentity(failure),
879
+ diagnostic: failure.message || failure.name,
880
+ });
881
+ }
882
+ }
883
+ // Bound auditor compliance-failure retention outranks a parent failure that the
884
+ // auditor path itself caused (retention EISDIR race). A typed terminating-tool
885
+ // host failure is next — it outranks weaker auditor provider-stop fallback so
886
+ // parent failInfrastructure abort pollution cannot wash a real diagnostic (#475).
887
+ try {
888
+ const auditorCompliance = await readBoundAuditorComplianceFailure(input.sessionFile);
889
+ if (auditorCompliance !== undefined)
890
+ return resolutionOf(auditorCompliance);
891
+ }
892
+ catch (error) {
893
+ const failure = sessionReadFailure(error, "failed to recover bound auditor failure");
894
+ return resolutionOf({
895
+ cause: "session",
896
+ identity: thrownIdentity(failure),
897
+ diagnostic: failure.message || failure.name,
898
+ });
899
+ }
900
+ try {
901
+ const terminatingFailure = typedFailedTerminatingToolKnownFailure(await readBoundSessionEntries(input.sessionFile));
902
+ if (terminatingFailure !== undefined)
903
+ return resolutionOf(terminatingFailure);
904
+ }
905
+ catch (error) {
906
+ if (!isMissingPathError(error)) {
907
+ const failure = sessionReadFailure(error, "failed to recover typed terminating-tool failure");
908
+ return resolutionOf({
909
+ cause: "session",
910
+ identity: thrownIdentity(failure),
911
+ diagnostic: failure.message || failure.name,
912
+ });
913
+ }
914
+ }
915
+ try {
916
+ const auditorStop = await readBoundAuditorProviderStopFallback(input.sessionFile);
917
+ if (auditorStop !== undefined)
918
+ return resolutionOf(auditorStop);
919
+ }
920
+ catch (error) {
921
+ const failure = sessionReadFailure(error, "failed to recover bound auditor provider stop");
922
+ return resolutionOf({
923
+ cause: "session",
924
+ identity: thrownIdentity(failure),
925
+ diagnostic: failure.message || failure.name,
926
+ });
927
+ }
928
+ // Reviewer axis evidence-children are next: fixed two-axis dispatch fails
929
+ // during activation with only child stops durable. Parent stop remains the
930
+ // fallback; credential is last.
931
+ try {
932
+ const evidenceChildFailure = await readBoundEvidenceChildKnownFailure(input.sessionFile);
933
+ if (evidenceChildFailure !== undefined)
934
+ return resolutionOf(evidenceChildFailure);
935
+ }
936
+ catch (error) {
937
+ const failure = sessionReadFailure(error, "failed to recover bound evidence-child failure");
938
+ return resolutionOf({
939
+ cause: "session",
940
+ identity: thrownIdentity(failure),
941
+ diagnostic: failure.message || failure.name,
942
+ });
943
+ }
944
+ const parentStop = await readSessionProviderStop(input.sessionFile);
945
+ // Typed HTTP observation: ENOENT=absence; other read/parse/shape failures keep real cause.
946
+ // This is the single sidecar read for both knownFailure projection and v1 resume.
947
+ let httpObservation;
948
+ if (input.runDirectory !== undefined) {
949
+ try {
950
+ httpObservation = await readLatestTypedProviderHttpObservation(input.runDirectory);
951
+ }
952
+ catch (error) {
953
+ const failure = error instanceof Error ? error : new Error(String(error));
954
+ return resolutionOf({
955
+ cause: "session",
956
+ identity: thrownIdentity(failure),
957
+ diagnostic: failure.message || failure.name,
958
+ }, { settled: true });
959
+ }
960
+ }
961
+ const typedHttp = {
962
+ settled: input.runDirectory !== undefined,
963
+ ...(httpObservation === undefined ? {} : { observation: httpObservation }),
964
+ };
965
+ if (parentStop === undefined) {
966
+ if (input.credential !== undefined)
967
+ return resolutionOf(input.credential, typedHttp);
968
+ if (httpObservation === undefined)
969
+ return resolutionOf(undefined, typedHttp);
970
+ // Project the HTTP observation's status + provider/source association.
971
+ return resolutionOf(knownFailureFromProviderStop({
972
+ stopReason: "error",
973
+ httpStatus: httpObservation.httpStatus,
974
+ provider: httpObservation.provider,
975
+ }), typedHttp);
976
+ }
977
+ return resolutionOf(knownFailureFromProviderStop({
978
+ ...parentStop,
979
+ ...(httpObservation === undefined
980
+ ? {}
981
+ : {
982
+ httpStatus: httpObservation.httpStatus,
983
+ // Observation association outranks session-configured provider name alone.
984
+ provider: httpObservation.provider,
985
+ }),
986
+ }), typedHttp);
987
+ }
988
+ /** Sole evidence-priority owner for public runners with Soul auditors. */
989
+ export async function resolveAuditedRunnerKnownFailure(input) {
990
+ return (await resolveAuditedRunnerFailureResolution(input)).knownFailure;
991
+ }
992
+ /**
993
+ * v1 resume observation for controlled-failure settlement — at most one sidecar read.
994
+ * Prefer the pre-resolved outcome from resolveAuditedRunnerFailureResolution.
995
+ * Non-absence failures never throw: they return observationReadFailure for the
996
+ * existing controlled-failure → error.json chain.
997
+ */
998
+ export async function resolveControlledFailureResumeObservation(input) {
999
+ if (input.typedHttpObservationSettled === true) {
1000
+ const observation = input.typedHttpObservation;
1001
+ if (observation !== undefined &&
1002
+ observation.httpStatus === 429 &&
1003
+ isV1ResumableProvider(observation.provider)) {
1004
+ return {
1005
+ typedHttp429: { httpStatus: 429, provider: observation.provider },
1006
+ };
1007
+ }
1008
+ return {};
1009
+ }
1010
+ try {
1011
+ const typedHttp429 = await readTypedHttp429Observation(input.runDirectory);
1012
+ return typedHttp429 === undefined ? {} : { typedHttp429 };
1013
+ }
1014
+ catch (error) {
1015
+ const failure = error instanceof Error ? error : new Error(String(error));
1016
+ return {
1017
+ observationReadFailure: {
1018
+ cause: "session",
1019
+ identity: thrownIdentity(failure),
1020
+ diagnostic: failure.message || failure.name,
1021
+ },
1022
+ };
1023
+ }
1024
+ }
1025
+ /** Spread into presentControlledFailure failureInput from one audited resolution. */
1026
+ export function controlledFailureInputFromResolution(resolution) {
1027
+ return {
1028
+ ...(resolution.knownFailure === undefined ? {} : { knownFailure: resolution.knownFailure }),
1029
+ ...(resolution.typedHttpObservationSettled
1030
+ ? {
1031
+ typedHttpObservationSettled: true,
1032
+ ...(resolution.typedHttpObservation === undefined
1033
+ ? {}
1034
+ : { typedHttpObservation: resolution.typedHttpObservation }),
1035
+ }
1036
+ : {}),
1037
+ };
1038
+ }
1039
+ function isRecord(value) {
1040
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1041
+ }
1042
+ function safelyRead(object, key) {
1043
+ try {
1044
+ return { readable: true, value: object[key] };
1045
+ }
1046
+ catch {
1047
+ return { readable: false };
1048
+ }
1049
+ }
1050
+ function auditNoReceiptDecisiveFact(candidate) {
1051
+ const projected = safelyRead(candidate, "auditNoReceipt");
1052
+ if (!projected.readable || projected.value === undefined)
1053
+ return {};
1054
+ try {
1055
+ return { auditNoReceipt: parseNoReceiptLifecycleFacts(projected.value) };
1056
+ }
1057
+ catch {
1058
+ return {};
1059
+ }
1060
+ }
1061
+ /** Typed unreadable audit leg beside accepted parent candidate (ADR 0055 / #675). */
1062
+ function auditUnreadableDecisiveFact(candidate) {
1063
+ const projected = safelyRead(candidate, "auditUnreadable");
1064
+ if (!projected.readable || projected.value === undefined)
1065
+ return {};
1066
+ if (typeof projected.value !== "object" || projected.value === null || Array.isArray(projected.value)) {
1067
+ return { auditUnreadable: projected.value };
1068
+ }
1069
+ return { auditUnreadable: projected.value };
1070
+ }
1071
+ /** Countersign terminal projection — escalate keeps decisionGate; continue keeps fix (#572 / ADR 0074). */
1072
+ function countersignDecisiveFacts(verdict, countersignStatus) {
1073
+ const facts = { countersignStatus };
1074
+ if (countersignStatus === "continue") {
1075
+ const fix = safelyRead(verdict, "fix");
1076
+ if (fix.readable && isRecord(fix.value)) {
1077
+ const summary = safelyRead(fix.value, "summary");
1078
+ if (summary.readable && typeof summary.value === "string") {
1079
+ facts.fixSummary = summary.value;
1080
+ }
1081
+ }
1082
+ }
1083
+ if (countersignStatus === "escalate") {
1084
+ const gate = safelyRead(verdict, "decisionGate");
1085
+ if (gate.readable && isRecord(gate.value)) {
1086
+ const question = safelyRead(gate.value, "question");
1087
+ const options = safelyRead(gate.value, "options");
1088
+ if (question.readable && typeof question.value === "string") {
1089
+ facts.decisionQuestion = question.value;
1090
+ }
1091
+ if (options.readable && Array.isArray(options.value)) {
1092
+ facts.decisionOptions = [...options.value];
1093
+ }
1094
+ }
1095
+ }
1096
+ const note = safelyRead(verdict, "note");
1097
+ if (note.readable && note.value !== undefined)
1098
+ facts.note = note.value;
1099
+ const evidence = safelyRead(verdict, "evidence");
1100
+ if (evidence.readable && evidence.value !== undefined)
1101
+ facts.evidence = evidence.value;
1102
+ return facts;
1103
+ }
1104
+ function judgeDecisiveFacts(verdict, judgeStatus) {
1105
+ const facts = {
1106
+ judgeStatus,
1107
+ ...auditNoReceiptDecisiveFact(verdict),
1108
+ ...auditUnreadableDecisiveFact(verdict),
1109
+ };
1110
+ const statusBase = judgeStatus;
1111
+ if (statusBase === "continue") {
1112
+ const fix = safelyRead(verdict, "fix");
1113
+ if (fix.readable && isRecord(fix.value)) {
1114
+ const summary = safelyRead(fix.value, "summary");
1115
+ if (summary.readable && typeof summary.value === "string") {
1116
+ facts.fixSummary = summary.value;
1117
+ }
1118
+ }
1119
+ const classes = safelyRead(verdict, "classes");
1120
+ if (classes.readable && Array.isArray(classes.value)) {
1121
+ try {
1122
+ facts.classes = classes.value.map((entry) => {
1123
+ if (!isRecord(entry))
1124
+ throw new Error("unreadable Judge class");
1125
+ return {
1126
+ name: entry.name,
1127
+ owner: entry.owner,
1128
+ boundary: entry.boundary,
1129
+ disposition: entry.disposition,
1130
+ };
1131
+ });
1132
+ facts.classCount = classes.value.length;
1133
+ }
1134
+ catch {
1135
+ // Optional class material is omitted as a unit when any row is unreadable.
1136
+ }
1137
+ }
1138
+ }
1139
+ if (statusBase === "escalate") {
1140
+ const gate = safelyRead(verdict, "decisionGate");
1141
+ if (gate.readable && isRecord(gate.value)) {
1142
+ const question = safelyRead(gate.value, "question");
1143
+ const options = safelyRead(gate.value, "options");
1144
+ if (question.readable && typeof question.value === "string") {
1145
+ facts.decisionQuestion = question.value;
1146
+ }
1147
+ if (options.readable && Array.isArray(options.value)) {
1148
+ facts.decisionOptions = [...options.value];
1149
+ }
1150
+ }
1151
+ }
1152
+ const note = safelyRead(verdict, "note");
1153
+ if (note.readable && note.value !== undefined)
1154
+ facts.note = note.value;
1155
+ const evidence = safelyRead(verdict, "evidence");
1156
+ if (evidence.readable && evidence.value !== undefined)
1157
+ facts.evidence = evidence.value;
1158
+ return facts;
1159
+ }
1160
+ function coderDecisiveFacts(output) {
1161
+ const candidate = output;
1162
+ const status = safelyRead(candidate, "status");
1163
+ const facts = {};
1164
+ if (status.readable && typeof status.value === "string")
1165
+ facts.coderStatus = status.value;
1166
+ const statusBase = status.readable && typeof status.value === "string"
1167
+ ? (status.value)
1168
+ : undefined;
1169
+ const remainingScope = safelyRead(candidate, "remainingScope");
1170
+ if (statusBase === "unfinished" && remainingScope.readable && typeof remainingScope.value === "string")
1171
+ facts.remainingScope = remainingScope.value;
1172
+ const reason = safelyRead(candidate, "reason");
1173
+ if (statusBase === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
1174
+ facts.reason = reason.value;
1175
+ }
1176
+ const report = safelyRead(candidate, "report");
1177
+ if (report.readable && typeof report.value === "string")
1178
+ facts.reportPresent = report.value.trim().length > 0;
1179
+ return facts;
1180
+ }
1181
+ function fixerDecisiveFacts(output) {
1182
+ const candidate = output;
1183
+ const status = safelyRead(candidate, "status");
1184
+ const facts = {};
1185
+ if (status.readable && typeof status.value === "string")
1186
+ facts.fixerStatus = status.value;
1187
+ const statusBase = status.readable && typeof status.value === "string"
1188
+ ? (status.value)
1189
+ : undefined;
1190
+ const remainingScope = safelyRead(candidate, "remainingScope");
1191
+ if ((statusBase === "unfinished" || statusBase === "refused") && remainingScope.readable && typeof remainingScope.value === "string")
1192
+ facts.remainingScope = remainingScope.value;
1193
+ const reason = safelyRead(candidate, "reason");
1194
+ if (statusBase === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
1195
+ facts.reason = reason.value;
1196
+ }
1197
+ const blockerRead = safelyRead(candidate, "blocker");
1198
+ if (statusBase === "refused" && blockerRead.readable && isRecord(blockerRead.value)) {
1199
+ const cause = safelyRead(blockerRead.value, "cause");
1200
+ if (cause.readable && typeof cause.value === "string")
1201
+ facts.blockerCause = cause.value;
1202
+ const prerequisiteId = safelyRead(blockerRead.value, "prerequisiteId");
1203
+ if (cause.readable && cause.value === "prerequisite_unmet" && prerequisiteId.readable && typeof prerequisiteId.value === "string")
1204
+ facts.prerequisiteId = prerequisiteId.value;
1205
+ }
1206
+ const classResults = safelyRead(candidate, "classResults");
1207
+ if (classResults.readable && Array.isArray(classResults.value)) {
1208
+ const rows = [];
1209
+ const blockers = [];
1210
+ try {
1211
+ for (const entry of classResults.value) {
1212
+ if (!isRecord(entry))
1213
+ throw new Error("unreadable class result");
1214
+ const name = safelyRead(entry, "name");
1215
+ const disposition = safelyRead(entry, "disposition");
1216
+ if (!name.readable || !disposition.readable)
1217
+ throw new Error("unreadable class result");
1218
+ rows.push({ name: name.value, disposition: disposition.value });
1219
+ const blocker = safelyRead(entry, "blocker");
1220
+ if (disposition.value === "refused" && blocker.readable && isRecord(blocker.value))
1221
+ blockers.push(blocker.value);
1222
+ }
1223
+ facts.classResultCount = rows.length;
1224
+ facts.classDispositions = rows;
1225
+ const causes = blockers.flatMap((blocker) => {
1226
+ const cause = safelyRead(blocker, "cause");
1227
+ return cause.readable && typeof cause.value === "string" ? [cause.value] : [];
1228
+ });
1229
+ if (causes.length > 0)
1230
+ facts.blockerCauses = causes;
1231
+ const prerequisiteIds = blockers.flatMap((blocker) => {
1232
+ const cause = safelyRead(blocker, "cause");
1233
+ const id = safelyRead(blocker, "prerequisiteId");
1234
+ return cause.readable && cause.value === "prerequisite_unmet" && id.readable && typeof id.value === "string" ? [id.value] : [];
1235
+ });
1236
+ if (prerequisiteIds.length > 0)
1237
+ facts.prerequisiteIds = prerequisiteIds;
1238
+ }
1239
+ catch {
1240
+ // Optional class projection is omitted as a unit when any row is unreadable.
1241
+ }
1242
+ }
1243
+ const report = safelyRead(candidate, "report");
1244
+ if (report.readable && typeof report.value === "string")
1245
+ facts.reportPresent = report.value.trim().length > 0;
1246
+ return facts;
1247
+ }
1248
+ function collectorDecisiveFacts(receipt) {
1249
+ const candidate = receipt;
1250
+ const facts = {};
1251
+ for (const key of ["repository", "prNumber", "targetHead", "manifestDigest"]) {
1252
+ const value = safelyRead(candidate, key);
1253
+ if (value.readable && value.value !== undefined)
1254
+ facts[key] = value.value;
1255
+ }
1256
+ const groups = safelyRead(candidate, "groups");
1257
+ if (groups.readable && Array.isArray(groups.value)) {
1258
+ try {
1259
+ facts.groups = groups.value.map((group) => {
1260
+ if (!isRecord(group))
1261
+ throw new Error("unreadable Collector group");
1262
+ const identity = safelyRead(group, "identity");
1263
+ const attendance = safelyRead(group, "attendance");
1264
+ const materials = safelyRead(group, "materials");
1265
+ const findings = safelyRead(group, "findings");
1266
+ if (!identity.readable || !attendance.readable ||
1267
+ !materials.readable || !Array.isArray(materials.value) ||
1268
+ !findings.readable || !Array.isArray(findings.value)) {
1269
+ throw new Error("unreadable Collector group");
1270
+ }
1271
+ return {
1272
+ identity: identity.value,
1273
+ attendance: attendance.value,
1274
+ materialCount: materials.value.length,
1275
+ findingCount: findings.value.length,
1276
+ };
1277
+ });
1278
+ }
1279
+ catch { /* omit unreadable optional projection */ }
1280
+ }
1281
+ return facts;
1282
+ }
1283
+ function doctorDecisiveFacts(output) {
1284
+ const candidate = output;
1285
+ const status = safelyRead(candidate, "status");
1286
+ const facts = {
1287
+ ...auditNoReceiptDecisiveFact(candidate),
1288
+ ...auditUnreadableDecisiveFact(candidate),
1289
+ };
1290
+ if (status.readable && typeof status.value === "string")
1291
+ facts.doctorStatus = status.value;
1292
+ const statusBase = status.readable && typeof status.value === "string"
1293
+ ? (status.value)
1294
+ : undefined;
1295
+ if (statusBase === "refused") {
1296
+ const reason = safelyRead(candidate, "reason");
1297
+ if (reason.readable && reason.value !== undefined)
1298
+ facts.reason = reason.value;
1299
+ const missing = safelyRead(candidate, "missingEvidence");
1300
+ if (missing.readable && Array.isArray(missing.value))
1301
+ facts.missingEvidenceCount = missing.value.length;
1302
+ return facts;
1303
+ }
1304
+ const caseValue = safelyRead(candidate, "case");
1305
+ if (caseValue.readable && isRecord(caseValue.value)) {
1306
+ const issueNumber = safelyRead(caseValue.value, "issueNumber");
1307
+ const runsPath = safelyRead(caseValue.value, "runsPath");
1308
+ if (issueNumber.readable && issueNumber.value !== undefined)
1309
+ facts.issueNumber = issueNumber.value;
1310
+ if (runsPath.readable && runsPath.value !== undefined)
1311
+ facts.runsPath = runsPath.value;
1312
+ }
1313
+ const findings = safelyRead(candidate, "findings");
1314
+ if (findings.readable && Array.isArray(findings.value))
1315
+ facts.findingsCount = findings.value.length;
1316
+ return facts;
1317
+ }
1318
+ function reviewerAxes(value) {
1319
+ if (!isRecord(value))
1320
+ return [];
1321
+ return ["standards", "spec"].filter((axis) => {
1322
+ const projected = safelyRead(value, axis);
1323
+ return projected.readable && projected.value !== undefined;
1324
+ });
1325
+ }
1326
+ function reviewerDecisiveFacts(output) {
1327
+ const candidate = output;
1328
+ const status = safelyRead(candidate, "status");
1329
+ const outcomes = safelyRead(candidate, "outcomes");
1330
+ const reports = safelyRead(candidate, "reports");
1331
+ const amendments = safelyRead(candidate, "amendments");
1332
+ const axes = reviewerAxes(outcomes.readable ? outcomes.value : undefined);
1333
+ const reportAxes = reviewerAxes(reports.readable ? reports.value : undefined);
1334
+ // Typed presence only — never copy amendment prose into public facts.
1335
+ const amendmentAxes = reviewerAxes(amendments.readable ? amendments.value : undefined);
1336
+ const acceptedBatch = safelyRead(candidate, "acceptedBatch");
1337
+ const specDisposition = safelyRead(candidate, "specDisposition");
1338
+ const facts = {
1339
+ axes,
1340
+ reportAxes,
1341
+ amendmentAxes,
1342
+ acceptedBatchPresent: acceptedBatch.readable && acceptedBatch.value !== undefined,
1343
+ ...auditNoReceiptDecisiveFact(candidate),
1344
+ };
1345
+ if (status.readable && typeof status.value === "string")
1346
+ facts.reviewerStatus = status.value;
1347
+ if (specDisposition.readable &&
1348
+ (specDisposition.value === "launched" || specDisposition.value === "skipped-missing")) {
1349
+ facts.specDisposition = specDisposition.value;
1350
+ }
1351
+ const diagnostic = safelyRead(candidate, "diagnostic");
1352
+ const statusBase = status.readable && typeof status.value === "string"
1353
+ ? (status.value)
1354
+ : undefined;
1355
+ if (statusBase === "refused" && diagnostic.readable) {
1356
+ facts.diagnosticPresent = typeof diagnostic.value === "string" && diagnostic.value.trim().length > 0;
1357
+ }
1358
+ return facts;
1359
+ }
1360
+ /**
1361
+ * ADR 0037: a shape-valid Collector receipt may still name the wrong live target.
1362
+ * Public success binds receipt identity to this admitted repository/PR/request manifest
1363
+ * at the existing settlement seam — not a second receipt factory or validator.
1364
+ */
1365
+ function collectorReceiptBindingFailure(diagnostic) {
1366
+ const error = new Error(diagnostic);
1367
+ error.name = "CollectorReceiptBindingError";
1368
+ error.knownCause = "output";
1369
+ return error;
1370
+ }
1371
+ function toolResultText(message) {
1372
+ const content = message.content;
1373
+ if (typeof content === "string")
1374
+ return content.trim();
1375
+ if (!Array.isArray(content))
1376
+ return "";
1377
+ return content
1378
+ .map((part) => {
1379
+ if (typeof part === "object" &&
1380
+ part !== null &&
1381
+ !Array.isArray(part) &&
1382
+ typeof part.text === "string") {
1383
+ return part.text;
1384
+ }
1385
+ return "";
1386
+ })
1387
+ .join("")
1388
+ .trim();
1389
+ }
1390
+ function boundErroredToolCandidate(entries, resultIndex, message, toolName) {
1391
+ if (message.toolName !== toolName || message.isError !== true)
1392
+ return undefined;
1393
+ const bound = boundRoleToolCallForResult(entries, resultIndex, message, toolName);
1394
+ const diagnostic = toolResultText(message);
1395
+ return bound === undefined || diagnostic === ""
1396
+ ? undefined
1397
+ : { candidate: bound.candidate, diagnostic, callIndex: bound.callIndex };
1398
+ }
1399
+ /** Collector operational tools that fail closed via host infrastructure abort. */
1400
+ const COLLECTOR_INFRASTRUCTURE_TOOLS = new Set([
1401
+ COLLECTOR_OBSERVE_TOOL,
1402
+ COLLECTOR_READ_TOOL,
1403
+ COLLECTOR_REQUEST_TOOL,
1404
+ COLLECTOR_WAIT_TOOL,
1405
+ ]);
1406
+ const COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC = {
1407
+ matchTool: (toolName) => COLLECTOR_INFRASTRUCTURE_TOOLS.has(toolName),
1408
+ cause: "activation",
1409
+ identityName: "CollectorInfrastructureError",
1410
+ // read alone has a correctable rejection mode (unknown/non-openable pointers);
1411
+ // only its typed infrastructure-failure fact counts as a real host failure.
1412
+ requireInfrastructureFact: (toolName) => toolName === COLLECTOR_READ_TOOL,
1413
+ };
1414
+ const ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC = {
1415
+ matchTool: (toolName) => toolName === ENGINE_DETOUR_TOOL_NAME,
1416
+ cause: "output",
1417
+ identityName: "EngineDetourInfrastructureError",
1418
+ };
1419
+ /**
1420
+ * Prefer a real infrastructure tool failure already on the session principal
1421
+ * over a later secondary provider-stop (failure-honesty).
1422
+ * Tool match + cause + identity are call-site parameters — one extraction body.
1423
+ */
1424
+ function extractInfrastructureToolFailure(entries, spec) {
1425
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
1426
+ const entry = entries[i];
1427
+ if (entry?.type !== "message")
1428
+ continue;
1429
+ const message = entry.message;
1430
+ if (message?.role !== "toolResult")
1431
+ continue;
1432
+ if (message.isError !== true)
1433
+ continue;
1434
+ if (typeof message.toolName !== "string" ||
1435
+ !spec.matchTool(message.toolName)) {
1436
+ continue;
1437
+ }
1438
+ if (spec.requireInfrastructureFact?.(message.toolName) === true) {
1439
+ if (!hasNavigatorInfrastructureFailureBase(message.details))
1440
+ continue;
1441
+ }
1442
+ const diagnostic = toolResultText(message);
1443
+ if (diagnostic.length === 0)
1444
+ continue;
1445
+ return {
1446
+ cause: spec.cause,
1447
+ diagnostic,
1448
+ identity: { name: spec.identityName },
1449
+ };
1450
+ }
1451
+ return undefined;
1452
+ }
1453
+ /**
1454
+ * Read the bound session principal for a parameterized infrastructure tool failure.
1455
+ * `currentAttemptOnly` bounds the reverse scan to the latest top-level user turn
1456
+ * so a prior attempt's residual cannot mask the current failure (#633).
1457
+ */
1458
+ async function readInfrastructureToolFailure(sessionFile, spec, options = {}) {
1459
+ try {
1460
+ let entries = await readBoundSessionEntries(sessionFile);
1461
+ if (options.currentAttemptOnly === true) {
1462
+ entries = entries.slice(currentAttemptStartIndex(entries));
1463
+ }
1464
+ return extractInfrastructureToolFailure(entries, spec);
1465
+ }
1466
+ catch {
1467
+ return undefined;
1468
+ }
1469
+ }
1470
+ /**
1471
+ * Prefer a real Collector infrastructure tool failure already on the session
1472
+ * principal over a later secondary provider-stop (failure-honesty).
1473
+ * Observe/request/wait host failures keep their diagnostic identity (e.g. HTTP 404).
1474
+ */
1475
+ export function extractCollectorInfrastructureFailure(entries) {
1476
+ return extractInfrastructureToolFailure(entries, COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC);
1477
+ }
1478
+ /** Read the bound session principal for a Collector infrastructure tool failure. */
1479
+ export async function readCollectorInfrastructureFailure(sessionFile) {
1480
+ return readInfrastructureToolFailure(sessionFile, COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC,
1481
+ // Multi-attempt resume: only a current-attempt infrastructure failure
1482
+ // may preempt the current failure cause (#633).
1483
+ { currentAttemptOnly: true });
1484
+ }
1485
+ /**
1486
+ * Prefer a real engine-detour infrastructure tool failure already on the session
1487
+ * principal over a later secondary provider-stop (failure-honesty / #357 T2).
1488
+ * Cause stays `output` — labor leg failed before accepted typed Receipt.
1489
+ */
1490
+ export function extractEngineDetourInfrastructureFailure(entries) {
1491
+ return extractInfrastructureToolFailure(entries, ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC);
1492
+ }
1493
+ /** Read the bound session principal for an engine-detour infrastructure failure. */
1494
+ export async function readEngineDetourInfrastructureFailure(sessionFile) {
1495
+ return readInfrastructureToolFailure(sessionFile, ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC);
1496
+ }
1497
+ /**
1498
+ * Compare a validated receipt with the admitted Collector invocation identity.
1499
+ * Throws a typed output failure when any identity field mismatches.
1500
+ */
1501
+ export function assertCollectorReceiptMatchesAdmitted(receipt, admitted) {
1502
+ if (receipt.repository !== admitted.repository.canonical) {
1503
+ throw collectorReceiptBindingFailure(`Collector receipt repository "${receipt.repository}" does not match admitted repository "${admitted.repository.canonical}"`);
1504
+ }
1505
+ if (receipt.prNumber !== admitted.prNumber) {
1506
+ throw collectorReceiptBindingFailure(`Collector receipt prNumber ${receipt.prNumber} does not match admitted prNumber ${admitted.prNumber}`);
1507
+ }
1508
+ if (receipt.manifestDigest !== admitted.manifestDigest) {
1509
+ throw collectorReceiptBindingFailure(`Collector receipt manifestDigest does not match admitted manifestDigest`);
1510
+ }
1511
+ }
1512
+ function auditToolNameForRole(role) {
1513
+ switch (role) {
1514
+ case "judge":
1515
+ return JUDGE_AUDIT_TOOL_NAME;
1516
+ case "doctor":
1517
+ return DOCTOR_AUDIT_TOOL_NAME;
1518
+ }
1519
+ }
1520
+ function boundRoleToolCallForResult(entries, resultIndex, message, outputToolName) {
1521
+ const callId = message.toolCallId;
1522
+ if (typeof callId !== "string" || callId.trim() === "")
1523
+ return undefined;
1524
+ const calls = [];
1525
+ let resultCount = 0;
1526
+ let matchingResultIndex = -1;
1527
+ for (let index = 0; index < entries.length; index += 1) {
1528
+ const candidateMessage = entries[index]?.message;
1529
+ if (candidateMessage?.role === "assistant" &&
1530
+ Array.isArray(candidateMessage.content)) {
1531
+ for (const part of candidateMessage.content) {
1532
+ if (!isRecord(part) || part.type !== "toolCall" || part.id !== callId) {
1533
+ continue;
1534
+ }
1535
+ if (part.name !== outputToolName)
1536
+ return undefined;
1537
+ calls.push({ callIndex: index, candidate: part.arguments });
1538
+ }
1539
+ }
1540
+ if (candidateMessage?.role === "toolResult" &&
1541
+ candidateMessage.toolCallId === callId) {
1542
+ resultCount += 1;
1543
+ if (candidateMessage.toolName !== outputToolName)
1544
+ return undefined;
1545
+ matchingResultIndex = index;
1546
+ }
1547
+ }
1548
+ // A binding is an event-bound one-to-one relation, not a reverse lookup of
1549
+ // whichever result happens to be last in the session.
1550
+ return calls.length === 1 && resultCount === 1 && matchingResultIndex === resultIndex
1551
+ && calls[0].callIndex < resultIndex
1552
+ ? calls[0]
1553
+ : undefined;
1554
+ }
1555
+ function sameAuditValue(left, right) {
1556
+ if (Object.is(left, right))
1557
+ return true;
1558
+ if (Array.isArray(left) && Array.isArray(right)) {
1559
+ return left.length === right.length && left.every((value, index) => sameAuditValue(value, right[index]));
1560
+ }
1561
+ if (isRecord(left) && isRecord(right)) {
1562
+ const leftKeys = Object.keys(left);
1563
+ const rightKeys = Object.keys(right);
1564
+ return leftKeys.length === rightKeys.length &&
1565
+ leftKeys.every((key) => Object.hasOwn(right, key) && sameAuditValue(left[key], right[key]));
1566
+ }
1567
+ return false;
1568
+ }
1569
+ /** Snapshot the exact enumerable string face that final Terminal projection uses. */
1570
+ function snapshotAuditDetails(details) {
1571
+ const snapshot = Object.create(null);
1572
+ for (const key of Object.keys(details)) {
1573
+ Object.defineProperty(snapshot, key, {
1574
+ value: details[key],
1575
+ enumerable: true,
1576
+ configurable: true,
1577
+ writable: true,
1578
+ });
1579
+ }
1580
+ return snapshot;
1581
+ }
1582
+ /**
1583
+ * Bind the public escalation face to the one retained response that sits inside
1584
+ * the same role output call/result interval. A `kind` field alone is never a
1585
+ * terminal identity; the retained response must be this seat's real escalate
1586
+ * decision and its projected audit-owned fields must agree with it.
1587
+ */
1588
+ function boundAuditEscalationForResult(entries, resultIndex, message, role, outputToolName) {
1589
+ const roleCall = boundRoleToolCallForResult(entries, resultIndex, message, outputToolName);
1590
+ if (roleCall === undefined)
1591
+ return undefined;
1592
+ const retained = boundRetainedAuditResponse(entries, roleCall.callIndex, resultIndex, auditToolNameForRole(role));
1593
+ if (retained === undefined)
1594
+ return undefined;
1595
+ try {
1596
+ const decision = readComplianceCandidate(retained.candidate);
1597
+ if (decision.status !== "escalate")
1598
+ return undefined;
1599
+ const details = message.details;
1600
+ if (!isAuditEscalationResult(details) || !isRecord(details))
1601
+ return undefined;
1602
+ // Read the public face exactly once. Besides making key enumeration and
1603
+ // getters fail closed, this prevents a stateful accessor from authenticating
1604
+ // one value and yielding another during final Terminal projection.
1605
+ const projectedDetails = snapshotAuditDetails(details);
1606
+ const hasDecisionConflicts = Object.hasOwn(decision, "conflicts");
1607
+ const hasDetailsConflicts = Object.hasOwn(projectedDetails, "conflicts");
1608
+ if (hasDecisionConflicts !== hasDetailsConflicts)
1609
+ return undefined;
1610
+ if (hasDecisionConflicts && !sameAuditValue(projectedDetails.conflicts, decision.conflicts))
1611
+ return undefined;
1612
+ const hasDecisionGate = Object.hasOwn(decision, "decisionGate");
1613
+ const hasDetailsGate = Object.hasOwn(projectedDetails, "auditDecisionGate");
1614
+ if (hasDecisionGate !== hasDetailsGate)
1615
+ return undefined;
1616
+ if (hasDecisionGate && !sameAuditValue(projectedDetails.auditDecisionGate, decision.decisionGate))
1617
+ return undefined;
1618
+ return { decision, details: projectedDetails };
1619
+ }
1620
+ catch {
1621
+ // Retained/public own-key enumeration, property reads, recursive equality,
1622
+ // and projection are all untrusted session evidence.
1623
+ return undefined;
1624
+ }
1625
+ }
1626
+ function isUnboundAuditEscalationFace(details) {
1627
+ try {
1628
+ if (isAuditEscalationResult(details))
1629
+ return true;
1630
+ }
1631
+ catch {
1632
+ // Hostile access is not authentic escalation evidence.
1633
+ }
1634
+ if (!isRecord(details))
1635
+ return false;
1636
+ const kind = safelyRead(details, "kind");
1637
+ return kind.readable && kind.value === "audit_escalation";
1638
+ }
1639
+ function boundRetainedAuditResponse(entries, callIndex, resultIndex, auditToolName) {
1640
+ const matches = [];
1641
+ for (let index = callIndex + 1; index < resultIndex; index += 1) {
1642
+ const entry = entries[index];
1643
+ if (entry?.type !== "custom" || entry.customType !== COMPLIANCE_RESPONSE_ENTRY_TYPE) {
1644
+ continue;
1645
+ }
1646
+ if (!isRecord(entry.data) || !isRecord(entry.data.response))
1647
+ continue;
1648
+ const response = entry.data.response;
1649
+ if (!Array.isArray(response.content))
1650
+ continue;
1651
+ const calls = response.content.filter((part) => isRecord(part) && part.type === "toolCall");
1652
+ if (calls.length !== 1 || calls[0]?.name !== auditToolName)
1653
+ continue;
1654
+ matches.push({ candidate: calls[0]?.arguments });
1655
+ }
1656
+ // Unique seat-bound match binds even when multi-turn investigation retained
1657
+ // intermediate non-decision responses in the same call/result interval.
1658
+ return matches.length === 1 ? matches[0] : undefined;
1659
+ }
1660
+ /**
1661
+ * #419 per-attempt process history. 史必追加,指针可覆盖;指针可以覆盖的前提是史已落。
1662
+ * Reuses the run session principal's append-only JSONL custom-entry shape
1663
+ * (plain custom entries are state records and never enter LLM context), so no
1664
+ * second ledger mechanism is introduced.
1665
+ */
1666
+ export const ATTEMPT_HISTORY_ENTRY_TYPE = "ak_run_attempt_history";
1667
+ /**
1668
+ * Append one attempt's complete result to the run's session principal.
1669
+ * Append failure throws — callers must not overwrite a pointer artifact when
1670
+ * the history entry backing the overwrite did not land (fail closed).
1671
+ */
1672
+ export async function appendRunAttemptHistory(source, outcome) {
1673
+ const entries = await readBoundSessionEntries(source.sessionFile);
1674
+ let parentId = null;
1675
+ let priorEntries = 0;
1676
+ for (const entry of entries) {
1677
+ if (typeof entry.id === "string" && entry.type !== "session")
1678
+ parentId = entry.id;
1679
+ if (entry.type === "custom" &&
1680
+ entry.customType === ATTEMPT_HISTORY_ENTRY_TYPE) {
1681
+ priorEntries += 1;
1682
+ }
1683
+ }
1684
+ const timestamp = new Date().toISOString();
1685
+ const attemptData = {
1686
+ sequence: priorEntries + 1,
1687
+ role: source.role,
1688
+ runId: source.runId,
1689
+ recordedAt: timestamp,
1690
+ outcome,
1691
+ };
1692
+ const line = `${JSON.stringify({
1693
+ type: "custom",
1694
+ customType: ATTEMPT_HISTORY_ENTRY_TYPE,
1695
+ data: attemptData,
1696
+ id: randomUUID(),
1697
+ parentId,
1698
+ timestamp,
1699
+ })}\n`;
1700
+ await appendFile(source.sessionFile, line, "utf8");
1701
+ try {
1702
+ sitianReport({
1703
+ level: "event",
1704
+ kind: "attempt-history",
1705
+ subject: { runId: source.runId },
1706
+ sessionParent: source.sessionFile,
1707
+ payload: {
1708
+ type: ATTEMPT_HISTORY_ENTRY_TYPE,
1709
+ ...attemptData,
1710
+ },
1711
+ source: "settlement",
1712
+ });
1713
+ }
1714
+ catch { }
1715
+ }
1716
+ function navigatorPhaseValue(value) {
1717
+ if (value === "plan" || value === "apply")
1718
+ return value;
1719
+ return null;
1720
+ }
1721
+ /**
1722
+ * Minimal attendance provenance against the bound marker (ADR 0043).
1723
+ * Keep only invocationId + post-terminal ordering. Runtime-self-produced
1724
+ * role/phase/subject/version are not re-reconciled here (ADR 0042).
1725
+ */
1726
+ function navigatorAttendanceCorrelatedWithBoundMarker(details, attendanceIndex, terminalIndex, marker) {
1727
+ if (attendanceIndex <= terminalIndex)
1728
+ return false;
1729
+ // Exact current invocation token is the bound marker principal.
1730
+ if (details.invocationId !== marker.invocationId)
1731
+ return false;
1732
+ return true;
1733
+ }
1734
+ function parseNavigatorAttendanceDetails(details) {
1735
+ const disposition = details.disposition;
1736
+ const advisoryDiagnostic = typeof details.routePlaybookReadFailure === "string"
1737
+ ? { advisoryDiagnostic: details.routePlaybookReadFailure }
1738
+ : {};
1739
+ if (disposition === "recommendation") {
1740
+ const next = details.next;
1741
+ if (!isRecord(next) || typeof next.role !== "string") {
1742
+ return {
1743
+ disposition: "unavailable",
1744
+ source: "unknown",
1745
+ reason: "navigator recommendation missing typed next role",
1746
+ };
1747
+ }
1748
+ const reason = typeof details.reason === "string" ? details.reason : "";
1749
+ const route = Array.isArray(details.route)
1750
+ ? details.route
1751
+ .filter(isRecord)
1752
+ .map((target) => ({
1753
+ role: String(target.role),
1754
+ phase: navigatorPhaseValue(target.phase),
1755
+ }))
1756
+ : undefined;
1757
+ return recommendationNavigatorFact({
1758
+ ...advisoryDiagnostic,
1759
+ next: {
1760
+ role: next.role,
1761
+ phase: navigatorPhaseValue(next.phase),
1762
+ },
1763
+ reason,
1764
+ ...(route === undefined ? {} : { route }),
1765
+ ...(typeof details.command === "string"
1766
+ ? { modelCommand: details.command }
1767
+ : {}),
1768
+ });
1769
+ }
1770
+ if (disposition === "unavailable") {
1771
+ return {
1772
+ disposition: "unavailable",
1773
+ ...advisoryDiagnostic,
1774
+ source: typeof details.unavailableSource === "string"
1775
+ ? details.unavailableSource
1776
+ : "unknown",
1777
+ reason: typeof details.unavailableReason === "string"
1778
+ ? details.unavailableReason
1779
+ : "Navigator unavailable",
1780
+ };
1781
+ }
1782
+ // arrival and legacy silence both mean affirmative lawful no next-role advice.
1783
+ if (disposition === "no-advice" || disposition === "arrival" || disposition === "silence") {
1784
+ return {
1785
+ disposition: "no-advice",
1786
+ ...advisoryDiagnostic,
1787
+ };
1788
+ }
1789
+ return {
1790
+ disposition: "unavailable",
1791
+ source: "unknown",
1792
+ reason: "Navigator attendance disposition is unparseable",
1793
+ };
1794
+ }
1795
+ /**
1796
+ * Project direct and historical paired gate rounds onto the public Terminal.
1797
+ * actualSeats derive only from accepted receipts, never expected/missing seats.
1798
+ */
1799
+ export function projectTerminalGateFact(rounds) {
1800
+ if (rounds.length === 0)
1801
+ return undefined;
1802
+ const seen = new Set();
1803
+ for (const round of rounds) {
1804
+ if (round.origin.kind === "historical_dispatch")
1805
+ seen.add("gatekeeper");
1806
+ seen.add(round.officer);
1807
+ }
1808
+ const actualSeats = ["gatekeeper", "inspector", "notary"].filter((seat) => seen.has(seat));
1809
+ return {
1810
+ actualSeats,
1811
+ rounds: rounds.map((round) => ({
1812
+ roundIndex: round.roundIndex,
1813
+ dispatch: round.origin.kind === "direct"
1814
+ ? { kind: "direct", officer: round.officer }
1815
+ : {
1816
+ kind: "historical_dispatch",
1817
+ officer: round.officer,
1818
+ ...(round.origin.reason === undefined
1819
+ ? {}
1820
+ : { reason: round.origin.reason }),
1821
+ },
1822
+ officer: {
1823
+ seat: round.officer,
1824
+ status: round.status,
1825
+ findings: round.findings,
1826
+ },
1827
+ })),
1828
+ };
1829
+ }
1830
+ /**
1831
+ * Read gate facts from the run's session/auditor-roles nest (#446/#478).
1832
+ * Missing directories → undefined (no-gate zero change).
1833
+ * Damaged discovered volumes propagate — never wash to "no gate".
1834
+ */
1835
+ export async function extractGateFactFromSessionDirectory(sessionDirectory, options = {}) {
1836
+ const directories = [join(sessionDirectory, "auditor-roles")];
1837
+ const parentSessionFile = options.parentSessionFile ?? join(sessionDirectory, "session.jsonl");
1838
+ const rounds = await readAnalystGateCyclesFromAuditorRoles(directories, {
1839
+ parentSessionFile,
1840
+ });
1841
+ return projectTerminalGateFact(rounds);
1842
+ }
1843
+ /**
1844
+ * Attach optional gate projection onto a settled Terminal base.
1845
+ * Shared by every settle path so auditor-roles is scanned once here only.
1846
+ * `runId` is not required — resumable failures omit it by contract.
1847
+ * Gate read damage propagates with its real identity (never washed to no-gate
1848
+ * or swallowed); callers that already hold a controlled failure still surface the
1849
+ * JSONL/session cause rather than pretend the gate was absent.
1850
+ */
1851
+ async function withOptionalGateProjection(base, sessionDirectory, gateContext = {}) {
1852
+ // A gate transport failure is already represented by typed evidence and has no
1853
+ // accepted gate cycle to project. Re-reading that rejected receipt as an
1854
+ // accepted cycle would replace the original failure with a projection error.
1855
+ const secondaryEvidence = base.roleOutcome.kind === "failure"
1856
+ ? base.roleOutcome.decisiveFacts.secondaryEvidence
1857
+ : undefined;
1858
+ if (isRecord(secondaryEvidence)
1859
+ && secondaryEvidence.kind === "role_infrastructure_failure"
1860
+ && (secondaryEvidence.stage === "gatekeeper"
1861
+ || secondaryEvidence.stage === "inspector"
1862
+ || secondaryEvidence.stage === "notary"))
1863
+ return base;
1864
+ // Defaults live solely in extractGateFactFromSessionDirectory — do not re-derive.
1865
+ const gate = await extractGateFactFromSessionDirectory(sessionDirectory, gateContext);
1866
+ return gate === undefined ? base : { ...base, gate };
1867
+ }
1868
+ export function extractNavigatorFact(entries) {
1869
+ // Affirmative attendance only. Missing / uncorrelated / unparseable is never no-advice.
1870
+ // Minimal provenance: latest durable terminal + nearest preceding marker +
1871
+ // invocationId + post-terminal order. Marker↔terminal cardinality belongs to
1872
+ // receipt settlement (isReceiptSettlementBindingClear), not attendance extraction.
1873
+ const terminal = findLatestDurablePackagedRoleTerminal(entries);
1874
+ if (terminal === undefined) {
1875
+ return {
1876
+ disposition: "unavailable",
1877
+ source: "unknown",
1878
+ reason: "Navigator attendance has no durable packaged role terminal",
1879
+ };
1880
+ }
1881
+ let markerIndex = -1;
1882
+ for (let i = terminal.index - 1; i >= 0; i -= 1) {
1883
+ const entry = entries[i];
1884
+ if (entry?.type === "custom" && entry.customType === NAVIGATOR_INVOCATION_ENTRY) {
1885
+ markerIndex = i;
1886
+ break;
1887
+ }
1888
+ }
1889
+ if (markerIndex < 0) {
1890
+ return {
1891
+ disposition: "unavailable",
1892
+ source: "unknown",
1893
+ reason: "Navigator attendance is uncorrelated with session invocation facts",
1894
+ };
1895
+ }
1896
+ const marker = parseInvocationMarkerIdentity(entries[markerIndex]?.data);
1897
+ if (marker === undefined) {
1898
+ return {
1899
+ disposition: "unavailable",
1900
+ source: "unknown",
1901
+ reason: "Navigator attendance is uncorrelated with session invocation facts",
1902
+ };
1903
+ }
1904
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
1905
+ const entry = entries[i];
1906
+ if (entry?.type === "custom_message" && entry.customType === "ak-navigator-attendance") {
1907
+ const details = entry.message?.details ?? entry.details;
1908
+ if (!isRecord(details)) {
1909
+ return {
1910
+ disposition: "unavailable",
1911
+ source: "unknown",
1912
+ reason: "Navigator attendance is unparseable",
1913
+ };
1914
+ }
1915
+ if (!navigatorAttendanceCorrelatedWithBoundMarker(details, i, terminal.index, marker)) {
1916
+ return {
1917
+ disposition: "unavailable",
1918
+ source: "unknown",
1919
+ reason: "Navigator attendance is uncorrelated with session invocation facts",
1920
+ };
1921
+ }
1922
+ return parseNavigatorAttendanceDetails(details);
1923
+ }
1924
+ }
1925
+ // Absence is not successful no-advice — require affirmative typed attendance.
1926
+ return {
1927
+ disposition: "unavailable",
1928
+ source: "unknown",
1929
+ reason: "Navigator attendance is missing from the session",
1930
+ };
1931
+ }
1932
+ /**
1933
+ * Exact-session Navigator fact for failure Terminal settlement.
1934
+ * Never infers no-advice from omission; session read failures stay typed unavailable
1935
+ * so the controlled-failure Terminal itself still settles.
1936
+ */
1937
+ async function extractNavigatorFactFromAdmittedSession(sessionFile) {
1938
+ try {
1939
+ const entries = await readBoundSessionEntries(sessionFile);
1940
+ return extractNavigatorFact(entries);
1941
+ }
1942
+ catch (error) {
1943
+ if (isMissingPathError(error)) {
1944
+ return {
1945
+ disposition: "unavailable",
1946
+ source: "unknown",
1947
+ reason: "Navigator attendance is missing from the session",
1948
+ };
1949
+ }
1950
+ return {
1951
+ disposition: "unavailable",
1952
+ source: "unknown",
1953
+ reason: "Navigator attendance is unavailable because the session could not be read",
1954
+ };
1955
+ }
1956
+ }
1957
+ export async function publishJudgeArtifacts(admitted, roleOutcome, coordinates) {
1958
+ // #419: history first — report/evidence stay last-write-wins views only
1959
+ // because every attempt's complete result has already been appended.
1960
+ await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
1961
+ const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
1962
+ const reportPath = join(artifactsDir, "report.json");
1963
+ const evidencePath = join(artifactsDir, "evidence.json");
1964
+ await writeFile(reportPath, `${JSON.stringify({
1965
+ role: "judge",
1966
+ runId: admitted.runId,
1967
+ outcome: roleOutcome,
1968
+ }, null, 2)}\n`, "utf8");
1969
+ await writeFile(evidencePath, `${JSON.stringify({
1970
+ runId: admitted.runId,
1971
+ sessionDirectory: coordinates.sessionDirectory,
1972
+ sessionFile: coordinates.sessionFile,
1973
+ admittedRequestPath: admitted.admittedRequestPath,
1974
+ attachments: admitted.attachments.map((a) => ({
1975
+ provenancePath: a.provenancePath,
1976
+ frozenPath: a.frozenPath,
1977
+ sha256: a.sha256,
1978
+ byteLength: a.byteLength,
1979
+ })),
1980
+ }, null, 2)}\n`, "utf8");
1981
+ return [
1982
+ { kind: "report", path: reportPath },
1983
+ { kind: "evidence", path: evidencePath },
1984
+ ];
1985
+ }
1986
+ /**
1987
+ * Publish lawful Coder success Artifacts on the shared #106 success interface.
1988
+ * Evidence records package method provenance without ambient home Skill paths.
1989
+ */
1990
+ export async function publishCoderArtifacts(admitted, roleOutcome, coordinates, options = {}) {
1991
+ await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
1992
+ const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
1993
+ const reportPath = join(artifactsDir, "report.json");
1994
+ const evidencePath = join(artifactsDir, "evidence.json");
1995
+ await writeFile(reportPath, `${JSON.stringify({
1996
+ role: "coder",
1997
+ runId: admitted.runId,
1998
+ phase: admitted.phase,
1999
+ outcome: roleOutcome,
2000
+ ...(options.coderOutput === undefined
2001
+ ? {}
2002
+ : { receipt: options.coderOutput }),
2003
+ }, null, 2)}\n`, "utf8");
2004
+ await writeFile(evidencePath, `${JSON.stringify({
2005
+ runId: admitted.runId,
2006
+ role: "coder",
2007
+ phase: admitted.phase,
2008
+ sessionDirectory: coordinates.sessionDirectory,
2009
+ sessionFile: coordinates.sessionFile,
2010
+ admittedRequestPath: admitted.admittedRequestPath,
2011
+ taskPath: admitted.taskPath,
2012
+ attachments: admitted.attachments.map((a) => ({
2013
+ provenancePath: a.provenancePath,
2014
+ frozenPath: a.frozenPath,
2015
+ sha256: a.sha256,
2016
+ byteLength: a.byteLength,
2017
+ })),
2018
+ ...(options.methodProvenance === undefined
2019
+ ? {}
2020
+ : { methodProvenance: options.methodProvenance }),
2021
+ }, null, 2)}\n`, "utf8");
2022
+ return [
2023
+ { kind: "report", path: reportPath },
2024
+ { kind: "evidence", path: evidencePath },
2025
+ ];
2026
+ }
2027
+ /**
2028
+ * Read session entries for lawful settlement. Missing path → undefined (absence).
2029
+ * Malformed JSONL / other read failures throw with knownCause=session.
2030
+ */
2031
+ async function readLawfulSettlementEntries(sessionFile) {
2032
+ try {
2033
+ return await readBoundSessionEntries(sessionFile);
2034
+ }
2035
+ catch (error) {
2036
+ // Missing path is absence of a lawful outcome; callers classify via session inspect.
2037
+ if (isMissingPathError(error))
2038
+ return undefined;
2039
+ // Malformed JSONL and other read failures keep typed session identity.
2040
+ throw error instanceof Error &&
2041
+ error.knownCause === "session"
2042
+ ? error
2043
+ : sessionReadFailure(error, "session unreadable");
2044
+ }
2045
+ }
2046
+ /**
2047
+ * Lawful Judge outcome presence only — no artifact publication.
2048
+ * Returns undefined for genuine absence (missing path / no lawful verdict).
2049
+ * Session-read failures propagate with typed identity.
2050
+ */
2051
+ export async function readLawfulJudgeRoleOutcome(admitted, authority) {
2052
+ const sealed = await sealedLedgerOutcome(admitted);
2053
+ if (sealed?.role === "judge") {
2054
+ const details = sealed.decisiveFacts;
2055
+ // sealed.status is the sole authority (written by acceptedFacts at seal).
2056
+ return {
2057
+ kind: "accepted",
2058
+ role: "judge",
2059
+ status: sealed.status,
2060
+ decisiveFacts: judgeDecisiveFacts(details, sealed.status),
2061
+ };
2062
+ }
2063
+ // Non-final: consume ledger audit-escalation projection (no JSONL accepted rebuild).
2064
+ return auditEscalationLedgerOutcome(admitted, "judge");
2065
+ }
2066
+ /**
2067
+ * Single lawful-settlement implementation (session → outcome/Navigator/artifacts).
2068
+ *
2069
+ * - Returns undefined only for genuine absence (missing session path, or no
2070
+ * lawful verdict in an otherwise readable session).
2071
+ * - Malformed JSONL / other session-read failures throw with knownCause=session
2072
+ * and original identity (SyntaxError name retained).
2073
+ * - Artifact publication failures propagate with their original typed identity.
2074
+ * - Lawful outcome presence is decided before publication so a later write error
2075
+ * cannot erase the fact that a lawful result already exists.
2076
+ */
2077
+ async function settleLawfulJudgeTerminalResult(admitted, authority) {
2078
+ const roleOutcome = await readLawfulJudgeRoleOutcome(admitted, authority);
2079
+ if (roleOutcome === undefined)
2080
+ return undefined;
2081
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
2082
+ const entries = await readLawfulSettlementEntries(coordinates.sessionFile) ?? [];
2083
+ const navigator = extractNavigatorFact(entries);
2084
+ // Lawful outcome exists — artifact publication keeps original errno/name.
2085
+ const artifacts = await publishJudgeArtifacts(admitted, roleOutcome, coordinates);
2086
+ return withOptionalGateProjection({
2087
+ roleOutcome,
2088
+ navigator,
2089
+ artifacts,
2090
+ runId: admitted.runId,
2091
+ }, coordinates.sessionDirectory);
2092
+ }
2093
+ /**
2094
+ * Settle a lawful typed terminal result from the admitted session.
2095
+ * Throws when no lawful outcome is present (tests/callers that require success).
2096
+ * Session-read and publication failures retain their typed identity.
2097
+ */
2098
+ export async function settleJudgeTerminalResult(admitted, authority) {
2099
+ const settled = await settleLawfulJudgeTerminalResult(admitted, authority);
2100
+ if (settled === undefined) {
2101
+ throw new Error("Judge Role run completed without a lawful typed terminal result");
2102
+ }
2103
+ return settled;
2104
+ }
2105
+ /**
2106
+ * Try to settle a lawful typed terminal result from the admitted session.
2107
+ * Returns undefined only for genuine absence (no lawful verdict / missing path).
2108
+ * Session malformation and publication exceptions propagate with typed identity.
2109
+ */
2110
+ export async function trySettleJudgeTerminalResult(admitted, authority) {
2111
+ return settleLawfulJudgeTerminalResult(admitted, authority);
2112
+ }
2113
+ async function settleLawfulCoderTerminalResult(admitted, authority, options = {}) {
2114
+ const ledgerOutcome = await closedLedgerOutcome(admitted, "coder");
2115
+ if (ledgerOutcome === undefined)
2116
+ return undefined;
2117
+ let roleOutcome = ledgerOutcome;
2118
+ let output;
2119
+ if (ledgerOutcome.kind === "accepted") {
2120
+ output = validateAcceptedCoderDetails(ledgerOutcome.decisiveFacts);
2121
+ roleOutcome = {
2122
+ kind: "accepted",
2123
+ role: "coder",
2124
+ status: ledgerOutcome.status,
2125
+ decisiveFacts: coderDecisiveFacts(output),
2126
+ };
2127
+ }
2128
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
2129
+ const entries = await readLawfulSettlementEntries(coordinates.sessionFile) ?? [];
2130
+ const navigator = extractNavigatorFact(entries);
2131
+ const artifacts = await publishCoderArtifacts(admitted, roleOutcome, coordinates, {
2132
+ ...(output === undefined ? {} : { coderOutput: output }),
2133
+ ...(options.methodProvenance === undefined
2134
+ ? {}
2135
+ : { methodProvenance: options.methodProvenance }),
2136
+ });
2137
+ return withOptionalGateProjection({
2138
+ roleOutcome,
2139
+ navigator,
2140
+ artifacts,
2141
+ runId: admitted.runId,
2142
+ }, coordinates.sessionDirectory);
2143
+ }
2144
+ /** Settle a lawful Coder Terminal from the admitted session (shared #106 success interface). */
2145
+ export async function settleCoderTerminalResult(admitted, authority, options = {}) {
2146
+ const settled = await settleLawfulCoderTerminalResult(admitted, authority, options);
2147
+ if (settled === undefined) {
2148
+ throw new Error("Coder Role run completed without a lawful typed terminal result");
2149
+ }
2150
+ return settled;
2151
+ }
2152
+ function sessionMessageText(message) {
2153
+ if (message === undefined)
2154
+ return "";
2155
+ if (typeof message.content === "string")
2156
+ return message.content;
2157
+ if (!Array.isArray(message.content))
2158
+ return "";
2159
+ const parts = [];
2160
+ for (const part of message.content) {
2161
+ if (typeof part === "object" &&
2162
+ part !== null &&
2163
+ !Array.isArray(part) &&
2164
+ part.type === "text" &&
2165
+ typeof part.text === "string") {
2166
+ parts.push(part.text);
2167
+ }
2168
+ }
2169
+ return parts.join("\n");
2170
+ }
2171
+ /**
2172
+ * Observe optional Fixer diagnosing-bugs Skill expansions from the session.
2173
+ * Availability is always package-bound; invocation is recorded only when observed.
2174
+ */
2175
+ export function extractFixerMethodInvocations(entries, options) {
2176
+ const observed = [];
2177
+ for (const entry of entries) {
2178
+ if (entry?.type !== "message")
2179
+ continue;
2180
+ const message = entry.message;
2181
+ if (message?.role !== "user")
2182
+ continue;
2183
+ const text = sessionMessageText(message);
2184
+ if (text.length === 0)
2185
+ continue;
2186
+ const hit = observePackagedMethodSkillInvocation(text, {
2187
+ name: "diagnosing-bugs",
2188
+ allowedLocations: options.allowedLocations,
2189
+ });
2190
+ if (hit !== undefined)
2191
+ observed.push(hit);
2192
+ }
2193
+ return Object.freeze(observed);
2194
+ }
2195
+ /**
2196
+ * Publish lawful Fixer success Artifacts on the shared #106 success interface.
2197
+ * Evidence records package diagnosis provenance and optional observed invocation.
2198
+ */
2199
+ export async function publishFixerArtifacts(admitted, roleOutcome, coordinates, options) {
2200
+ await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2201
+ const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2202
+ const reportPath = join(artifactsDir, "report.json");
2203
+ const evidencePath = join(artifactsDir, "evidence.json");
2204
+ await writeFile(reportPath, `${JSON.stringify({
2205
+ role: "fixer",
2206
+ runId: admitted.runId,
2207
+ phase: admitted.phase,
2208
+ outcome: roleOutcome,
2209
+ ...(options.fixerOutput === undefined
2210
+ ? {}
2211
+ : { receipt: options.fixerOutput }),
2212
+ }, null, 2)}\n`, "utf8");
2213
+ await writeFile(evidencePath, `${JSON.stringify({
2214
+ runId: admitted.runId,
2215
+ role: "fixer",
2216
+ phase: admitted.phase,
2217
+ sessionDirectory: coordinates.sessionDirectory,
2218
+ sessionFile: coordinates.sessionFile,
2219
+ admittedRequestPath: admitted.admittedRequestPath,
2220
+ packetPath: admitted.packetPath,
2221
+ ...(admitted.prerequisitesPath === undefined
2222
+ ? {}
2223
+ : { prerequisitesPath: admitted.prerequisitesPath }),
2224
+ prerequisites: admitted.prerequisites,
2225
+ attachments: admitted.attachments.map((a) => ({
2226
+ provenancePath: a.provenancePath,
2227
+ frozenPath: a.frozenPath,
2228
+ sha256: a.sha256,
2229
+ byteLength: a.byteLength,
2230
+ })),
2231
+ methodProvenance: options.methodProvenance,
2232
+ // Optional diagnosis: availability is package-bound; invocation only when observed.
2233
+ methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
2234
+ methodInvocations: options.methodInvocations ?? [],
2235
+ }, null, 2)}\n`, "utf8");
2236
+ return [
2237
+ { kind: "report", path: reportPath },
2238
+ { kind: "evidence", path: evidencePath },
2239
+ ];
2240
+ }
2241
+ async function settleLawfulFixerTerminalResult(admitted, authority, options) {
2242
+ const ledgerOutcome = await closedLedgerOutcome(admitted, "fixer");
2243
+ if (ledgerOutcome === undefined)
2244
+ return undefined;
2245
+ let roleOutcome = ledgerOutcome;
2246
+ let output;
2247
+ if (ledgerOutcome.kind === "accepted") {
2248
+ output = validateFixerOutput(ledgerOutcome.decisiveFacts);
2249
+ roleOutcome = {
2250
+ kind: "accepted",
2251
+ role: "fixer",
2252
+ status: ledgerOutcome.status,
2253
+ decisiveFacts: fixerDecisiveFacts(output),
2254
+ };
2255
+ }
2256
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
2257
+ const { sessionDirectory, sessionFile } = coordinates;
2258
+ const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2259
+ const navigator = extractNavigatorFact(entries);
2260
+ const methodInvocations = extractFixerMethodInvocations(entries, {
2261
+ allowedLocations: [
2262
+ options.methodSkillPath,
2263
+ options.methodSkillConfiguredPath,
2264
+ ],
2265
+ });
2266
+ const artifacts = await publishFixerArtifacts(admitted, roleOutcome, coordinates, {
2267
+ ...(output === undefined ? {} : { fixerOutput: output }),
2268
+ methodProvenance: options.methodProvenance,
2269
+ methodInvocations,
2270
+ });
2271
+ return withOptionalGateProjection({
2272
+ roleOutcome,
2273
+ navigator,
2274
+ artifacts,
2275
+ runId: admitted.runId,
2276
+ }, sessionDirectory);
2277
+ }
2278
+ /** Settle a lawful Fixer Terminal from the admitted session (shared #106 success interface). */
2279
+ export async function settleFixerTerminalResult(admitted, authority, options) {
2280
+ const settled = await settleLawfulFixerTerminalResult(admitted, authority, options);
2281
+ if (settled === undefined) {
2282
+ throw new Error("Fixer Role run completed without a lawful typed terminal result");
2283
+ }
2284
+ return settled;
2285
+ }
2286
+ export async function publishCollectorArtifacts(admitted, roleOutcome, coordinates, options = {}) {
2287
+ await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2288
+ const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2289
+ const reportPath = join(artifactsDir, "report.json");
2290
+ const evidencePath = join(artifactsDir, "evidence.json");
2291
+ await writeFile(reportPath, `${JSON.stringify({
2292
+ role: "collector",
2293
+ runId: admitted.runId,
2294
+ outcome: roleOutcome,
2295
+ ...(options.collectorReceipt === undefined
2296
+ ? {}
2297
+ : { receipt: options.collectorReceipt }),
2298
+ }, null, 2)}\n`, "utf8");
2299
+ await writeFile(evidencePath, `${JSON.stringify({
2300
+ runId: admitted.runId,
2301
+ role: "collector",
2302
+ prNumber: admitted.prNumber,
2303
+ repository: admitted.repository.canonical,
2304
+ manifestDigest: admitted.manifestDigest,
2305
+ sessionDirectory: coordinates.sessionDirectory,
2306
+ sessionFile: coordinates.sessionFile,
2307
+ admittedRequestPath: admitted.admittedRequestPath,
2308
+ attachments: admitted.attachments.map((a) => ({
2309
+ provenancePath: a.provenancePath,
2310
+ frozenPath: a.frozenPath,
2311
+ sha256: a.sha256,
2312
+ byteLength: a.byteLength,
2313
+ })),
2314
+ }, null, 2)}\n`, "utf8");
2315
+ return [
2316
+ { kind: "report", path: reportPath },
2317
+ { kind: "evidence", path: evidencePath },
2318
+ ];
2319
+ }
2320
+ async function settleLawfulCollectorTerminalResult(admitted, authority) {
2321
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
2322
+ const { sessionDirectory, sessionFile } = coordinates;
2323
+ const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2324
+ const roleOutcome = await sealedLedgerOutcome(admitted);
2325
+ if (roleOutcome?.role !== "collector") {
2326
+ // Bounded to the current attempt so multi-attempt resume timeout/no-output
2327
+ // is not masked by a prior wait-tool residual (#633).
2328
+ const scanStart = currentAttemptStartIndex(entries);
2329
+ for (let index = entries.length - 1; index >= scanStart; index -= 1) {
2330
+ const message = entries[index]?.message;
2331
+ if (message?.role !== "toolResult")
2332
+ continue;
2333
+ const residual = boundErroredToolCandidate(entries, index, message, COLLECTOR_WAIT_TOOL);
2334
+ if (residual === undefined)
2335
+ continue;
2336
+ const candidate = residual.candidate;
2337
+ const duration = isRecord(candidate) ? candidate.durationMs : undefined;
2338
+ if (Number.isSafeInteger(duration) && duration >= 1 && duration <= 900_000) {
2339
+ continue;
2340
+ }
2341
+ return {
2342
+ roleOutcome: buildResidualIncompleteTerminalOutcome({
2343
+ role: "collector",
2344
+ candidate,
2345
+ diagnostic: residual.diagnostic,
2346
+ }),
2347
+ navigator: { disposition: "no-advice" },
2348
+ artifacts: [],
2349
+ runId: admitted.runId,
2350
+ };
2351
+ }
2352
+ return undefined;
2353
+ }
2354
+ const receipt = validateAcceptedCollectorReceipt(roleOutcome.decisiveFacts);
2355
+ assertCollectorReceiptMatchesAdmitted(receipt, admitted);
2356
+ const accepted = {
2357
+ kind: "accepted",
2358
+ role: "collector",
2359
+ status: roleOutcome.status,
2360
+ decisiveFacts: collectorDecisiveFacts(receipt),
2361
+ };
2362
+ const navigator = extractNavigatorFact(entries);
2363
+ const artifacts = await publishCollectorArtifacts(admitted, accepted, coordinates, { collectorReceipt: receipt });
2364
+ return withOptionalGateProjection({
2365
+ roleOutcome: accepted,
2366
+ navigator,
2367
+ artifacts,
2368
+ runId: admitted.runId,
2369
+ }, sessionDirectory);
2370
+ }
2371
+ /** Settle a lawful Collector Terminal from the admitted session. */
2372
+ export async function settleCollectorTerminalResult(admitted, authority) {
2373
+ const settled = await settleLawfulCollectorTerminalResult(admitted, authority);
2374
+ if (settled === undefined) {
2375
+ throw new Error("Collector Role run completed without a lawful typed terminal result");
2376
+ }
2377
+ return settled;
2378
+ }
2379
+ /** Try to settle a lawful Collector Terminal; undefined only for genuine absence. */
2380
+ export async function trySettleCollectorTerminalResult(admitted, authority) {
2381
+ return settleLawfulCollectorTerminalResult(admitted, authority);
2382
+ }
2383
+ export async function publishDoctorArtifacts(admitted, roleOutcome, coordinates, options = {}) {
2384
+ await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2385
+ const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2386
+ const reportPath = join(artifactsDir, "report.json");
2387
+ const evidencePath = join(artifactsDir, "evidence.json");
2388
+ await writeFile(reportPath, `${JSON.stringify({
2389
+ role: "doctor",
2390
+ runId: admitted.runId,
2391
+ outcome: roleOutcome,
2392
+ ...(options.doctorOutput === undefined
2393
+ ? {}
2394
+ : { receipt: options.doctorOutput }),
2395
+ }, null, 2)}\n`, "utf8");
2396
+ await writeFile(evidencePath, `${JSON.stringify({
2397
+ runId: admitted.runId,
2398
+ role: "doctor",
2399
+ issueNumber: admitted.issueNumber,
2400
+ caseRunsPath: admitted.caseRunsPath,
2401
+ caseIdentity: admitted.caseIdentity,
2402
+ sessionDirectory: coordinates.sessionDirectory,
2403
+ sessionFile: coordinates.sessionFile,
2404
+ admittedRequestPath: admitted.admittedRequestPath,
2405
+ attachments: admitted.attachments.map((a) => ({
2406
+ provenancePath: a.provenancePath,
2407
+ frozenPath: a.frozenPath,
2408
+ sha256: a.sha256,
2409
+ byteLength: a.byteLength,
2410
+ })),
2411
+ }, null, 2)}\n`, "utf8");
2412
+ return [
2413
+ { kind: "report", path: reportPath },
2414
+ { kind: "evidence", path: evidencePath },
2415
+ ];
2416
+ }
2417
+ async function settleLawfulDoctorTerminalResult(admitted, authority) {
2418
+ const sealed = await sealedLedgerOutcome(admitted);
2419
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
2420
+ const { sessionDirectory, sessionFile } = coordinates;
2421
+ const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2422
+ if (sealed?.role !== "doctor") {
2423
+ const escalation = await auditEscalationLedgerOutcome(admitted, "doctor");
2424
+ if (escalation === undefined)
2425
+ return undefined;
2426
+ const artifacts = await publishDoctorArtifacts(admitted, escalation, coordinates);
2427
+ return withOptionalGateProjection({
2428
+ roleOutcome: escalation,
2429
+ navigator: extractNavigatorFact(entries),
2430
+ artifacts,
2431
+ runId: admitted.runId,
2432
+ }, sessionDirectory);
2433
+ }
2434
+ const output = validateRecordedDoctorOutput(sealed.decisiveFacts);
2435
+ const roleOutcome = {
2436
+ kind: "accepted",
2437
+ role: "doctor",
2438
+ status: sealed.status,
2439
+ decisiveFacts: doctorDecisiveFacts(output),
2440
+ };
2441
+ // Bind completed receipt case identity to the admitted Issue evidence case.
2442
+ if (String(output.status) === "completed") {
2443
+ const completedCase = output.case;
2444
+ if (completedCase.issueNumber !== admitted.caseIdentity.issueNumber ||
2445
+ completedCase.runsPath !== admitted.caseIdentity.runsPath) {
2446
+ const error = new Error("Doctor receipt case identity does not match admitted case identity");
2447
+ error.name = "DoctorReceiptBindingError";
2448
+ error.knownCause = "output";
2449
+ throw error;
2450
+ }
2451
+ }
2452
+ const navigator = extractNavigatorFact(entries);
2453
+ const artifacts = await publishDoctorArtifacts(admitted, roleOutcome, coordinates, { doctorOutput: output });
2454
+ return withOptionalGateProjection({
2455
+ roleOutcome,
2456
+ navigator,
2457
+ artifacts,
2458
+ runId: admitted.runId,
2459
+ }, sessionDirectory);
2460
+ }
2461
+ /** Settle a lawful Doctor Terminal from the admitted session. */
2462
+ export async function settleDoctorTerminalResult(admitted, authority) {
2463
+ const settled = await settleLawfulDoctorTerminalResult(admitted, authority);
2464
+ if (settled === undefined) {
2465
+ throw new Error("Doctor Role run completed without a lawful typed terminal result");
2466
+ }
2467
+ return settled;
2468
+ }
2469
+ /** Try to settle a lawful Doctor Terminal; undefined only for genuine absence. */
2470
+ export async function trySettleDoctorTerminalResult(admitted, authority) {
2471
+ return settleLawfulDoctorTerminalResult(admitted, authority);
2472
+ }
2473
+ /** Latest top-level user message index; 0 when the session has none (initial attempt). */
2474
+ function currentAttemptStartIndex(entries) {
2475
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
2476
+ const entry = entries[i];
2477
+ if (entry?.type === "message" && entry.message?.role === "user") {
2478
+ return i;
2479
+ }
2480
+ }
2481
+ return 0;
2482
+ }
2483
+ async function settleLawfulSeatAcceptedTerminalResult(admitted, authority, spec, scope) {
2484
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
2485
+ const { sessionDirectory, sessionFile } = coordinates;
2486
+ const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2487
+ const roleOutcome = await closedLedgerOutcome(admitted, spec.role, scope);
2488
+ if (roleOutcome?.kind === "audit_escalation") {
2489
+ const navigator = extractNavigatorFact(entries);
2490
+ return withOptionalGateProjection({
2491
+ roleOutcome,
2492
+ navigator,
2493
+ artifacts: [],
2494
+ runId: admitted.runId,
2495
+ }, sessionDirectory);
2496
+ }
2497
+ if (roleOutcome?.role !== spec.role) {
2498
+ // No usable release → existing non-zero failure channel with candidate (#475 / ADR 0055).
2499
+ // One reverse pass: prefer errored residual; else latest accepted-once non-usable details.
2500
+ // Bounded to the current attempt so multi-attempt resume timeout/no-output
2501
+ // is not masked by a prior residual (#599 / #633).
2502
+ const scanStart = currentAttemptStartIndex(entries);
2503
+ let acceptedNonUsable;
2504
+ for (let index = entries.length - 1; index >= scanStart; index -= 1) {
2505
+ const message = entries[index]?.message;
2506
+ if (message?.role !== "toolResult")
2507
+ continue;
2508
+ const residual = boundErroredToolCandidate(entries, index, message, spec.toolName);
2509
+ if (residual !== undefined) {
2510
+ return settleFailureTerminalResult(admitted, {
2511
+ cause: "output",
2512
+ diagnostic: residual.diagnostic,
2513
+ details: stampShapeUnreadableDetails(residual.candidate),
2514
+ }, authority);
2515
+ }
2516
+ if (acceptedNonUsable === undefined &&
2517
+ message.toolName === spec.toolName &&
2518
+ isAcceptedPackagedRoleTerminalResult(message)) {
2519
+ // Accepted once but not a lawful seat release — hold as fallback.
2520
+ if (!spec.tryAcceptDetails(message.details)) {
2521
+ acceptedNonUsable = message.details;
2522
+ }
2523
+ }
2524
+ }
2525
+ if (acceptedNonUsable !== undefined) {
2526
+ return settleFailureTerminalResult(admitted, {
2527
+ cause: "output",
2528
+ diagnostic: spec.nonUsableDiagnostic,
2529
+ details: stampShapeUnreadableDetails(acceptedNonUsable),
2530
+ }, authority);
2531
+ }
2532
+ return undefined;
2533
+ }
2534
+ if (!spec.tryAcceptDetails(roleOutcome.decisiveFacts)) {
2535
+ return settleFailureTerminalResult(admitted, {
2536
+ cause: "output",
2537
+ diagnostic: spec.nonUsableDiagnostic,
2538
+ details: stampShapeUnreadableDetails(roleOutcome.decisiveFacts),
2539
+ }, authority);
2540
+ }
2541
+ const acceptedOutcome = spec.projectAccepted(roleOutcome);
2542
+ const navigator = extractNavigatorFact(entries);
2543
+ return withOptionalGateProjection({
2544
+ roleOutcome: acceptedOutcome,
2545
+ navigator,
2546
+ artifacts: [],
2547
+ runId: admitted.runId,
2548
+ }, sessionDirectory);
2549
+ }
2550
+ function tryAcceptWithValidator(validate) {
2551
+ return (details) => {
2552
+ try {
2553
+ validate(details);
2554
+ return true;
2555
+ }
2556
+ catch {
2557
+ return false;
2558
+ }
2559
+ };
2560
+ }
2561
+ async function settleLawfulNotaryTerminalResult(admitted, authority, scope) {
2562
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2563
+ role: "notary",
2564
+ toolName: NOTARY_OUTPUT_TOOL_NAME,
2565
+ nonUsableDiagnostic: "符宝郎回执无显式 pass/bounce/escalate",
2566
+ tryAcceptDetails: tryAcceptWithValidator(validateRecordedNotaryOutput),
2567
+ projectAccepted: (sealed) => {
2568
+ const output = validateRecordedNotaryOutput(sealed.decisiveFacts);
2569
+ const accepted = {
2570
+ kind: "accepted",
2571
+ role: "notary",
2572
+ status: sealed.status,
2573
+ decisiveFacts: notaryDecisiveFacts(output),
2574
+ };
2575
+ return accepted;
2576
+ },
2577
+ }, scope);
2578
+ }
2579
+ /** Settle a lawful Notary Terminal from the admitted session. */
2580
+ export async function settleNotaryTerminalResult(admitted, authority, scope) {
2581
+ const settled = await settleLawfulNotaryTerminalResult(admitted, authority, scope);
2582
+ if (settled === undefined) {
2583
+ throw new Error("Notary Role run completed without a lawful typed terminal result");
2584
+ }
2585
+ return settled;
2586
+ }
2587
+ /** Try to settle a lawful Notary Terminal; undefined only for genuine absence. */
2588
+ export async function trySettleNotaryTerminalResult(admitted, authority, scope) {
2589
+ return settleLawfulNotaryTerminalResult(admitted, authority, scope);
2590
+ }
2591
+ async function settleLawfulCountersignTerminalResult(admitted, authority, scope) {
2592
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2593
+ role: "countersign",
2594
+ toolName: COUNTERSIGN_OUTPUT_TOOL_NAME,
2595
+ nonUsableDiagnostic: "给事中回执无显式 署/封驳/上呈",
2596
+ tryAcceptDetails: tryAcceptWithValidator(validateRecordedCountersignOutput),
2597
+ projectAccepted: (sealed) => {
2598
+ const verdict = validateRecordedCountersignOutput(sealed.decisiveFacts);
2599
+ const accepted = {
2600
+ kind: "accepted",
2601
+ role: "countersign",
2602
+ status: sealed.status,
2603
+ decisiveFacts: countersignDecisiveFacts(verdict, sealed.status),
2604
+ };
2605
+ return accepted;
2606
+ },
2607
+ }, scope);
2608
+ }
2609
+ /** Settle a lawful Countersign Terminal from the admitted session. */
2610
+ export async function settleCountersignTerminalResult(admitted, authority, scope) {
2611
+ const settled = await settleLawfulCountersignTerminalResult(admitted, authority, scope);
2612
+ if (settled === undefined) {
2613
+ throw new Error("Countersign Role run completed without a lawful typed terminal result");
2614
+ }
2615
+ return settled;
2616
+ }
2617
+ /** Try to settle a lawful Countersign Terminal; undefined only for genuine absence. */
2618
+ export async function trySettleCountersignTerminalResult(admitted, authority, scope) {
2619
+ return settleLawfulCountersignTerminalResult(admitted, authority, scope);
2620
+ }
2621
+ async function settleLawfulGleanerLeftTerminalResult(admitted, authority, scope) {
2622
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2623
+ role: "gleaner-left",
2624
+ toolName: GLEANER_LEFT_OUTPUT_TOOL_NAME,
2625
+ nonUsableDiagnostic: "左拾遗回执无显式 completed",
2626
+ tryAcceptDetails: tryAcceptWithValidator(validateRecordedGleanerLeftOutput),
2627
+ projectAccepted: (sealed) => {
2628
+ const output = validateRecordedGleanerLeftOutput(sealed.decisiveFacts);
2629
+ const accepted = {
2630
+ kind: "accepted",
2631
+ role: "gleaner-left",
2632
+ status: sealed.status,
2633
+ decisiveFacts: gleanerLeftDecisiveFacts(output),
2634
+ };
2635
+ return accepted;
2636
+ },
2637
+ }, scope);
2638
+ }
2639
+ /** Settle a lawful Gleaner-Left Terminal from the admitted session. */
2640
+ export async function settleGleanerLeftTerminalResult(admitted, authority, scope) {
2641
+ const settled = await settleLawfulGleanerLeftTerminalResult(admitted, authority, scope);
2642
+ if (settled === undefined) {
2643
+ throw new Error("Gleaner-Left Role run completed without a lawful typed terminal result");
2644
+ }
2645
+ return settled;
2646
+ }
2647
+ /** Try to settle a lawful Gleaner-Left Terminal; undefined only for genuine absence. */
2648
+ export async function trySettleGleanerLeftTerminalResult(admitted, authority, scope) {
2649
+ return settleLawfulGleanerLeftTerminalResult(admitted, authority, scope);
2650
+ }
2651
+ async function settleLawfulDiaristTerminalResult(admitted, authority, scope) {
2652
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2653
+ role: "diarist",
2654
+ toolName: DIARIST_OUTPUT_TOOL_NAME,
2655
+ nonUsableDiagnostic: "起居郎回执无显式 completed",
2656
+ tryAcceptDetails: tryAcceptWithValidator(validateRecordedDiaristOutput),
2657
+ projectAccepted: (sealed) => {
2658
+ const output = validateRecordedDiaristOutput(sealed.decisiveFacts);
2659
+ const accepted = {
2660
+ kind: "accepted",
2661
+ role: "diarist",
2662
+ status: sealed.status,
2663
+ decisiveFacts: diaristDecisiveFacts(output),
2664
+ };
2665
+ return accepted;
2666
+ },
2667
+ }, scope);
2668
+ }
2669
+ /** Try to settle a lawful Diarist Terminal; undefined only for genuine absence. */
2670
+ export async function trySettleDiaristTerminalResult(admitted, authority, scope) {
2671
+ return settleLawfulDiaristTerminalResult(admitted, authority, scope);
2672
+ }
2673
+ async function settleLawfulInspectorTerminalResult(admitted, authority, scope) {
2674
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2675
+ role: "inspector",
2676
+ toolName: INSPECTOR_OUTPUT_TOOL_NAME,
2677
+ nonUsableDiagnostic: "察院回执无显式 pass/bounce/escalate",
2678
+ tryAcceptDetails: tryAcceptWithValidator(validateRecordedInspectorOutput),
2679
+ projectAccepted: (sealed) => {
2680
+ const output = validateRecordedInspectorOutput(sealed.decisiveFacts);
2681
+ const accepted = {
2682
+ kind: "accepted",
2683
+ role: "inspector",
2684
+ status: sealed.status,
2685
+ decisiveFacts: inspectorDecisiveFacts(output),
2686
+ };
2687
+ return accepted;
2688
+ },
2689
+ }, scope);
2690
+ }
2691
+ export async function trySettleInspectorTerminalResult(admitted, authority, scope) {
2692
+ return settleLawfulInspectorTerminalResult(admitted, authority, scope);
2693
+ }
2694
+ async function settleLawfulGatekeeperTerminalResult(admitted, authority, scope) {
2695
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2696
+ role: "gatekeeper",
2697
+ toolName: GATEKEEPER_OUTPUT_TOOL_NAME,
2698
+ nonUsableDiagnostic: "门下省决议无显式 dispatch/pass",
2699
+ tryAcceptDetails: tryAcceptWithValidator(validateRecordedGatekeeperOutput),
2700
+ projectAccepted: (sealed) => {
2701
+ const output = validateRecordedGatekeeperOutput(sealed.decisiveFacts);
2702
+ const accepted = {
2703
+ kind: "accepted",
2704
+ role: "gatekeeper",
2705
+ status: output.status,
2706
+ decisiveFacts: gatekeeperDecisiveFacts(output),
2707
+ };
2708
+ return accepted;
2709
+ },
2710
+ }, scope);
2711
+ }
2712
+ /** Try to settle a lawful Gatekeeper Terminal; undefined only for genuine absence. */
2713
+ export async function trySettleGatekeeperTerminalResult(admitted, authority, scope) {
2714
+ return settleLawfulGatekeeperTerminalResult(admitted, authority, scope);
2715
+ }
2716
+ async function settleLawfulNavigatorTerminalResult(admitted, authority, scope) {
2717
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2718
+ role: "navigator",
2719
+ toolName: NAVIGATOR_OUTPUT_TOOL_NAME,
2720
+ nonUsableDiagnostic: "游奕使回执无显式路线建议",
2721
+ tryAcceptDetails: tryAcceptWithValidator(validateRecordedNavigatorOutput),
2722
+ projectAccepted: (sealed) => {
2723
+ const output = validateRecordedNavigatorOutput(sealed.decisiveFacts);
2724
+ const accepted = {
2725
+ kind: "accepted",
2726
+ role: "navigator",
2727
+ status: "advice",
2728
+ decisiveFacts: navigatorDecisiveFacts(output),
2729
+ };
2730
+ return accepted;
2731
+ },
2732
+ }, scope);
2733
+ }
2734
+ /** Try to settle a lawful Navigator Terminal; undefined only for genuine absence. */
2735
+ export async function trySettleNavigatorTerminalResult(admitted, authority, scope) {
2736
+ return settleLawfulNavigatorTerminalResult(admitted, authority, scope);
2737
+ }
2738
+ async function settleLawfulAuditorTerminalResult(admitted, authority, scope) {
2739
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2740
+ role: "auditor",
2741
+ toolName: AUDITOR_OUTPUT_TOOL_NAME,
2742
+ nonUsableDiagnostic: "审刑院回执无显式 pass/revise/escalate",
2743
+ // Only pass/revise/escalate are lawful releases. Unreadable mystery status
2744
+ // fails closed with candidate retained for parent ComplianceCandidateUnreadableError.
2745
+ tryAcceptDetails: (details) => projectLawfulAuditorOutput(details) !== undefined,
2746
+ projectAccepted: (sealed) => {
2747
+ const lawful = projectLawfulAuditorOutput(sealed.decisiveFacts);
2748
+ if (lawful === undefined) {
2749
+ throw new Error("auditor projectAccepted requires lawful pass/revise/escalate");
2750
+ }
2751
+ const accepted = {
2752
+ kind: "accepted",
2753
+ role: "auditor",
2754
+ status: lawful.status,
2755
+ decisiveFacts: auditorDecisiveFacts(lawful),
2756
+ };
2757
+ return accepted;
2758
+ },
2759
+ }, scope);
2760
+ }
2761
+ export async function trySettleAuditorTerminalResult(admitted, authority, scope) {
2762
+ return settleLawfulAuditorTerminalResult(admitted, authority, scope);
2763
+ }
2764
+ async function settleLawfulEvidenceChildTerminalResult(admitted, authority, scope) {
2765
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2766
+ role: "evidence-child",
2767
+ toolName: EVIDENCE_CHILD_OUTPUT_TOOL_NAME,
2768
+ nonUsableDiagnostic: "取证回执无报告正文",
2769
+ tryAcceptDetails: tryAcceptWithValidator(validateRecordedEvidenceChildOutput),
2770
+ projectAccepted: (sealed) => {
2771
+ const output = validateRecordedEvidenceChildOutput(sealed.decisiveFacts);
2772
+ const accepted = {
2773
+ kind: "accepted",
2774
+ role: "evidence-child",
2775
+ status: "report",
2776
+ decisiveFacts: evidenceChildDecisiveFacts(output),
2777
+ };
2778
+ return accepted;
2779
+ },
2780
+ }, scope);
2781
+ }
2782
+ export async function trySettleEvidenceChildTerminalResult(admitted, authority, scope) {
2783
+ return settleLawfulEvidenceChildTerminalResult(admitted, authority, scope);
2784
+ }
2785
+ /** Try to settle a lawful Coder Terminal; undefined only for genuine absence. */
2786
+ export async function trySettleCoderTerminalResult(admitted, authority, options = {}) {
2787
+ return settleLawfulCoderTerminalResult(admitted, authority, options);
2788
+ }
2789
+ /** Try to settle a lawful Fixer Terminal; undefined only for genuine absence. */
2790
+ export async function trySettleFixerTerminalResult(admitted, authority, options) {
2791
+ return settleLawfulFixerTerminalResult(admitted, authority, options);
2792
+ }
2793
+ /**
2794
+ * Observe forced Reviewer code-review Skill expansions from the session.
2795
+ * Expansion evidence is package-path only; ambient home locations never count.
2796
+ */
2797
+ export function extractReviewerMethodInvocations(entries, options) {
2798
+ const observed = [];
2799
+ for (const entry of entries) {
2800
+ if (entry?.type !== "message")
2801
+ continue;
2802
+ const message = entry.message;
2803
+ if (message?.role !== "user")
2804
+ continue;
2805
+ const text = sessionMessageText(message);
2806
+ if (text.length === 0)
2807
+ continue;
2808
+ const hit = observePackagedMethodSkillInvocation(text, {
2809
+ name: "code-review",
2810
+ allowedLocations: options.allowedLocations,
2811
+ });
2812
+ if (hit !== undefined)
2813
+ observed.push(hit);
2814
+ }
2815
+ return Object.freeze(observed);
2816
+ }
2817
+ /**
2818
+ * Publish lawful Reviewer success Artifacts on the shared #106 success interface.
2819
+ * Evidence records package code-review provenance and typed expansion
2820
+ * observation without ambient home Skill paths.
2821
+ */
2822
+ export async function publishReviewerArtifacts(admitted, roleOutcome, coordinates, options) {
2823
+ await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2824
+ const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2825
+ const reportPath = join(artifactsDir, "report.json");
2826
+ const evidencePath = join(artifactsDir, "evidence.json");
2827
+ await writeFile(reportPath, `${JSON.stringify({
2828
+ role: "reviewer",
2829
+ runId: admitted.runId,
2830
+ outcome: roleOutcome,
2831
+ ...(options.reviewerReceipt === undefined
2832
+ ? {}
2833
+ : { receipt: options.reviewerReceipt }),
2834
+ }, null, 2)}\n`, "utf8");
2835
+ await writeFile(evidencePath, `${JSON.stringify({
2836
+ runId: admitted.runId,
2837
+ role: "reviewer",
2838
+ sessionDirectory: coordinates.sessionDirectory,
2839
+ sessionFile: coordinates.sessionFile,
2840
+ admittedRequestPath: admitted.admittedRequestPath,
2841
+ baseRevision: admitted.baseRevision,
2842
+ authorityRefs: [...admitted.authorityRefs],
2843
+ ...(admitted.instructionEmpty
2844
+ ? {}
2845
+ : { callerProvenance: admitted.instruction }),
2846
+ // Self-fetch Spec bytes + source annotation when primary path produced material (#343).
2847
+ ...(options.reviewerReceipt?.specFetchedMaterial === undefined
2848
+ ? {}
2849
+ : { specFetchedMaterial: options.reviewerReceipt.specFetchedMaterial }),
2850
+ attachments: admitted.attachments.map((a) => ({
2851
+ provenancePath: a.provenancePath,
2852
+ frozenPath: a.frozenPath,
2853
+ sha256: a.sha256,
2854
+ byteLength: a.byteLength,
2855
+ })),
2856
+ methodProvenance: options.methodProvenance,
2857
+ // Forced package method: availability is package-bound; expansion only when observed.
2858
+ methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
2859
+ methodInvocations: options.methodInvocations ?? [],
2860
+ }, null, 2)}\n`, "utf8");
2861
+ return [
2862
+ { kind: "report", path: reportPath },
2863
+ { kind: "evidence", path: evidencePath },
2864
+ ];
2865
+ }
2866
+ async function settleLawfulReviewerTerminalResult(admitted, authority, options) {
2867
+ const sealed = await sealedLedgerOutcome(admitted);
2868
+ if (sealed?.role !== "reviewer")
2869
+ return undefined;
2870
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
2871
+ const { sessionDirectory, sessionFile } = coordinates;
2872
+ const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2873
+ const receipt = validateRuntimeReviewerReceipt(sealed.decisiveFacts);
2874
+ const roleOutcome = {
2875
+ kind: "accepted",
2876
+ role: "reviewer",
2877
+ status: sealed.status,
2878
+ decisiveFacts: reviewerDecisiveFacts(receipt),
2879
+ };
2880
+ const navigator = extractNavigatorFact(entries);
2881
+ const methodInvocations = extractReviewerMethodInvocations(entries, {
2882
+ allowedLocations: [
2883
+ options.methodSkillPath,
2884
+ options.methodSkillConfiguredPath,
2885
+ ],
2886
+ });
2887
+ const artifacts = await publishReviewerArtifacts(admitted, roleOutcome, coordinates, {
2888
+ reviewerReceipt: receipt,
2889
+ methodProvenance: options.methodProvenance,
2890
+ methodInvocations,
2891
+ });
2892
+ return withOptionalGateProjection({
2893
+ roleOutcome,
2894
+ navigator,
2895
+ artifacts,
2896
+ runId: admitted.runId,
2897
+ }, sessionDirectory);
2898
+ }
2899
+ /** Settle a lawful Reviewer Terminal from the admitted session (shared #106 success interface). */
2900
+ export async function settleReviewerTerminalResult(admitted, authority, options) {
2901
+ const settled = await settleLawfulReviewerTerminalResult(admitted, authority, options);
2902
+ if (settled === undefined) {
2903
+ throw new Error("Reviewer Role run completed without a lawful typed terminal result");
2904
+ }
2905
+ return settled;
2906
+ }
2907
+ /** Try to settle a lawful Reviewer Terminal; undefined only for genuine absence. */
2908
+ export async function trySettleReviewerTerminalResult(admitted, authority, options) {
2909
+ return settleLawfulReviewerTerminalResult(admitted, authority, options);
2910
+ }
2911
+ function mergerDecisiveFacts(output) {
2912
+ const candidate = output;
2913
+ const facts = {};
2914
+ const status = safelyRead(candidate, "status");
2915
+ const attemptId = safelyRead(candidate, "attemptId");
2916
+ if (status.readable && typeof status.value === "string")
2917
+ facts.mergerStatus = status.value;
2918
+ if (attemptId.readable && attemptId.value !== undefined)
2919
+ facts.attemptId = attemptId.value;
2920
+ const statusBase = status.readable && typeof status.value === "string"
2921
+ ? (status.value)
2922
+ : undefined;
2923
+ const decisiveKey = statusBase === "completed" ? "mergeCommitId" : "diagnosis";
2924
+ const decisive = safelyRead(candidate, decisiveKey);
2925
+ if (decisive.readable && decisive.value !== undefined)
2926
+ facts[decisiveKey] = decisive.value;
2927
+ return facts;
2928
+ }
2929
+ /**
2930
+ * Observe forced Merger resolving-merge-conflicts Skill expansions from the session.
2931
+ * Expansion evidence is package-path only; ambient home locations never count.
2932
+ */
2933
+ export function extractMergerMethodInvocations(entries, options) {
2934
+ const observed = [];
2935
+ for (const entry of entries) {
2936
+ if (entry?.type !== "message")
2937
+ continue;
2938
+ const message = entry.message;
2939
+ if (message?.role !== "user")
2940
+ continue;
2941
+ const text = sessionMessageText(message);
2942
+ if (text.length === 0)
2943
+ continue;
2944
+ const hit = observePackagedMethodSkillInvocation(text, {
2945
+ name: "resolving-merge-conflicts",
2946
+ allowedLocations: options.allowedLocations,
2947
+ });
2948
+ if (hit !== undefined)
2949
+ observed.push(hit);
2950
+ }
2951
+ return Object.freeze(observed);
2952
+ }
2953
+ /**
2954
+ * Publish lawful Merger success Artifacts on the shared #106 success interface.
2955
+ * Evidence records package method provenance, forced expansion observation, and
2956
+ * adapter-derived mechanical envelope facts without ambient home Skill paths.
2957
+ */
2958
+ export async function publishMergerArtifacts(admitted, roleOutcome, coordinates, options) {
2959
+ await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2960
+ const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2961
+ const reportPath = join(artifactsDir, "report.json");
2962
+ const evidencePath = join(artifactsDir, "evidence.json");
2963
+ await writeFile(reportPath, `${JSON.stringify({
2964
+ role: "merger",
2965
+ runId: admitted.runId,
2966
+ outcome: roleOutcome,
2967
+ ...(options.mergerOutput === undefined
2968
+ ? {}
2969
+ : { receipt: options.mergerOutput }),
2970
+ }, null, 2)}\n`, "utf8");
2971
+ await writeFile(evidencePath, `${JSON.stringify({
2972
+ runId: admitted.runId,
2973
+ role: "merger",
2974
+ sessionDirectory: coordinates.sessionDirectory,
2975
+ sessionFile: coordinates.sessionFile,
2976
+ admittedRequestPath: admitted.admittedRequestPath,
2977
+ mergerInputPath: admitted.mergerInputPath,
2978
+ derived: admitted.derived,
2979
+ attachments: admitted.attachments.map((a) => ({
2980
+ provenancePath: a.provenancePath,
2981
+ frozenPath: a.frozenPath,
2982
+ sha256: a.sha256,
2983
+ byteLength: a.byteLength,
2984
+ })),
2985
+ methodProvenance: options.methodProvenance,
2986
+ methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
2987
+ methodInvocations: options.methodInvocations ?? [],
2988
+ }, null, 2)}\n`, "utf8");
2989
+ return [
2990
+ { kind: "report", path: reportPath },
2991
+ { kind: "evidence", path: evidencePath },
2992
+ ];
2993
+ }
2994
+ async function settleLawfulMergerTerminalResult(admitted, authority, options) {
2995
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
2996
+ const { sessionDirectory, sessionFile } = coordinates;
2997
+ const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2998
+ const roleOutcome = await sealedLedgerOutcome(admitted);
2999
+ if (roleOutcome?.role !== "merger") {
3000
+ // Ledger owns 0041: a non-sole closed round is not residual-incomplete material.
3001
+ const latestOutcome = await readLatestSubmissionOutcome(admitted.projectRoot, admitted.runId, sealedLedgerHome(admitted));
3002
+ if (latestOutcome?.outcome === "correctable-rejection" && latestOutcome.code === "non-sole-round") {
3003
+ return undefined;
3004
+ }
3005
+ // Residual incomplete: shape/identity fail after the sole-round barrier passed (ledger typed).
3006
+ // Settlement does not re-judge calls.length.
3007
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
3008
+ const message = entries[index]?.message;
3009
+ if (message?.role !== "toolResult")
3010
+ continue;
3011
+ const residual = boundErroredToolCandidate(entries, index, message, MERGER_OUTPUT_TOOL_NAME);
3012
+ if (residual === undefined)
3013
+ continue;
3014
+ const attemptId = isRecord(residual.candidate)
3015
+ ? safelyRead(residual.candidate, "attemptId")
3016
+ : { readable: true, value: undefined };
3017
+ // Admitted-attempt identity binding only (ADR 0037) — not sole-final cardinality.
3018
+ if (!attemptId.readable || attemptId.value !== admitted.runId)
3019
+ continue;
3020
+ try {
3021
+ validateMergerOutput(residual.candidate, admitted.runId);
3022
+ }
3023
+ catch {
3024
+ return {
3025
+ roleOutcome: buildResidualIncompleteTerminalOutcome({
3026
+ role: "merger",
3027
+ candidate: residual.candidate,
3028
+ diagnostic: residual.diagnostic,
3029
+ }),
3030
+ navigator: { disposition: "no-advice" },
3031
+ artifacts: [],
3032
+ runId: admitted.runId,
3033
+ };
3034
+ }
3035
+ }
3036
+ return undefined;
3037
+ }
3038
+ const output = validateMergerOutput(roleOutcome.decisiveFacts, admitted.runId);
3039
+ const accepted = {
3040
+ kind: "accepted",
3041
+ role: "merger",
3042
+ status: roleOutcome.status,
3043
+ decisiveFacts: mergerDecisiveFacts(output),
3044
+ };
3045
+ const methodInvocations = extractMergerMethodInvocations(entries, {
3046
+ allowedLocations: [
3047
+ options.methodSkillPath,
3048
+ options.methodSkillConfiguredPath,
3049
+ ],
3050
+ });
3051
+ // Every invocation must expand the merge-only method before conflict work.
3052
+ if (methodInvocations.length === 0)
3053
+ return undefined;
3054
+ const navigator = extractNavigatorFact(entries);
3055
+ const artifacts = await publishMergerArtifacts(admitted, accepted, coordinates, {
3056
+ mergerOutput: output,
3057
+ methodProvenance: options.methodProvenance,
3058
+ methodInvocations,
3059
+ });
3060
+ return withOptionalGateProjection({
3061
+ roleOutcome: accepted,
3062
+ navigator,
3063
+ artifacts,
3064
+ runId: admitted.runId,
3065
+ }, sessionDirectory);
3066
+ }
3067
+ /** Settle a lawful Merger Terminal from the admitted session (shared #106 success interface). */
3068
+ export async function settleMergerTerminalResult(admitted, authority, options) {
3069
+ const settled = await settleLawfulMergerTerminalResult(admitted, authority, options);
3070
+ if (settled === undefined) {
3071
+ throw new Error("Merger Role run completed without a lawful typed terminal result");
3072
+ }
3073
+ return settled;
3074
+ }
3075
+ /** Try to settle a lawful Merger Terminal; undefined only for genuine absence. */
3076
+ export async function trySettleMergerTerminalResult(admitted, authority, options) {
3077
+ return settleLawfulMergerTerminalResult(admitted, authority, options);
3078
+ }
3079
+ function publicationAttemptFromError(path, error) {
3080
+ if (error instanceof Error) {
3081
+ const identity = {
3082
+ name: error.name,
3083
+ };
3084
+ const code = error.code;
3085
+ if (typeof code === "string" || typeof code === "number") {
3086
+ identity.code = code;
3087
+ }
3088
+ return {
3089
+ path,
3090
+ diagnostic: error.message || error.name || "write failed",
3091
+ identity,
3092
+ };
3093
+ }
3094
+ return { path, diagnostic: String(error) };
3095
+ }
3096
+ /**
3097
+ * Directories eligible for open-ended unique failure-artifact placement.
3098
+ * Always includes the ledger runs/ parent of the run directory so an
3099
+ * unwritable run tree cannot strand the original controlled failure.
3100
+ */
3101
+ function uniqueFailureFallbackDirs(runDirectory, baseDir) {
3102
+ const dirs = [];
3103
+ for (const dir of [baseDir, runDirectory, dirname(runDirectory)]) {
3104
+ if (!dirs.includes(dir))
3105
+ dirs.push(dir);
3106
+ }
3107
+ return dirs;
3108
+ }
3109
+ /**
3110
+ * Resolve a writable artifacts base directory. If `artifacts/` cannot be created
3111
+ * (e.g. a file occupies that name), fall back to the run directory itself.
3112
+ */
3113
+ async function resolveFailureArtifactsBase(runDirectory) {
3114
+ const artifactsDir = join(runDirectory, "artifacts");
3115
+ try {
3116
+ await ensureRunArtifactsDir(runDirectory);
3117
+ return { baseDir: artifactsDir };
3118
+ }
3119
+ catch (error) {
3120
+ return {
3121
+ baseDir: runDirectory,
3122
+ attempt: publicationAttemptFromError(artifactsDir, error),
3123
+ };
3124
+ }
3125
+ }
3126
+ /**
3127
+ * Write JSON across preferred paths, then unique open-ended fallbacks.
3128
+ * Finite fixed names must not be able to exhaust durability and strand the
3129
+ * original controlled failure outside settlement.
3130
+ */
3131
+ async function writeFailureJsonRetainingCause(preferredCandidates, uniqueFallbackDirs, stem, basePayload, priorIssues) {
3132
+ const issues = [...priorIssues];
3133
+ const candidates = [
3134
+ ...preferredCandidates,
3135
+ // One unique name per fallback dir — collisions on fixed names cannot exhaust this.
3136
+ ...uniqueFallbackDirs.map((dir) => join(dir, `${stem}.${randomUUID()}.json`)),
3137
+ ];
3138
+ for (let i = 0; i < candidates.length; i += 1) {
3139
+ const path = candidates[i];
3140
+ const payload = issues.length === 0
3141
+ ? basePayload
3142
+ : { ...basePayload, publicationIssues: issues };
3143
+ try {
3144
+ await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
3145
+ return { path, issues };
3146
+ }
3147
+ catch (error) {
3148
+ issues.push(publicationAttemptFromError(path, error));
3149
+ }
3150
+ }
3151
+ const last = issues.at(-1);
3152
+ const error = new Error(last?.diagnostic ?? "unable to write durable failure artifact");
3153
+ if (last?.identity?.name !== undefined && last.identity.name !== "") {
3154
+ error.name = last.identity.name;
3155
+ }
3156
+ if (last?.identity?.code !== undefined) {
3157
+ error.code = last.identity.code;
3158
+ }
3159
+ error.publicationAttempts = issues;
3160
+ throw error;
3161
+ }
3162
+ export async function publishFailureArtifacts(admitted, failure, authority) {
3163
+ const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
3164
+ const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(admitted.runDirectory);
3165
+ const priorIssues = baseAttempt === undefined ? [] : [baseAttempt];
3166
+ // #419: each attempt's complete failure result joins the appended history
3167
+ // before any fixed-name artifact view is rewritten. History failure must not
3168
+ // strand the original controlled failure outside settlement — it rides
3169
+ // publicationIssues instead of aborting durability.
3170
+ try {
3171
+ await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile }, {
3172
+ kind: "failure",
3173
+ role: admitted.role,
3174
+ ...failure,
3175
+ });
3176
+ }
3177
+ catch (error) {
3178
+ priorIssues.push(publicationAttemptFromError(sessionFile, error));
3179
+ }
3180
+ // Prefer conventional names; unique fallback dirs keep colliding fixed paths
3181
+ // from stranding the original failure outside settlement. Include the ledger
3182
+ // runs/ parent so a locked run directory (EACCES) cannot exhaust durability.
3183
+ const underArtifacts = baseDir === join(admitted.runDirectory, "artifacts");
3184
+ const uniqueFallbackDirs = uniqueFailureFallbackDirs(admitted.runDirectory, baseDir);
3185
+ const errorCandidates = underArtifacts
3186
+ ? [
3187
+ join(baseDir, "error.json"),
3188
+ join(baseDir, "error.settlement.json"),
3189
+ join(admitted.runDirectory, "error.settlement.json"),
3190
+ ]
3191
+ : [
3192
+ join(baseDir, "error.settlement.json"),
3193
+ join(baseDir, "error.json"),
3194
+ ];
3195
+ const evidenceCandidates = underArtifacts
3196
+ ? [
3197
+ join(baseDir, "evidence.json"),
3198
+ join(baseDir, "evidence.settlement.json"),
3199
+ join(admitted.runDirectory, "evidence.settlement.json"),
3200
+ ]
3201
+ : [
3202
+ join(baseDir, "evidence.settlement.json"),
3203
+ join(baseDir, "evidence.json"),
3204
+ ];
3205
+ const errorPayloadBase = {
3206
+ kind: "error",
3207
+ role: admitted.role,
3208
+ runId: admitted.runId,
3209
+ cause: failure.cause,
3210
+ diagnostic: failure.diagnostic,
3211
+ ...(failure.identity === undefined ? {} : { identity: failure.identity }),
3212
+ ...(failure.details === undefined ? {} : { details: failure.details }),
3213
+ };
3214
+ const errorWrite = await writeFailureJsonRetainingCause(errorCandidates, uniqueFallbackDirs, "error", errorPayloadBase, priorIssues);
3215
+ const evidencePayload = {
3216
+ runId: admitted.runId,
3217
+ sessionDirectory: sessionDirectory,
3218
+ sessionFile: sessionFile,
3219
+ admittedRequestPath: admitted.admittedRequestPath,
3220
+ attachments: admitted.attachments.map((a) => ({
3221
+ provenancePath: a.provenancePath,
3222
+ frozenPath: a.frozenPath,
3223
+ sha256: a.sha256,
3224
+ byteLength: a.byteLength,
3225
+ })),
3226
+ failureCause: failure.cause,
3227
+ };
3228
+ const evidenceWrite = await writeFailureJsonRetainingCause(evidenceCandidates, uniqueFallbackDirs, "evidence", evidencePayload,
3229
+ // Evidence records the same publication collisions observed placing the error body.
3230
+ errorWrite.issues);
3231
+ return [
3232
+ { kind: "error", path: errorWrite.path },
3233
+ { kind: "evidence", path: evidenceWrite.path },
3234
+ ];
3235
+ }
3236
+ /**
3237
+ * Durably record a controlled failure (Error Artifact first), then return the
3238
+ * Terminal aggregate. Presentation must happen only after this resolves.
3239
+ */
3240
+ /** Redact exact run ID from any string leaves inside decisive facts (arrays/objects included). */
3241
+ function redactDecisiveFactValue(value, runId) {
3242
+ if (typeof value === "string")
3243
+ return redactExactRunId(value, runId);
3244
+ if (Array.isArray(value)) {
3245
+ return value.map((entry) => redactDecisiveFactValue(entry, runId));
3246
+ }
3247
+ if (typeof value === "object" && value !== null) {
3248
+ const out = {};
3249
+ for (const [key, child] of Object.entries(value)) {
3250
+ out[key] = redactDecisiveFactValue(child, runId);
3251
+ }
3252
+ return out;
3253
+ }
3254
+ return value;
3255
+ }
3256
+ /** Redact exact run ID from decisive facts at the public Terminal boundary. */
3257
+ function redactDecisiveFactsForPublicTerminal(facts, runId) {
3258
+ const out = {};
3259
+ for (const [key, value] of Object.entries(facts)) {
3260
+ out[key] = redactDecisiveFactValue(value, runId);
3261
+ }
3262
+ return out;
3263
+ }
3264
+ /** Redact exact run ID from navigator free-text fields (reason only; commands are registry-owned). */
3265
+ function redactNavigatorFactForPublicTerminal(navigator, runId) {
3266
+ const advisoryDiagnostic = navigator.advisoryDiagnostic === undefined
3267
+ ? {}
3268
+ : { advisoryDiagnostic: redactExactRunId(navigator.advisoryDiagnostic, runId) };
3269
+ if (navigator.disposition === "recommendation") {
3270
+ return {
3271
+ ...navigator,
3272
+ ...advisoryDiagnostic,
3273
+ reason: redactExactRunId(navigator.reason, runId),
3274
+ };
3275
+ }
3276
+ if (navigator.disposition === "unavailable") {
3277
+ return {
3278
+ ...navigator,
3279
+ ...advisoryDiagnostic,
3280
+ reason: redactExactRunId(navigator.reason, runId),
3281
+ };
3282
+ }
3283
+ return { ...navigator, ...advisoryDiagnostic };
3284
+ }
3285
+ /**
3286
+ * Durably record a controlled failure (Error Artifact first), then return the
3287
+ * Terminal aggregate. Presentation must happen only after this resolves.
3288
+ */
3289
+ /**
3290
+ * Shared controlled-failure Terminal settlement (#107 ownership).
3291
+ * Role identity comes from the admitted run; no new failure classes are introduced here.
3292
+ */
3293
+ export async function settleFailureTerminalResult(admitted, failure, authority, options = {}) {
3294
+ const coordinates = coordinatesFromAdmitted(authority, admitted);
3295
+ const { sessionDirectory, sessionFile } = coordinates;
3296
+ // #288 is lawful only when the lifecycle owner persisted an exhausted,
3297
+ // current-attempt fact. Transcript reconstruction must not turn arbitrary output
3298
+ // failures (or bytes retained from a prior resume attempt) into exit zero.
3299
+ if (failure.cause === "output") {
3300
+ const entries = await readBoundSessionEntries(sessionFile).catch(() => undefined);
3301
+ if (entries !== undefined) {
3302
+ let attemptStart = 0;
3303
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
3304
+ if (entries[index]?.type === "message" && entries[index]?.message?.role === "user") {
3305
+ attemptStart = index;
3306
+ break;
3307
+ }
3308
+ }
3309
+ const lifecycleEntry = entries.slice(attemptStart).reverse().find((entry) => entry.customType === NO_RECEIPT_LIFECYCLE_ENTRY_TYPE || entry.message?.customType === NO_RECEIPT_LIFECYCLE_ENTRY_TYPE);
3310
+ const raw = lifecycleEntry?.data ?? lifecycleEntry?.message?.details;
3311
+ if (raw !== undefined) {
3312
+ try {
3313
+ const facts = parseNoReceiptLifecycleFacts(raw);
3314
+ if (facts.runPointer === admitted.runDirectory && facts.attemptPointer === `current:${admitted.runDirectory}`) {
3315
+ const decisiveFacts = facts;
3316
+ // #478: no_receipt is still a public Terminal — project accepted gate facts.
3317
+ return withOptionalGateProjection({
3318
+ roleOutcome: { kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt", ...facts, decisiveFacts },
3319
+ navigator: await extractNavigatorFactFromAdmittedSession(sessionFile),
3320
+ artifacts: [],
3321
+ runId: admitted.runId,
3322
+ }, sessionDirectory);
3323
+ }
3324
+ }
3325
+ catch { /* malformed lifecycle bytes remain the existing nonzero output failure */ }
3326
+ }
3327
+ }
3328
+ }
3329
+ // Exact-session attendance only — never infer no-advice from caller omission.
3330
+ const navigator = await extractNavigatorFactFromAdmittedSession(sessionFile);
3331
+ // Private durable artifacts retain the original diagnostic identity (including run ID).
3332
+ const artifacts = await publishFailureArtifacts(admitted, failure, authority);
3333
+ const decisiveFacts = {
3334
+ cause: failure.cause,
3335
+ diagnostic: failure.diagnostic,
3336
+ };
3337
+ if (failure.identity?.name !== undefined) {
3338
+ decisiveFacts.errorName = failure.identity.name;
3339
+ }
3340
+ if (failure.identity?.code !== undefined) {
3341
+ decisiveFacts.errorCode = failure.identity.code;
3342
+ }
3343
+ if (failure.details !== undefined) {
3344
+ decisiveFacts.secondaryEvidence = failure.details;
3345
+ }
3346
+ // Resumable failures: durable artifacts still land under the run directory, but
3347
+ // the public Terminal must not re-disclose the run ID via top-level runId,
3348
+ // path components, or untrusted free text — only resume.command may carry it
3349
+ // (AC2 / #108).
3350
+ if (options.resume !== undefined) {
3351
+ const publicDiagnostic = redactExactRunId(failure.diagnostic, admitted.runId);
3352
+ const publicFacts = redactDecisiveFactsForPublicTerminal({ ...decisiveFacts, diagnostic: publicDiagnostic }, admitted.runId);
3353
+ const roleOutcome = {
3354
+ kind: "failure",
3355
+ role: admitted.role,
3356
+ cause: failure.cause,
3357
+ diagnostic: publicDiagnostic,
3358
+ decisiveFacts: publicFacts,
3359
+ };
3360
+ // #478: resume desensitization stays; gate is additive typed fact only.
3361
+ return withOptionalGateProjection({
3362
+ roleOutcome,
3363
+ navigator: redactNavigatorFactForPublicTerminal(navigator, admitted.runId),
3364
+ artifacts: [],
3365
+ resume: options.resume,
3366
+ }, sessionDirectory);
3367
+ }
3368
+ const roleOutcome = {
3369
+ kind: "failure",
3370
+ role: admitted.role,
3371
+ cause: failure.cause,
3372
+ diagnostic: failure.diagnostic,
3373
+ decisiveFacts,
3374
+ };
3375
+ // #478: ordinary controlled failure still surfaces accepted gate facts.
3376
+ return withOptionalGateProjection({
3377
+ roleOutcome,
3378
+ navigator,
3379
+ artifacts,
3380
+ runId: admitted.runId,
3381
+ }, sessionDirectory);
3382
+ }
3383
+ /** Judge-named alias retained for #107 call sites. */
3384
+ export async function settleJudgeFailureTerminalResult(admitted, failure, authority, options = {}) {
3385
+ return settleFailureTerminalResult(admitted, failure, authority, options);
3386
+ }
3387
+ /**
3388
+ * Emit one complete failure Terminal on stdout and one concise stderr diagnostic.
3389
+ * Artifacts are already durable on the TerminalResult.
3390
+ */
3391
+ export function presentFailureTerminal(terminal, io) {
3392
+ if (terminal.roleOutcome.kind !== "failure" && terminal.roleOutcome.kind !== "no_receipt") {
3393
+ throw new TypeError("presentFailureTerminal requires a failure or no-receipt role outcome");
3394
+ }
3395
+ io.stdout(formatTerminalResult(terminal));
3396
+ if (terminal.roleOutcome.kind === "failure") {
3397
+ io.stderr(formatFailureStderrDiagnostic({
3398
+ cause: terminal.roleOutcome.cause,
3399
+ diagnostic: terminal.roleOutcome.diagnostic,
3400
+ }));
3401
+ }
3402
+ }
3403
+ function defaultNavigatorGraceSleep() {
3404
+ let timer;
3405
+ const sleep = ((ms) => new Promise((resolve) => {
3406
+ timer = setTimeout(() => {
3407
+ timer = undefined;
3408
+ resolve();
3409
+ }, ms);
3410
+ }));
3411
+ sleep.cancel = () => {
3412
+ if (timer !== undefined) {
3413
+ clearTimeout(timer);
3414
+ timer = undefined;
3415
+ }
3416
+ };
3417
+ return sleep;
3418
+ }
3419
+ /**
3420
+ * Race a promise against the post-role Navigator grace.
3421
+ * On timeout, returns the timeout sentinel; the caller records unavailable and
3422
+ * ignores or disposes late completion.
3423
+ * When work settles first, the grace sleep is canceled synchronously so its
3424
+ * timer/resource cannot keep the process alive after the race resolves.
3425
+ */
3426
+ export function raceNavigatorGrace(work, graceMs = NAVIGATOR_POST_ROLE_GRACE_MS, sleep = defaultNavigatorGraceSleep()) {
3427
+ return new Promise((resolve, reject) => {
3428
+ let settled = false;
3429
+ const finish = (action) => {
3430
+ if (settled)
3431
+ return;
3432
+ settled = true;
3433
+ sleep.cancel?.();
3434
+ action();
3435
+ };
3436
+ void work.then((value) => finish(() => resolve({ status: "done", value })), (error) => finish(() => reject(error)));
3437
+ void sleep(graceMs).then(() => {
3438
+ finish(() => resolve({ status: "timeout" }));
3439
+ });
3440
+ });
3441
+ }