@akagilnc/pi-workflow-roles 0.1.5382 → 0.1.5462

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 (87) hide show
  1. package/README.md +4 -2
  2. package/README.zh-CN.md +4 -2
  3. package/dist/acp-host/production-host.js +8843 -9012
  4. package/dist/archivist-record-pointer.js +38 -0
  5. package/dist/compliance-transport.js +7 -5
  6. package/dist/doctor-auditor.js +1 -0
  7. package/dist/engine-detour-usage.js +9 -36
  8. package/dist/gatekeeper-role.js +4 -18
  9. package/dist/headless-host/description.js +0 -209
  10. package/dist/headless-host/production-host.js +7828 -8247
  11. package/dist/host-contracts.js +8 -0
  12. package/dist/judge-role.js +14 -1
  13. package/dist/migrate-book-topology.js +215 -96
  14. package/dist/navigator-attendance.js +10 -9
  15. package/dist/navigator-invocation-identity.js +18 -0
  16. package/dist/packaged-role-registry.js +1 -9
  17. package/dist/pi/role-turn-host.js +46 -19
  18. package/dist/public-cli/auto-resume.js +29 -66
  19. package/dist/public-cli/cli.js +1 -0
  20. package/dist/public-cli/countersign-run.js +4 -18
  21. package/dist/public-cli/instruction-seat-run.js +258 -69
  22. package/dist/public-cli/main.js +7488 -7758
  23. package/dist/public-cli/post-admission.js +451 -349
  24. package/dist/public-cli/process-cancel.js +8 -0
  25. package/dist/public-cli/run-lifecycle.js +24 -94
  26. package/dist/public-cli/settlement.js +624 -1471
  27. package/dist/public-cli/terminal.js +34 -53
  28. package/dist/public-cli/turn-request.js +3 -0
  29. package/dist/public-role-summons.js +3 -8
  30. package/dist/receipt-delivery-policy.js +162 -28
  31. package/dist/run-terminal-artifacts.js +19 -14
  32. package/dist/serialize-thrown-value.js +7 -1
  33. package/dist/submission-errors.js +11 -1
  34. package/dist/submission-gate.js +27 -4
  35. package/dist/submission-ledger.js +21 -89
  36. package/package.json +1 -1
  37. package/scripts/build-package.mjs +0 -2
  38. package/src/acp-host/role-turn-host.ts +168 -75
  39. package/src/archivist-record-pointer.ts +48 -0
  40. package/src/compliance-transport.ts +13 -8
  41. package/src/doctor-auditor.ts +3 -0
  42. package/src/engine-detour-usage.ts +10 -39
  43. package/src/external-host-turn-loop.ts +100 -43
  44. package/src/gatekeeper-role.ts +7 -22
  45. package/src/headless-host/description.ts +4 -214
  46. package/src/headless-host/production-host.ts +2 -1
  47. package/src/headless-host/role-turn-host.ts +153 -186
  48. package/src/host-contracts.ts +20 -10
  49. package/src/judge-role.ts +24 -5
  50. package/src/navigator-attendance.ts +17 -12
  51. package/src/navigator-invocation-identity.ts +31 -0
  52. package/src/navigator-public-session.ts +1 -1
  53. package/src/navigator-session-contracts.ts +1 -1
  54. package/src/packaged-role-registry.ts +2 -10
  55. package/src/pi/adapter.ts +33 -11
  56. package/src/pi/role-turn-host.ts +49 -28
  57. package/src/prepared-role-turn.ts +2 -0
  58. package/src/public-cli/auto-resume.ts +27 -66
  59. package/src/public-cli/cli.ts +1 -0
  60. package/src/public-cli/countersign-run.ts +4 -22
  61. package/src/public-cli/instruction-seat-run.ts +307 -74
  62. package/src/public-cli/post-admission.ts +615 -459
  63. package/src/public-cli/process-cancel.ts +9 -0
  64. package/src/public-cli/run-lifecycle.ts +25 -126
  65. package/src/public-cli/settlement.ts +801 -1859
  66. package/src/public-cli/terminal.ts +42 -70
  67. package/src/public-cli/turn-request.ts +10 -1
  68. package/src/public-role-summons.ts +3 -8
  69. package/src/receipt-delivery-policy.ts +219 -28
  70. package/src/role-envelope.ts +21 -10
  71. package/src/role-runtime-dependencies.ts +1 -0
  72. package/src/role-runtime.ts +79 -102
  73. package/src/run-terminal-artifacts.ts +21 -17
  74. package/src/serialize-thrown-value.ts +7 -1
  75. package/src/submission-correctable-error.ts +5 -2
  76. package/src/submission-errors.ts +12 -1
  77. package/src/submission-gate.ts +34 -4
  78. package/src/submission-ledger.ts +30 -116
  79. package/src/worker-role.ts +19 -7
  80. package/src/worker-submission-gates.ts +29 -7
  81. package/dist/audit-escalation.js +0 -7
  82. package/dist/pi/known-failure.js +0 -51
  83. package/dist/typed-provider-http.js +0 -57
  84. package/dist/upstream-error-testimony.js +0 -19
  85. package/src/pi/known-failure.ts +0 -76
  86. package/src/typed-provider-http.ts +0 -89
  87. package/src/upstream-error-testimony.ts +0 -45
@@ -6,7 +6,7 @@
6
6
  import { dirname, resolve } from "node:path";
7
7
  import { isUnboundRunDirectory } from "../role-run-placement.js";
8
8
  import { AUDITOR_DOSSIER_PROMPT } from "../compliance-transport.js";
9
- import { bookDirectOfficerRunPointer } from "../archivist-record-pointer.js";
9
+ import { bookDirectOfficerRunPointer, readDirectOfficerRunPointer } from "../archivist-record-pointer.js";
10
10
  import { createPiDoctorAuditor } from "../doctor-auditor.js";
11
11
  import { officerConclusionReask, gateOfficerForSubject } from "../gatekeeper-role.js";
12
12
  import { receivedDiscriminator } from "../submission-errors.js";
@@ -15,6 +15,7 @@ import { runJudgeGates } from "../judge-role.js";
15
15
  import { WORKER_DONE_STATUSES } from "../worker-submission-contracts.js";
16
16
  import { SECRETARIAT_GATE_OFFICER_ENTRY_TYPE } from "../secretariat-contracts.js";
17
17
  import { readableGateItem } from "../readable-gate-item.js";
18
+ import { deliveryLimitFromConfig } from "../receipt-delivery-policy.js";
18
19
  import { runIdFromRunDirectory } from "../run-terminal-artifacts.js";
19
20
  import { createDefaultGateOfficerSummon, requireSubmissionGate } from "../submission-gate.js";
20
21
  import { readRecordedSubmissionRows } from "../submission-ledger.js";
@@ -28,13 +29,14 @@ import { packagedAdmitsCountersign, packagedBindsBoardTicket, packagedRebindSour
28
29
  import { readAuditorResumeBinding } from "../auditor-soul.js";
29
30
  import { CliUsageError } from "./cli-errors.js";
30
31
  import { admitPublicRole, bindAdmittedTicketNumber, buildInstructionTransportPrompt, materializeCountersignInvocation, persistAdmittedSourceRunPath, recordAdmittedCorrelation, recordChildDiaristRun, relocateAdmittedRunToTicket, withPreparedAttachments, } from "./invocation.js";
31
- import { presentControlledFailure, prepareSummonsResumeMaterials, roleTurnOptions, runPostAdmissionOneShot, runPostAdmissionResumable, runPostAdmissionSeatResume, showResumeErrorPointer, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
32
- import { buildAutoResumeContinuationPrompt, loadResumablePublicRole, markRunAdmitted, parentRunPathFromGatePointerInstruction, } from "./run-lifecycle.js";
32
+ import { presentControlledFailure, packageFaultNoteFor, prepareSummonsResumeMaterials, roleTurnOptions, runPostAdmissionOneShot, runPostAdmissionResumable, runPostAdmissionSeatResume, showResumeErrorPointer, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
33
+ import { presentTerminal } from "./auto-resume.js";
34
+ import { buildAutoResumeContinuationPrompt, loadResumablePublicRole, markRunAdmitted, parentRunPathFromGatePointerInstruction, readCurrentCourt, } from "./run-lifecycle.js";
33
35
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
34
- import { presentStructuralRejection, readBoundSessionEntries, seatKnownFailureResolver, attachPostAuditProjection, trySettlePublicSeat, } from "./settlement.js";
36
+ import { presentStructuralRejection, readBoundSessionEntries, attachPostAuditProjection, trySettlePublicSeat, } from "./settlement.js";
35
37
  import { warnMissingMethodSkills } from "./machine-method-skills.js";
36
38
  import { formatTerminalResult, isLawfulTypedTerminalOutcome, } from "./terminal.js";
37
- import { admittedSeatTurnDetails, projectPublicTurnAxes, projectRoleTurnRequest, requiredMethodSkills, } from "./turn-request.js";
39
+ import { admittedSeatTurnDetails, projectRoleTurnRequest, requiredMethodSkills, } from "./turn-request.js";
38
40
  import { invokeCourtDiarist, latestPayloadEscalated, } from "./countersign-run.js";
39
41
  import { isRecord, errorText } from "../unknown-value.js";
40
42
  const AUDITED_ROLES = new Set(["judge", "fixer", "coder", "secretariat", "countersign", "doctor"]);
@@ -80,11 +82,81 @@ function unreadablePostSubmissionStatus(admitted, terminal) {
80
82
  ? undefined
81
83
  : route.reask ?? officerConclusionReask(status);
82
84
  }
83
- async function reaskUnreadablePostSubmissionStatus(admitted, terminal, env, io) {
84
- const message = unreadablePostSubmissionStatus(admitted, terminal);
85
+ /**
86
+ * One more unreadable-status reask. This loop has its own counter and reads
87
+ * the same autoResumeLimit as the other loops. Exhaustion is not a readable
88
+ * status: the caller presents the volumes already in hand and does not advance
89
+ * review. ADR 0007 audit continues are not this shape.
90
+ */
91
+ function withUnreadableReask(env) {
92
+ const spent = env.unreadableReasksSpent ?? 0;
93
+ if (spent >= deliveryLimitFromConfig(env.autoResumeLimit))
94
+ return undefined;
95
+ return { ...env, unreadableReasksSpent: spent + 1 };
96
+ }
97
+ /** Budget spent on an unreadable status. Distinct from "status is readable". */
98
+ const UNREADABLE_REASK_EXHAUSTED = { unreadableReaskExhausted: true };
99
+ function isUnreadableReaskExhausted(value) {
100
+ return value === UNREADABLE_REASK_EXHAUSTED;
101
+ }
102
+ async function presentOriginalVolume(held, terminal, io, runDirectory) {
103
+ if (terminal !== undefined && runDirectory !== undefined) {
104
+ // Direction facts were attached after the held volume was formatted.
105
+ await presentTerminal(terminal, { ...io, omitFailureStderrDiagnostic: true }, runDirectory, terminal.roleOutcome.decisiveFacts?.directionUnsettled === true ? undefined : held);
106
+ return;
107
+ }
108
+ // No settled host report exists to preserve at this pre-result boundary.
109
+ for (const value of held)
110
+ io.stdout(value);
111
+ }
112
+ /** Runtime fact beside the original volume. Payloads stay as submitted. */
113
+ function withUnsettledDirection(parent, officer) {
114
+ const outcome = parent.roleOutcome;
115
+ if (outcome.kind !== "accepted" && outcome.kind !== "audit_escalation")
116
+ return parent;
117
+ const officerOutcome = officer?.roleOutcome;
118
+ const officerPayloads = officer?.submissions !== undefined && officer.submissions.length > 0
119
+ ? officer.submissions
120
+ : officerOutcome !== undefined && officerOutcome.kind !== "no_receipt"
121
+ ? officerOutcome.payloads ?? []
122
+ : [];
123
+ return {
124
+ ...parent,
125
+ roleOutcome: {
126
+ ...outcome,
127
+ decisiveFacts: {
128
+ ...(outcome.decisiveFacts ?? {}),
129
+ directionUnsettled: true,
130
+ subsequentAudit: "incomplete",
131
+ ...(officerOutcome === undefined ? {} : { officerRole: officerOutcome.role }),
132
+ ...(officer?.runId === undefined ? {} : { officerRunId: officer.runId }),
133
+ ...(officerPayloads.length === 0 ? {} : { officerPayloads }),
134
+ },
135
+ },
136
+ };
137
+ }
138
+ function heldUnreadableTerminal(terminal) {
139
+ if (terminal?.roleOutcome.kind !== "accepted")
140
+ return false;
141
+ if (latestPayloadEscalated(terminal.roleOutcome))
142
+ return false;
143
+ const status = latestQueueStatus(terminal);
144
+ return typeof status !== "string" || !REVIEW_QUEUE_STATUSES.has(status);
145
+ }
146
+ async function presentUnsettledAudit(parent, officer, io, runDirectory) {
147
+ const terminal = withUnsettledDirection(parent, officer);
148
+ await presentTerminal(terminal, { ...io, omitFailureStderrDiagnostic: true }, runDirectory);
149
+ if (officer !== undefined)
150
+ await presentTerminal(officer, { ...io, omitFailureStderrDiagnostic: true }, runDirectory);
151
+ return terminal;
152
+ }
153
+ async function reaskUnreadablePostSubmissionStatus(admitted, terminal, env, io, message = unreadablePostSubmissionStatus(admitted, terminal)) {
85
154
  if (message === undefined)
86
155
  return undefined;
87
- return runPublicInstructionSeatResume({ runId: admitted.runId, message }, { ...env, autoResumeLimit: 0 }, io);
156
+ const next = withUnreadableReask(env);
157
+ if (next === undefined)
158
+ return UNREADABLE_REASK_EXHAUSTED;
159
+ return runPublicInstructionSeatResume({ runId: admitted.runId, summons: { instruction: message } }, next, io);
88
160
  }
89
161
  function roleRecord(role) {
90
162
  const record = packagedRoleMetadata(role);
@@ -108,10 +180,6 @@ function initialPrompt(admitted, env) {
108
180
  packageRoot: env.packageRoot,
109
181
  }));
110
182
  }
111
- function infraFailure(role) {
112
- const resolveRunnerKnownFailure = seatKnownFailureResolver(role);
113
- return resolveRunnerKnownFailure === undefined ? {} : { resolveRunnerKnownFailure };
114
- }
115
183
  function isBoardTicketSeat(seat) {
116
184
  return packagedBindsBoardTicket(seat.role);
117
185
  }
@@ -124,6 +192,9 @@ async function bindAndRelocateDiarist(admitted, authority, lease) {
124
192
  }
125
193
  return await relocateAdmittedRunToTicket(admitted, authority, lease);
126
194
  }
195
+ function packageFaultScope(admitted, env, io) {
196
+ return { notePackageFault: packageFaultNoteFor(admitted, env, io) };
197
+ }
127
198
  function seatAdapters(admitted, env) {
128
199
  const record = roleRecord(admitted.role);
129
200
  const present = "presentSettled" in record ? record.presentSettled : "default";
@@ -133,7 +204,6 @@ function seatAdapters(admitted, env) {
133
204
  ...(present === "typed"
134
205
  ? { shouldPresentSettled: (terminal) => isLawfulTypedTerminalOutcome(terminal.roleOutcome) }
135
206
  : {}),
136
- ...infraFailure(admitted.role),
137
207
  ...(packagedBindsBoardTicket(admitted.role)
138
208
  ? {
139
209
  beforeDispatch: async (seat, lease) => {
@@ -232,13 +302,19 @@ async function dispatchAdmitted(admitted, env, io) {
232
302
  };
233
303
  const result = await execute(env);
234
304
  const unreadableReask = await reaskUnreadablePostSubmissionStatus(admitted, result.terminal, env, io);
305
+ if (isUnreadableReaskExhausted(unreadableReask)) {
306
+ const terminal = result.terminal === undefined
307
+ ? undefined
308
+ : withUnsettledDirection(result.terminal);
309
+ await presentOriginalVolume(held, terminal, io, admitted.runDirectory);
310
+ return terminal === undefined ? result : { ...result, terminal };
311
+ }
235
312
  if (unreadableReask !== undefined)
236
313
  return unreadableReask;
237
314
  if (AUDITED_ROLES.has(admitted.role) && result.terminal?.roleOutcome.kind === "accepted") {
238
315
  return auditSubmittedRole(result, env, io);
239
316
  }
240
- for (const value of held)
241
- io.stdout(value);
317
+ await presentOriginalVolume(held, result.terminal, io, admitted.runDirectory);
242
318
  return result;
243
319
  }
244
320
  /**
@@ -306,7 +382,11 @@ async function runOmittedLensBatch(argv, parsed, env, io) {
306
382
  },
307
383
  artifacts: terminals.flatMap((item) => item.artifacts),
308
384
  };
309
- io.stdout(formatTerminalResult(terminal));
385
+ const childRunDirectory = children.completeness.admitted?.runDirectory ?? children.correctness.admitted?.runDirectory;
386
+ if (childRunDirectory !== undefined)
387
+ await presentTerminal(terminal, { ...io, omitFailureStderrDiagnostic: true }, childRunDirectory);
388
+ else
389
+ io.stdout(formatTerminalResult(terminal));
310
390
  return { exitCode: failed ? 1 : 0, terminal };
311
391
  }
312
392
  /**
@@ -387,20 +467,14 @@ async function runCountersignBody(parsed, env, io) {
387
467
  await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
388
468
  admitted = { ...admitted, sourceRunPath: gateParentRunPath };
389
469
  }
390
- const turnProjection = {
391
- ...projectPublicTurnAxes(env),
392
- ...(env.correlationId === undefined || env.correlationId.trim() === ""
393
- ? {}
394
- : { correlationId: env.correlationId }),
395
- continuation: {
396
- kind: "initial",
397
- prompt: (env.reviewReask ?? env.gateReviewInstruction)
398
- ?? buildInstructionTransportPrompt(admitted, engineSessionMaterialFromOptions({
399
- ...pickEngineAxis(env),
400
- packageRoot: env.packageRoot,
401
- })),
402
- },
403
- };
470
+ const turnProjection = roleTurnOptions(env, admitted, {
471
+ kind: "initial",
472
+ prompt: (env.reviewReask ?? env.gateReviewInstruction)
473
+ ?? buildInstructionTransportPrompt(admitted, engineSessionMaterialFromOptions({
474
+ ...pickEngineAxis(env),
475
+ packageRoot: env.packageRoot,
476
+ })),
477
+ });
404
478
  const turnRequest = buildInstructionSeatTurnRequest(admitted, turnProjection);
405
479
  const result = await runPostAdmissionOneShot({
406
480
  admitted,
@@ -448,8 +522,7 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
448
522
  });
449
523
  if (result.terminal?.roleOutcome.kind === "accepted")
450
524
  return auditSubmittedRole(result, env, io);
451
- for (const value of held)
452
- io.stdout(value);
525
+ await presentOriginalVolume(held, result.terminal, io, result.admitted?.runDirectory);
453
526
  return result;
454
527
  }
455
528
  if ("parallelLenses" in record && record.parallelLenses === true) {
@@ -611,7 +684,7 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
611
684
  catch (error) {
612
685
  return await presentControlledFailure(admitted, {
613
686
  timedOut: false, code: null, stderr: "", thrown: error,
614
- }, seatAdapters(admitted, env), env.principalAuthority, io);
687
+ }, seatAdapters(admitted, env), env.principalAuthority, io, true, packageFaultNoteFor(admitted, env, io));
615
688
  }
616
689
  if (outcome.admitted !== undefined && isUnboundRunDirectory(outcome.admitted.runDirectory)) {
617
690
  await recordChildDiaristRun(admitted, outcome.admitted.runId);
@@ -620,12 +693,12 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
620
693
  return await presentControlledFailure(admitted, {
621
694
  timedOut: false, code: null, stderr: "",
622
695
  thrown: new Error(outcome.failedWithoutEscalate.diagnostic),
623
- }, seatAdapters(admitted, env), env.principalAuthority, io);
696
+ }, seatAdapters(admitted, env), env.principalAuthority, io, true, packageFaultNoteFor(admitted, env, io));
624
697
  }
625
698
  // The diarist's escalation pauses only the diarist's own run; call order
626
699
  // belongs to the caller, so the Secretariat still takes its own turn.
627
700
  if (outcome.identity.kind === "escalate" && outcome.terminal !== undefined) {
628
- io.stdout(formatTerminalResult(outcome.terminal));
701
+ await presentTerminal(outcome.terminal, { ...io, omitFailureStderrDiagnostic: true }, outcome.admitted?.runDirectory ?? admitted.runDirectory);
629
702
  }
630
703
  }
631
704
  return dispatchAdmitted(admitted, env, io);
@@ -684,14 +757,20 @@ export async function runPublicInstructionSeatResume(request, env, io) {
684
757
  const unreadableReask = result.admitted === undefined
685
758
  ? undefined
686
759
  : await reaskUnreadablePostSubmissionStatus(result.admitted, result.terminal, env, io);
760
+ if (isUnreadableReaskExhausted(unreadableReask)) {
761
+ const terminal = result.terminal === undefined
762
+ ? undefined
763
+ : withUnsettledDirection(result.terminal);
764
+ await presentOriginalVolume(held, terminal, io, result.admitted?.runDirectory);
765
+ return terminal === undefined ? result : { ...result, terminal };
766
+ }
687
767
  if (unreadableReask !== undefined)
688
768
  return unreadableReask;
689
769
  if (result.admitted !== undefined && AUDITED_ROLES.has(result.admitted.role)
690
770
  && result.terminal?.roleOutcome.kind === "accepted") {
691
771
  return auditSubmittedRole(result, env, io);
692
772
  }
693
- for (const value of held)
694
- io.stdout(value);
773
+ await presentOriginalVolume(held, result.terminal, io, result.admitted?.runDirectory);
695
774
  return result;
696
775
  };
697
776
  let binding;
@@ -717,23 +796,42 @@ export async function runPublicInstructionSeatResume(request, env, io) {
717
796
  const GATE_CHILD_ROLES = new Set(["notary", "auditor", "inspector", "countersign"]);
718
797
  /**
719
798
  * A gate child's conclusion outside the three states goes back to that child.
720
- * The words are the existing officer re-ask. A host failure stops here.
799
+ * The words are the existing officer re-ask. Each reask spends this loop's own
800
+ * copy of the configured ceiling; exhaustion keeps the terminal already in hand.
801
+ * A host failure stops here.
802
+ * Countersign already spends that ceiling inside its own seat, or the summoning
803
+ * gate does. This loop does not open a second one for the same conclusion.
721
804
  */
722
805
  async function queueConclusionFromChild(child, env, io) {
723
806
  if (!GATE_CHILD_ROLES.has(child.role))
724
807
  return undefined;
808
+ if (child.role === "countersign") {
809
+ const terminal = await trySettlePublicSeat(child, env.principalAuthority, { ...await readCurrentCourt(child.runDirectory), ...packageFaultScope(child, env, io) });
810
+ const status = latestQueueStatus(terminal);
811
+ if (terminal !== undefined && typeof status === "string" && REVIEW_QUEUE_STATUSES.has(status)) {
812
+ return { admitted: child, terminal, status };
813
+ }
814
+ return { stop: { exitCode: 0, ...(terminal === undefined ? {} : { terminal }) } };
815
+ }
725
816
  let current = child;
817
+ // Notary, auditor, and inspector have no seat-local status reask. This loop
818
+ // is their one budget. The child resume keeps the caller's env so delivery,
819
+ // failure recovery, and other reask loops stay separate.
820
+ let budgetEnv = { ...env, unreadableReasksSpent: 0 };
726
821
  for (;;) {
727
- const terminal = await trySettlePublicSeat(current, env.principalAuthority, undefined);
822
+ const terminal = await trySettlePublicSeat(current, env.principalAuthority, { ...await readCurrentCourt(current.runDirectory), ...packageFaultScope(current, env, io) });
728
823
  const status = latestQueueStatus(terminal);
729
824
  if (terminal !== undefined && typeof status === "string" && REVIEW_QUEUE_STATUSES.has(status)) {
730
825
  return { admitted: current, terminal, status };
731
826
  }
732
- const reasked = await runPublicInstructionSeatResume({
733
- runId: current.runId,
734
- message: officerConclusionReask(status),
735
- }, env, io);
736
- if (reasked.exitCode !== 0 || reasked.admitted === undefined || reasked.terminal === undefined) {
827
+ const reasked = await reaskUnreadablePostSubmissionStatus(current, terminal, budgetEnv, io, officerConclusionReask(status));
828
+ if (reasked === undefined || isUnreadableReaskExhausted(reasked)) {
829
+ return { stop: { exitCode: 0, ...(terminal === undefined ? {} : { terminal }) } };
830
+ }
831
+ budgetEnv = withUnreadableReask(budgetEnv);
832
+ if (reasked.exitCode !== 0 || reasked.admitted === undefined || reasked.terminal === undefined
833
+ || reasked.terminal.roleOutcome.kind === "no_receipt"
834
+ || reasked.terminal.roleOutcome.decisiveFacts?.directionUnsettled === true) {
737
835
  return { stop: reasked };
738
836
  }
739
837
  const reaskedStatus = latestQueueStatus(reasked.terminal);
@@ -753,29 +851,72 @@ export async function continueParentAfterChild(parentRunId, child, env, io) {
753
851
  }
754
852
  const admitted = loaded.admitted;
755
853
  const resolved = await queueConclusionFromChild(child, env, io);
756
- if (resolved !== undefined && "stop" in resolved)
757
- return resolved.stop;
854
+ if (resolved !== undefined && "stop" in resolved) {
855
+ const stopped = resolved.stop;
856
+ if (stopped.exitCode === 0
857
+ && heldUnreadableTerminal(stopped.terminal)
858
+ && stopped.terminal !== undefined) {
859
+ const parentTerminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
860
+ if (parentTerminal?.roleOutcome.kind === "accepted"
861
+ || parentTerminal?.roleOutcome.kind === "audit_escalation") {
862
+ const terminal = await presentUnsettledAudit(parentTerminal, stopped.terminal, io, admitted.runDirectory);
863
+ return { exitCode: 0, admitted, terminal };
864
+ }
865
+ }
866
+ return stopped;
867
+ }
758
868
  if (resolved !== undefined && resolved.status === "escalate") {
759
869
  return { exitCode: 0, admitted: resolved.admitted, terminal: resolved.terminal };
760
870
  }
761
871
  if (resolved !== undefined && (resolved.status === "continue" || resolved.status === "converged")) {
762
872
  if (AUDITED_ROLES.has(admitted.role) && resolved.status === "converged") {
763
- const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
873
+ const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, packageFaultScope(admitted, env, io));
764
874
  if (terminal?.roleOutcome.kind !== "accepted")
765
875
  throw new Error("pending submission is not recorded");
766
- return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted.role, latestQueuePayload(resolved.terminal), resolved.admitted.runId);
876
+ return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted, latestQueuePayload(resolved.terminal));
767
877
  }
768
878
  return runPublicInstructionSeatResume({
769
879
  runId: parentRunId,
770
- message: readableGateItem(latestQueuePayload(resolved.terminal)),
771
- }, { ...env, autoResumeLimit: 0 }, io);
880
+ summons: { instruction: readableGateItem(latestQueuePayload(resolved.terminal)) },
881
+ }, { ...env, unreadableReasksSpent: 0 }, io);
772
882
  }
773
883
  const result = await dispatchAdmitted(admitted, env, { ...io, omitFailureStderrDiagnostic: true });
774
884
  showResumeErrorPointer(io, result.exitCode, result.terminal);
775
885
  return result;
776
886
  }
887
+ /**
888
+ * A gate is already passed only when an officer run reviewed this submission
889
+ * and its latest queue word is converged. The resumed seat's role is not itself
890
+ * a pass. Unknown or unreadable evidence does not skip the gate.
891
+ */
892
+ async function officerPassedCurrentSubmission(input) {
893
+ const { env, officer } = input;
894
+ const pointer = await readDirectOfficerRunPointer(input.parentSessionFile, officer);
895
+ if (pointer?.runDirectory === undefined)
896
+ return false;
897
+ if (pointer.submissionToolCallId !== input.toolCallId)
898
+ return false;
899
+ const officerRunId = runIdFromRunDirectory(pointer.runDirectory);
900
+ if (officerRunId === undefined)
901
+ return false;
902
+ let admitted;
903
+ try {
904
+ const loaded = await loadResumablePublicRole(env.home, officerRunId, env.principalAuthority);
905
+ admitted = loaded.admitted;
906
+ }
907
+ catch {
908
+ return false;
909
+ }
910
+ if (admitted.role !== officer)
911
+ return false;
912
+ // A summon binding identifies the requested review, not its outcome. An open
913
+ // court (including a failed host turn) must not borrow a previous court's seal.
914
+ const court = await readCurrentCourt(admitted.runDirectory);
915
+ const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, court);
916
+ return latestQueueStatus(terminal) === "converged";
917
+ }
777
918
  /** Finished submissions enter the existing audit gate after their tool call has returned. */
778
- async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, passedRunId) {
919
+ async function auditSubmittedRole(turn, env, io, resumedOfficer, passedReceipt) {
779
920
  const admitted = turn.admitted;
780
921
  if (admitted === undefined || !AUDITED_ROLES.has(admitted.role)
781
922
  || turn.terminal?.roleOutcome.kind !== "accepted")
@@ -786,11 +927,30 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
786
927
  const record = isRecord(accepted)
787
928
  ? accepted : undefined;
788
929
  const status = admitted.role === "secretariat" ? record?.secretariatStatus : record?.status;
930
+ // A gate summon already owns this conclusion's reasks. The seat must not
931
+ // start another budget on the same run (parentRunPath is that summon).
932
+ const gateOwnsStatusReask = typeof env.parentRunPath === "string" && env.parentRunPath.trim() !== "";
789
933
  if (admitted.role === "secretariat" && status !== "converged" && status !== "escalate") {
790
- return runPublicInstructionSeatResume({ runId: admitted.runId, message: SECRETARIAT_STATUS_REASK }, { ...env, autoResumeLimit: 0 }, io);
934
+ if (gateOwnsStatusReask)
935
+ return turn;
936
+ const next = withUnreadableReask(env);
937
+ if (next === undefined) {
938
+ const terminal = withUnsettledDirection(turn.terminal);
939
+ await presentTerminal(terminal, { ...io, omitFailureStderrDiagnostic: true }, admitted.runDirectory);
940
+ return { ...turn, terminal };
941
+ }
942
+ return runPublicInstructionSeatResume({ runId: admitted.runId, summons: { instruction: SECRETARIAT_STATUS_REASK } }, next, io);
791
943
  }
792
944
  if (admitted.role === "countersign" && (typeof status !== "string" || !REVIEW_QUEUE_STATUSES.has(status))) {
793
- return runPublicInstructionSeatResume({ runId: admitted.runId, message: officerConclusionReask(status) }, { ...env, autoResumeLimit: 0 }, io);
945
+ if (gateOwnsStatusReask)
946
+ return turn;
947
+ const next = withUnreadableReask(env);
948
+ if (next === undefined) {
949
+ const terminal = withUnsettledDirection(turn.terminal);
950
+ await presentTerminal(terminal, { ...io, omitFailureStderrDiagnostic: true }, admitted.runDirectory);
951
+ return { ...turn, terminal };
952
+ }
953
+ return runPublicInstructionSeatResume({ runId: admitted.runId, summons: { instruction: officerConclusionReask(status) } }, next, io);
794
954
  }
795
955
  // Self-escalation is a valid open routing state for these seats only.
796
956
  // Doctor's declared domain is completed|refused — an open "escalate" must
@@ -802,7 +962,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
802
962
  || ((admitted.role === "fixer" || admitted.role === "coder")
803
963
  && (typeof status !== "string" || !WORKER_DONE_STATUSES.has(status)));
804
964
  if (skipAudit) {
805
- io.stdout(formatTerminalResult(turn.terminal));
965
+ await presentTerminal(turn.terminal, { ...io, omitFailureStderrDiagnostic: true }, admitted.runDirectory);
806
966
  return turn;
807
967
  }
808
968
  const rows = await readRecordedSubmissionRows(admitted.projectRoot, admitted.runId, env.home);
@@ -811,7 +971,9 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
811
971
  if (toolCallId === undefined)
812
972
  throw new Error("accepted submission has no tool call identity");
813
973
  const sessionFile = env.principalAuthority.decode(admitted.principal).sessionFile;
814
- const entries = admitted.role === "doctor" ? await readBoundSessionEntries(sessionFile) : [];
974
+ const entries = admitted.role === "doctor"
975
+ ? await readBoundSessionEntries(sessionFile)
976
+ : [];
815
977
  const context = {
816
978
  cwd: admitted.projectRoot,
817
979
  mode: "print",
@@ -827,11 +989,18 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
827
989
  },
828
990
  abort() { },
829
991
  };
992
+ const passedThisSubmission = (officer) => officerPassedCurrentSubmission({
993
+ env,
994
+ parentSessionFile: sessionFile,
995
+ officer,
996
+ toolCallId,
997
+ });
830
998
  if (admitted.role === "doctor") {
831
- if (passedOfficer !== "auditor") {
999
+ if (!await passedThisSubmission("auditor")) {
832
1000
  let lastSummon;
833
1001
  const decision = await createPiDoctorAuditor()({
834
1002
  context, submission: accepted,
1003
+ ...(env.autoResumeLimit === undefined ? {} : { autoResumeLimit: env.autoResumeLimit }),
835
1004
  ...(env.signal === undefined ? {} : { signal: env.signal }),
836
1005
  summonAuditor: async (subject, sourceRunDirectory, signal, reask, submission) => {
837
1006
  const summoned = await summonPublicRole({
@@ -851,6 +1020,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
851
1020
  if (officerSession !== undefined) {
852
1021
  bookDirectOfficerRunPointer({
853
1022
  parentSessionFile: sessionFile, officer: "auditor", sessionFile: officerSession,
1023
+ submissionToolCallId: toolCallId,
854
1024
  ...(summoned.runDirectory === undefined ? {} : { runDirectory: summoned.runDirectory }),
855
1025
  });
856
1026
  }
@@ -860,12 +1030,21 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
860
1030
  });
861
1031
  if (decision.status === "continue") {
862
1032
  return runPublicInstructionSeatResume({
863
- runId: admitted.runId, message: readableGateItem(decision.receipt ?? decision.violations),
864
- }, { ...env, autoResumeLimit: 0 }, io);
1033
+ runId: admitted.runId, summons: { instruction: readableGateItem(decision.receipt ?? decision.violations) },
1034
+ }, { ...env, unreadableReasksSpent: 0 }, io);
1035
+ }
1036
+ if (decision.status === "received") {
1037
+ if (lastSummon?.terminal === undefined) {
1038
+ throw new Error("doctor audit kept an unreadable reply with no terminal");
1039
+ }
1040
+ if (turn.terminal === undefined)
1041
+ throw new Error("doctor submission has no terminal");
1042
+ const terminal = await presentUnsettledAudit(turn.terminal, lastSummon.terminal, io, admitted.runDirectory);
1043
+ return { exitCode: 0, admitted, terminal };
865
1044
  }
866
1045
  if (decision.status !== "converged") {
867
1046
  if (lastSummon?.terminal !== undefined) {
868
- io.stdout(formatTerminalResult(lastSummon.terminal));
1047
+ await presentTerminal(lastSummon.terminal, { ...io, omitFailureStderrDiagnostic: true }, lastSummon.admitted?.runDirectory ?? admitted.runDirectory);
869
1048
  return { exitCode: lastSummon.exitCode,
870
1049
  ...(lastSummon.admitted === undefined ? {} : { admitted: lastSummon.admitted }),
871
1050
  terminal: lastSummon.terminal };
@@ -883,13 +1062,14 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
883
1062
  ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
884
1063
  });
885
1064
  let chain;
886
- let officerRunId = passedRunId;
1065
+ let officerRunId = resumedOfficer?.runId;
887
1066
  const runGate = (subject) => requireSubmissionGate({
888
1067
  context,
889
1068
  subject,
890
1069
  toolCallId,
891
1070
  submission: accepted,
892
1071
  summonOfficer,
1072
+ ...(env.autoResumeLimit === undefined ? {} : { autoResumeLimit: env.autoResumeLimit }),
893
1073
  ...(env.signal === undefined ? {} : { signal: env.signal }),
894
1074
  hostActions: {
895
1075
  failInfrastructure(error) { throw error; },
@@ -898,8 +1078,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
898
1078
  });
899
1079
  if (admitted.role === "judge") {
900
1080
  chain = await runJudgeGates({
901
- gateAlreadyConverged: async (subject) => passedOfficer === "auditor"
902
- || (passedOfficer === "notary" && subject.kind === "judge_draft"),
1081
+ gateAlreadyConverged: async (subject) => passedThisSubmission(gateOfficerForSubject(subject)),
903
1082
  runGate,
904
1083
  });
905
1084
  }
@@ -910,7 +1089,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
910
1089
  ? { kind: "secretariat_verdict" }
911
1090
  : { kind: "countersign_verdict" };
912
1091
  const officer = gateOfficerForSubject(subject);
913
- if (passedOfficer === officer) {
1092
+ if (await passedThisSubmission(officer)) {
914
1093
  chain = { status: "converged", passes: [] };
915
1094
  }
916
1095
  else {
@@ -926,10 +1105,20 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
926
1105
  receipt: pass.receipt,
927
1106
  ...(pass.runId === undefined ? {} : { runId: pass.runId }),
928
1107
  ...(pass.runDirectory === undefined ? {} : { runDirectory: pass.runDirectory }),
1108
+ ...(pass.terminal === undefined ? {} : { terminal: pass.terminal }),
929
1109
  }],
930
1110
  };
931
1111
  }
932
1112
  }
1113
+ if (chain.status === "needs_reask") {
1114
+ const officerTerminal = chain.passes.at(-1)?.terminal;
1115
+ if (officerTerminal === undefined)
1116
+ throw new Error("unreadable audit has no terminal result");
1117
+ if (turn.terminal === undefined)
1118
+ throw new Error("audited submission has no terminal");
1119
+ const terminal = await presentUnsettledAudit(turn.terminal, officerTerminal, io, admitted.runDirectory);
1120
+ return { exitCode: 0, admitted, terminal };
1121
+ }
933
1122
  if (chain.status === "escalate") {
934
1123
  const escalation = chain.passes.at(-1);
935
1124
  const escalatedRunId = escalation?.runId
@@ -938,17 +1127,17 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
938
1127
  if (escalatedRunId === undefined)
939
1128
  throw new Error("escalated audit has no resumable run identity");
940
1129
  const officer = await loadResumablePublicRole(env.home, escalatedRunId, env.principalAuthority);
941
- const terminal = await trySettlePublicSeat(officer.admitted, env.principalAuthority, undefined);
1130
+ const terminal = await trySettlePublicSeat(officer.admitted, env.principalAuthority, { ...await readCurrentCourt(officer.admitted.runDirectory), ...packageFaultScope(officer.admitted, env, io) });
942
1131
  if (terminal === undefined)
943
1132
  throw new Error("escalated audit has no terminal result");
944
- io.stdout(formatTerminalResult(terminal));
1133
+ await presentTerminal(terminal, { ...io, omitFailureStderrDiagnostic: true }, officer.admitted.runDirectory);
945
1134
  return { exitCode: 0, admitted: officer.admitted, terminal };
946
1135
  }
947
1136
  if (chain.status === "continue") {
948
1137
  return runPublicInstructionSeatResume({
949
1138
  runId: admitted.runId,
950
- message: readableGateItem(chain.passes.at(-1)?.receipt),
951
- }, { ...env, autoResumeLimit: 0 }, io);
1139
+ summons: { instruction: readableGateItem(chain.passes.at(-1)?.receipt) },
1140
+ }, { ...env, unreadableReasksSpent: 0 }, io);
952
1141
  }
953
1142
  if (admitted.role === "secretariat") {
954
1143
  const pass = chain.passes.at(-1);
@@ -966,8 +1155,8 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
966
1155
  // settle would republish the same attempt.
967
1156
  if (turn.terminal === undefined)
968
1157
  throw new Error(`audited ${admitted.role} submission did not settle`);
969
- const terminal = await attachPostAuditProjection(admitted, env.principalAuthority, turn.terminal);
1158
+ const terminal = await attachPostAuditProjection(admitted, env.principalAuthority, turn.terminal, packageFaultScope(admitted, env, io));
970
1159
  turn = { ...turn, terminal };
971
- io.stdout(formatTerminalResult(terminal));
1160
+ await presentTerminal(terminal, { ...io, omitFailureStderrDiagnostic: true }, admitted.runDirectory);
972
1161
  return turn;
973
1162
  }