@akagilnc/pi-workflow-roles 0.1.5375 → 0.1.5406

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 (79) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/dist/acp-host/production-host.js +6381 -5684
  4. package/dist/archivist-record-pointer.js +38 -0
  5. package/dist/collector-tool-schemas.js +18 -0
  6. package/dist/compliance-transport.js +5 -0
  7. package/dist/diarist-contracts.js +26 -0
  8. package/dist/doctor-auditor.js +1 -0
  9. package/dist/engine-detour-usage.js +6 -6
  10. package/dist/filed-submission.js +28 -0
  11. package/dist/gatekeeper-role.js +6 -10
  12. package/dist/gleaner-left-contracts.js +16 -0
  13. package/dist/headless-host/production-host.js +1644 -947
  14. package/dist/judge-role.js +21 -16
  15. package/dist/migrate-book-topology.js +452 -81
  16. package/dist/navigator-attendance.js +10 -9
  17. package/dist/navigator-invocation-identity.js +18 -0
  18. package/dist/package-contracts/reviewer-output.js +16 -0
  19. package/dist/package-contracts/worker-output.js +18 -2
  20. package/dist/pi/role-turn-host.js +9 -0
  21. package/dist/public-cli/cli.js +1 -0
  22. package/dist/public-cli/instruction-seat-run.js +258 -55
  23. package/dist/public-cli/main.js +1209 -346
  24. package/dist/public-cli/post-admission.js +427 -244
  25. package/dist/public-cli/settlement.js +111 -48
  26. package/dist/public-cli/terminal.js +12 -1
  27. package/dist/public-cli/turn-request.js +3 -0
  28. package/dist/receipt-delivery-policy.js +152 -17
  29. package/dist/role-submission-declarations.js +131 -0
  30. package/dist/secretariat-contracts.js +23 -1
  31. package/dist/submission-errors.js +11 -1
  32. package/dist/submission-gate.js +32 -12
  33. package/package.json +1 -1
  34. package/src/archivist-record-pointer.ts +48 -0
  35. package/src/auditor-role.ts +2 -8
  36. package/src/collector-role.ts +5 -28
  37. package/src/compliance-transport.ts +10 -2
  38. package/src/countersign-role.ts +2 -7
  39. package/src/diarist-contracts.ts +37 -0
  40. package/src/diarist-role.ts +4 -44
  41. package/src/doctor-auditor.ts +3 -0
  42. package/src/doctor-role.ts +16 -11
  43. package/src/engine-detour-usage.ts +7 -6
  44. package/src/external-host-turn-loop.ts +24 -12
  45. package/src/filed-submission.ts +44 -0
  46. package/src/gatekeeper-role.ts +8 -16
  47. package/src/gleaner-left-contracts.ts +26 -1
  48. package/src/gleaner-left-role.ts +4 -32
  49. package/src/host-contracts.ts +7 -0
  50. package/src/inspector-role.ts +2 -7
  51. package/src/judge-role.ts +31 -24
  52. package/src/merger-role.ts +5 -8
  53. package/src/navigator-attendance.ts +17 -12
  54. package/src/navigator-invocation-identity.ts +31 -0
  55. package/src/navigator-role.ts +2 -8
  56. package/src/notary-role.ts +5 -20
  57. package/src/package-contracts/reviewer-output.ts +25 -0
  58. package/src/package-contracts/worker-output.ts +43 -5
  59. package/src/pi/adapter.ts +33 -8
  60. package/src/pi/role-turn-host.ts +7 -0
  61. package/src/public-cli/cli.ts +1 -0
  62. package/src/public-cli/instruction-seat-run.ts +292 -61
  63. package/src/public-cli/post-admission.ts +556 -324
  64. package/src/public-cli/settlement.ts +112 -50
  65. package/src/public-cli/terminal.ts +16 -4
  66. package/src/public-cli/turn-request.ts +10 -1
  67. package/src/receipt-delivery-policy.ts +209 -17
  68. package/src/reviewer-role.ts +7 -35
  69. package/src/role-envelope.ts +5 -1
  70. package/src/role-runtime-dependencies.ts +1 -0
  71. package/src/role-runtime.ts +72 -56
  72. package/src/role-submission-declarations.ts +143 -0
  73. package/src/secretariat-contracts.ts +35 -0
  74. package/src/secretariat-role.ts +4 -40
  75. package/src/submission-correctable-error.ts +5 -2
  76. package/src/submission-errors.ts +12 -1
  77. package/src/submission-gate.ts +41 -12
  78. package/src/worker-role.ts +81 -86
  79. package/src/worker-submission-gates.ts +32 -7
@@ -6,13 +6,16 @@
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
+ import { receivedDiscriminator } from "../submission-errors.js";
13
+ import { REVIEW_QUEUE_STATUSES } from "../review-submission.js";
12
14
  import { runJudgeGates } from "../judge-role.js";
13
15
  import { WORKER_DONE_STATUSES } from "../worker-submission-contracts.js";
14
16
  import { SECRETARIAT_GATE_OFFICER_ENTRY_TYPE } from "../secretariat-contracts.js";
15
17
  import { readableGateItem } from "../readable-gate-item.js";
18
+ import { deliveryLimitFromConfig } from "../receipt-delivery-policy.js";
16
19
  import { runIdFromRunDirectory } from "../run-terminal-artifacts.js";
17
20
  import { createDefaultGateOfficerSummon, requireSubmissionGate } from "../submission-gate.js";
18
21
  import { readRecordedSubmissionRows } from "../submission-ledger.js";
@@ -27,17 +30,16 @@ import { readAuditorResumeBinding } from "../auditor-soul.js";
27
30
  import { CliUsageError } from "./cli-errors.js";
28
31
  import { admitPublicRole, bindAdmittedTicketNumber, buildInstructionTransportPrompt, materializeCountersignInvocation, persistAdmittedSourceRunPath, recordAdmittedCorrelation, recordChildDiaristRun, relocateAdmittedRunToTicket, withPreparedAttachments, } from "./invocation.js";
29
32
  import { presentControlledFailure, prepareSummonsResumeMaterials, roleTurnOptions, runPostAdmissionOneShot, runPostAdmissionResumable, runPostAdmissionSeatResume, showResumeErrorPointer, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
30
- import { buildAutoResumeContinuationPrompt, loadResumablePublicRole, markRunAdmitted, parentRunPathFromGatePointerInstruction, } from "./run-lifecycle.js";
33
+ import { buildAutoResumeContinuationPrompt, loadResumablePublicRole, markRunAdmitted, parentRunPathFromGatePointerInstruction, readCurrentCourt, } from "./run-lifecycle.js";
31
34
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
32
35
  import { presentStructuralRejection, readBoundSessionEntries, seatKnownFailureResolver, attachPostAuditProjection, trySettlePublicSeat, } from "./settlement.js";
33
36
  import { warnMissingMethodSkills } from "./machine-method-skills.js";
34
- import { formatTerminalResult, isLawfulTypedTerminalOutcome, } from "./terminal.js";
35
- import { admittedSeatTurnDetails, projectPublicTurnAxes, projectRoleTurnRequest, requiredMethodSkills, } from "./turn-request.js";
37
+ import { coalesceSubmissionRows, formatTerminalResult, isLawfulTypedTerminalOutcome, } from "./terminal.js";
38
+ import { admittedSeatTurnDetails, projectRoleTurnRequest, requiredMethodSkills, } from "./turn-request.js";
36
39
  import { invokeCourtDiarist, latestPayloadEscalated, } from "./countersign-run.js";
37
40
  import { isRecord, errorText } from "../unknown-value.js";
38
41
  const AUDITED_ROLES = new Set(["judge", "fixer", "coder", "secretariat", "countersign", "doctor"]);
39
42
  const SECRETARIAT_STATUS_REASK = "secretariatStatus 不是 converged、escalate 之一。请重新交卷,secretariatStatus 写明其一。";
40
- const COUNTERSIGN_STATUS_REASK = "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。";
41
43
  const WORKER_ROUTING_STATUSES = new Set([
42
44
  "planned", "completed", "refused", "partially_completed", "unfinished",
43
45
  ]);
@@ -46,8 +48,7 @@ const DOCTOR_ROUTING_STATUSES = new Set(["completed", "refused"]);
46
48
  const DOCTOR_STATUS_REASK = "status 不是 completed、refused 之一。请重新交卷,status 写明其一。";
47
49
  const POST_SUBMISSION_ROUTING = {
48
50
  judge: {
49
- statuses: new Set(["converged", "continue", "escalate"]),
50
- reask: "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。",
51
+ statuses: REVIEW_QUEUE_STATUSES,
51
52
  },
52
53
  coder: {
53
54
  statuses: WORKER_ROUTING_STATUSES,
@@ -75,18 +76,88 @@ function unreadablePostSubmissionStatus(admitted, terminal) {
75
76
  if (route === undefined)
76
77
  return undefined;
77
78
  const payload = terminal.roleOutcome.payloads?.at(-1);
78
- const status = isRecord(payload)
79
- ? payload.status
80
- : undefined;
79
+ const status = receivedDiscriminator(payload, "status");
81
80
  return typeof status === "string" && route.statuses.has(status)
82
81
  ? undefined
83
- : route.reask;
82
+ : route.reask ?? officerConclusionReask(status);
83
+ }
84
+ /**
85
+ * One more unreadable-status reask. This loop has its own counter and reads
86
+ * the same autoResumeLimit as the other loops. Exhaustion is not a readable
87
+ * status: the caller presents the volumes already in hand and does not advance
88
+ * review. ADR 0007 audit continues are not this shape.
89
+ */
90
+ function withUnreadableReask(env) {
91
+ const spent = env.unreadableReasksSpent ?? 0;
92
+ if (spent >= deliveryLimitFromConfig(env.autoResumeLimit))
93
+ return undefined;
94
+ return { ...env, unreadableReasksSpent: spent + 1 };
95
+ }
96
+ /** Budget spent on an unreadable status. Distinct from "status is readable". */
97
+ const UNREADABLE_REASK_EXHAUSTED = { unreadableReaskExhausted: true };
98
+ function isUnreadableReaskExhausted(value) {
99
+ return value === UNREADABLE_REASK_EXHAUSTED;
100
+ }
101
+ function presentOriginalVolume(held, terminal, io) {
102
+ // The held buffer was formatted before direction facts were attached.
103
+ if (terminal?.roleOutcome.decisiveFacts?.directionUnsettled === true) {
104
+ io.stdout(formatTerminalResult(terminal));
105
+ return;
106
+ }
107
+ if (held.length > 0) {
108
+ for (const value of held)
109
+ io.stdout(value);
110
+ return;
111
+ }
112
+ if (terminal !== undefined)
113
+ io.stdout(formatTerminalResult(terminal));
114
+ }
115
+ /** Runtime fact beside the original volume. Payloads stay as submitted. */
116
+ function withUnsettledDirection(parent, officer) {
117
+ const outcome = parent.roleOutcome;
118
+ if (outcome.kind !== "accepted" && outcome.kind !== "audit_escalation")
119
+ return parent;
120
+ const officerOutcome = officer?.roleOutcome;
121
+ const officerPayloads = coalesceSubmissionRows(officer?.submissions, officerOutcome !== undefined && officerOutcome.kind !== "no_receipt"
122
+ ? officerOutcome.payloads
123
+ : undefined);
124
+ return {
125
+ ...parent,
126
+ roleOutcome: {
127
+ ...outcome,
128
+ decisiveFacts: {
129
+ ...(outcome.decisiveFacts ?? {}),
130
+ directionUnsettled: true,
131
+ subsequentAudit: "incomplete",
132
+ ...(officerOutcome === undefined ? {} : { officerRole: officerOutcome.role }),
133
+ ...(officer?.runId === undefined ? {} : { officerRunId: officer.runId }),
134
+ ...(officerPayloads.length === 0 ? {} : { officerPayloads }),
135
+ },
136
+ },
137
+ };
138
+ }
139
+ function heldUnreadableTerminal(terminal) {
140
+ if (terminal?.roleOutcome.kind !== "accepted")
141
+ return false;
142
+ if (latestPayloadEscalated(terminal.roleOutcome))
143
+ return false;
144
+ const status = latestQueueStatus(terminal);
145
+ return typeof status !== "string" || !REVIEW_QUEUE_STATUSES.has(status);
146
+ }
147
+ function presentUnsettledAudit(parent, officer, io) {
148
+ const terminal = withUnsettledDirection(parent, officer);
149
+ io.stdout(formatTerminalResult(terminal));
150
+ if (officer !== undefined)
151
+ io.stdout(formatTerminalResult(officer));
152
+ return terminal;
84
153
  }
85
- async function reaskUnreadablePostSubmissionStatus(admitted, terminal, env, io) {
86
- const message = unreadablePostSubmissionStatus(admitted, terminal);
154
+ async function reaskUnreadablePostSubmissionStatus(admitted, terminal, env, io, message = unreadablePostSubmissionStatus(admitted, terminal)) {
87
155
  if (message === undefined)
88
156
  return undefined;
89
- return runPublicInstructionSeatResume({ runId: admitted.runId, message }, { ...env, autoResumeLimit: 0 }, io);
157
+ const next = withUnreadableReask(env);
158
+ if (next === undefined)
159
+ return UNREADABLE_REASK_EXHAUSTED;
160
+ return runPublicInstructionSeatResume({ runId: admitted.runId, message }, next, io);
90
161
  }
91
162
  function roleRecord(role) {
92
163
  const record = packagedRoleMetadata(role);
@@ -234,6 +305,13 @@ async function dispatchAdmitted(admitted, env, io) {
234
305
  };
235
306
  const result = await execute(env);
236
307
  const unreadableReask = await reaskUnreadablePostSubmissionStatus(admitted, result.terminal, env, io);
308
+ if (isUnreadableReaskExhausted(unreadableReask)) {
309
+ const terminal = result.terminal === undefined
310
+ ? undefined
311
+ : withUnsettledDirection(result.terminal);
312
+ presentOriginalVolume(held, terminal, io);
313
+ return terminal === undefined ? result : { ...result, terminal };
314
+ }
237
315
  if (unreadableReask !== undefined)
238
316
  return unreadableReask;
239
317
  if (AUDITED_ROLES.has(admitted.role) && result.terminal?.roleOutcome.kind === "accepted") {
@@ -389,20 +467,14 @@ async function runCountersignBody(parsed, env, io) {
389
467
  await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
390
468
  admitted = { ...admitted, sourceRunPath: gateParentRunPath };
391
469
  }
392
- const turnProjection = {
393
- ...projectPublicTurnAxes(env),
394
- ...(env.correlationId === undefined || env.correlationId.trim() === ""
395
- ? {}
396
- : { correlationId: env.correlationId }),
397
- continuation: {
398
- kind: "initial",
399
- prompt: (env.reviewReask ?? env.gateReviewInstruction)
400
- ?? buildInstructionTransportPrompt(admitted, engineSessionMaterialFromOptions({
401
- ...pickEngineAxis(env),
402
- packageRoot: env.packageRoot,
403
- })),
404
- },
405
- };
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
+ });
406
478
  const turnRequest = buildInstructionSeatTurnRequest(admitted, turnProjection);
407
479
  const result = await runPostAdmissionOneShot({
408
480
  admitted,
@@ -679,13 +751,37 @@ export async function runPublicInstructionSeatResume(request, env, io) {
679
751
  },
680
752
  afterAdmittedLoad: async (admitted) => {
681
753
  await warnMissingMethodSkills(env.home, env.host, admitted.role, requiredMethodSkills(admitted), io.stdout);
682
- return { kind: "continue", adapters: seatAdapters(admitted, env) };
754
+ const adapters = seatAdapters(admitted, env);
755
+ if ((env.unreadableReasksSpent ?? 0) === 0 && env.reviewReask === undefined) {
756
+ return { kind: "continue", adapters };
757
+ }
758
+ const original = await adapters.trySettle(admitted, env.principalAuthority, await readCurrentCourt(admitted.runDirectory));
759
+ const unsettled = POST_SUBMISSION_ROUTING[admitted.role] === undefined
760
+ ? heldUnreadableTerminal(original)
761
+ : unreadablePostSubmissionStatus(admitted, original) !== undefined;
762
+ // Select this reask's existing subject before dispatch. The common
763
+ // settlement then keeps it only when no fresh receipt supersedes it;
764
+ // actual failures still take their normal failure path.
765
+ return {
766
+ kind: "continue",
767
+ adapters: original === undefined || !unsettled ? adapters : {
768
+ ...adapters,
769
+ trySettle: async (seat, authority, scope) => await adapters.trySettle(seat, authority, scope) ?? original,
770
+ },
771
+ };
683
772
  },
684
773
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
685
774
  });
686
775
  const unreadableReask = result.admitted === undefined
687
776
  ? undefined
688
777
  : await reaskUnreadablePostSubmissionStatus(result.admitted, result.terminal, env, io);
778
+ if (isUnreadableReaskExhausted(unreadableReask)) {
779
+ const terminal = result.terminal === undefined
780
+ ? undefined
781
+ : withUnsettledDirection(result.terminal);
782
+ presentOriginalVolume(held, terminal, io);
783
+ return terminal === undefined ? result : { ...result, terminal };
784
+ }
689
785
  if (unreadableReask !== undefined)
690
786
  return unreadableReask;
691
787
  if (result.admitted !== undefined && AUDITED_ROLES.has(result.admitted.role)
@@ -716,31 +812,49 @@ export async function runPublicInstructionSeatResume(request, env, io) {
716
812
  }
717
813
  return resume();
718
814
  }
719
- const QUEUE_CONCLUSIONS = new Set(["converged", "continue", "escalate"]);
720
815
  const GATE_CHILD_ROLES = new Set(["notary", "auditor", "inspector", "countersign"]);
721
816
  /**
722
817
  * A gate child's conclusion outside the three states goes back to that child.
723
- * The words are the existing officer re-ask. A host failure stops here.
818
+ * The words are the existing officer re-ask. Each reask spends this loop's own
819
+ * copy of the configured ceiling; exhaustion keeps the terminal already in hand.
820
+ * A host failure stops here.
821
+ * Countersign already spends that ceiling inside its own seat, or the summoning
822
+ * gate does. This loop does not open a second one for the same conclusion.
724
823
  */
725
824
  async function queueConclusionFromChild(child, env, io) {
726
825
  if (!GATE_CHILD_ROLES.has(child.role))
727
826
  return undefined;
827
+ if (child.role === "countersign") {
828
+ const terminal = await trySettlePublicSeat(child, env.principalAuthority, await readCurrentCourt(child.runDirectory));
829
+ const status = latestQueueStatus(terminal);
830
+ if (terminal !== undefined && typeof status === "string" && REVIEW_QUEUE_STATUSES.has(status)) {
831
+ return { admitted: child, terminal, status };
832
+ }
833
+ return { stop: { exitCode: 0, ...(terminal === undefined ? {} : { terminal }) } };
834
+ }
728
835
  let current = child;
836
+ // Notary, auditor, and inspector have no seat-local status reask. This loop
837
+ // is their one budget. The child resume keeps the caller's env so delivery,
838
+ // failure recovery, and other reask loops stay separate.
839
+ let budgetEnv = { ...env, unreadableReasksSpent: 0 };
729
840
  for (;;) {
730
- const terminal = await trySettlePublicSeat(current, env.principalAuthority, undefined);
841
+ const terminal = await trySettlePublicSeat(current, env.principalAuthority, await readCurrentCourt(current.runDirectory));
731
842
  const status = latestQueueStatus(terminal);
732
- if (terminal !== undefined && status !== undefined && QUEUE_CONCLUSIONS.has(status)) {
843
+ if (terminal !== undefined && typeof status === "string" && REVIEW_QUEUE_STATUSES.has(status)) {
733
844
  return { admitted: current, terminal, status };
734
845
  }
735
- const reasked = await runPublicInstructionSeatResume({
736
- runId: current.runId,
737
- message: officerConclusionReask(status),
738
- }, env, io);
739
- if (reasked.exitCode !== 0 || reasked.admitted === undefined || reasked.terminal === undefined) {
846
+ const reasked = await reaskUnreadablePostSubmissionStatus(current, terminal, budgetEnv, io, officerConclusionReask(status));
847
+ if (reasked === undefined || isUnreadableReaskExhausted(reasked)) {
848
+ return { stop: { exitCode: 0, ...(terminal === undefined ? {} : { terminal }) } };
849
+ }
850
+ budgetEnv = withUnreadableReask(budgetEnv);
851
+ if (reasked.exitCode !== 0 || reasked.admitted === undefined || reasked.terminal === undefined
852
+ || reasked.terminal.roleOutcome.kind === "no_receipt"
853
+ || reasked.terminal.roleOutcome.decisiveFacts?.directionUnsettled === true) {
740
854
  return { stop: reasked };
741
855
  }
742
856
  const reaskedStatus = latestQueueStatus(reasked.terminal);
743
- if (reaskedStatus !== undefined && QUEUE_CONCLUSIONS.has(reaskedStatus)) {
857
+ if (typeof reaskedStatus === "string" && REVIEW_QUEUE_STATUSES.has(reaskedStatus)) {
744
858
  return { admitted: reasked.admitted, terminal: reasked.terminal, status: reaskedStatus };
745
859
  }
746
860
  if (latestPayloadEscalated(reasked.terminal.roleOutcome))
@@ -756,8 +870,20 @@ export async function continueParentAfterChild(parentRunId, child, env, io) {
756
870
  }
757
871
  const admitted = loaded.admitted;
758
872
  const resolved = await queueConclusionFromChild(child, env, io);
759
- if (resolved !== undefined && "stop" in resolved)
760
- return resolved.stop;
873
+ if (resolved !== undefined && "stop" in resolved) {
874
+ const stopped = resolved.stop;
875
+ if (stopped.exitCode === 0
876
+ && heldUnreadableTerminal(stopped.terminal)
877
+ && stopped.terminal !== undefined) {
878
+ const parentTerminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
879
+ if (parentTerminal?.roleOutcome.kind === "accepted"
880
+ || parentTerminal?.roleOutcome.kind === "audit_escalation") {
881
+ const terminal = presentUnsettledAudit(parentTerminal, stopped.terminal, io);
882
+ return { exitCode: 0, admitted, terminal };
883
+ }
884
+ }
885
+ return stopped;
886
+ }
761
887
  if (resolved !== undefined && resolved.status === "escalate") {
762
888
  return { exitCode: 0, admitted: resolved.admitted, terminal: resolved.terminal };
763
889
  }
@@ -766,19 +892,50 @@ export async function continueParentAfterChild(parentRunId, child, env, io) {
766
892
  const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
767
893
  if (terminal?.roleOutcome.kind !== "accepted")
768
894
  throw new Error("pending submission is not recorded");
769
- return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted.role, latestQueuePayload(resolved.terminal), resolved.admitted.runId);
895
+ return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted, latestQueuePayload(resolved.terminal));
770
896
  }
771
897
  return runPublicInstructionSeatResume({
772
898
  runId: parentRunId,
773
899
  message: readableGateItem(latestQueuePayload(resolved.terminal)),
774
- }, { ...env, autoResumeLimit: 0 }, io);
900
+ }, { ...env, unreadableReasksSpent: 0 }, io);
775
901
  }
776
902
  const result = await dispatchAdmitted(admitted, env, { ...io, omitFailureStderrDiagnostic: true });
777
903
  showResumeErrorPointer(io, result.exitCode, result.terminal);
778
904
  return result;
779
905
  }
906
+ /**
907
+ * A gate is already passed only when an officer run reviewed this submission
908
+ * and its latest queue word is converged. The resumed seat's role is not itself
909
+ * a pass. Unknown or unreadable evidence does not skip the gate.
910
+ */
911
+ async function officerPassedCurrentSubmission(input) {
912
+ const { env, officer } = input;
913
+ const pointer = await readDirectOfficerRunPointer(input.parentSessionFile, officer);
914
+ if (pointer?.runDirectory === undefined)
915
+ return false;
916
+ if (pointer.submissionToolCallId !== input.toolCallId)
917
+ return false;
918
+ const officerRunId = runIdFromRunDirectory(pointer.runDirectory);
919
+ if (officerRunId === undefined)
920
+ return false;
921
+ let admitted;
922
+ try {
923
+ const loaded = await loadResumablePublicRole(env.home, officerRunId, env.principalAuthority);
924
+ admitted = loaded.admitted;
925
+ }
926
+ catch {
927
+ return false;
928
+ }
929
+ if (admitted.role !== officer)
930
+ return false;
931
+ // A summon binding identifies the requested review, not its outcome. An open
932
+ // court (including a failed host turn) must not borrow a previous court's seal.
933
+ const court = await readCurrentCourt(admitted.runDirectory);
934
+ const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, court);
935
+ return latestQueueStatus(terminal) === "converged";
936
+ }
780
937
  /** Finished submissions enter the existing audit gate after their tool call has returned. */
781
- async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, passedRunId) {
938
+ async function auditSubmittedRole(turn, env, io, resumedOfficer, passedReceipt) {
782
939
  const admitted = turn.admitted;
783
940
  if (admitted === undefined || !AUDITED_ROLES.has(admitted.role)
784
941
  || turn.terminal?.roleOutcome.kind !== "accepted")
@@ -789,11 +946,30 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
789
946
  const record = isRecord(accepted)
790
947
  ? accepted : undefined;
791
948
  const status = admitted.role === "secretariat" ? record?.secretariatStatus : record?.status;
949
+ // A gate summon already owns this conclusion's reasks. The seat must not
950
+ // start another budget on the same run (parentRunPath is that summon).
951
+ const gateOwnsStatusReask = typeof env.parentRunPath === "string" && env.parentRunPath.trim() !== "";
792
952
  if (admitted.role === "secretariat" && status !== "converged" && status !== "escalate") {
793
- return runPublicInstructionSeatResume({ runId: admitted.runId, message: SECRETARIAT_STATUS_REASK }, { ...env, autoResumeLimit: 0 }, io);
953
+ if (gateOwnsStatusReask)
954
+ return turn;
955
+ const next = withUnreadableReask(env);
956
+ if (next === undefined) {
957
+ const terminal = withUnsettledDirection(turn.terminal);
958
+ io.stdout(formatTerminalResult(terminal));
959
+ return { ...turn, terminal };
960
+ }
961
+ return runPublicInstructionSeatResume({ runId: admitted.runId, message: SECRETARIAT_STATUS_REASK }, next, io);
794
962
  }
795
- if (admitted.role === "countersign" && (typeof status !== "string" || !QUEUE_CONCLUSIONS.has(status))) {
796
- return runPublicInstructionSeatResume({ runId: admitted.runId, message: COUNTERSIGN_STATUS_REASK }, { ...env, autoResumeLimit: 0 }, io);
963
+ if (admitted.role === "countersign" && (typeof status !== "string" || !REVIEW_QUEUE_STATUSES.has(status))) {
964
+ if (gateOwnsStatusReask)
965
+ return turn;
966
+ const next = withUnreadableReask(env);
967
+ if (next === undefined) {
968
+ const terminal = withUnsettledDirection(turn.terminal);
969
+ io.stdout(formatTerminalResult(terminal));
970
+ return { ...turn, terminal };
971
+ }
972
+ return runPublicInstructionSeatResume({ runId: admitted.runId, message: officerConclusionReask(status) }, next, io);
797
973
  }
798
974
  // Self-escalation is a valid open routing state for these seats only.
799
975
  // Doctor's declared domain is completed|refused — an open "escalate" must
@@ -830,11 +1006,18 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
830
1006
  },
831
1007
  abort() { },
832
1008
  };
1009
+ const passedThisSubmission = (officer) => officerPassedCurrentSubmission({
1010
+ env,
1011
+ parentSessionFile: sessionFile,
1012
+ officer,
1013
+ toolCallId,
1014
+ });
833
1015
  if (admitted.role === "doctor") {
834
- if (passedOfficer !== "auditor") {
1016
+ if (!await passedThisSubmission("auditor")) {
835
1017
  let lastSummon;
836
1018
  const decision = await createPiDoctorAuditor()({
837
1019
  context, submission: accepted,
1020
+ ...(env.autoResumeLimit === undefined ? {} : { autoResumeLimit: env.autoResumeLimit }),
838
1021
  ...(env.signal === undefined ? {} : { signal: env.signal }),
839
1022
  summonAuditor: async (subject, sourceRunDirectory, signal, reask, submission) => {
840
1023
  const summoned = await summonPublicRole({
@@ -854,6 +1037,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
854
1037
  if (officerSession !== undefined) {
855
1038
  bookDirectOfficerRunPointer({
856
1039
  parentSessionFile: sessionFile, officer: "auditor", sessionFile: officerSession,
1040
+ submissionToolCallId: toolCallId,
857
1041
  ...(summoned.runDirectory === undefined ? {} : { runDirectory: summoned.runDirectory }),
858
1042
  });
859
1043
  }
@@ -864,7 +1048,16 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
864
1048
  if (decision.status === "continue") {
865
1049
  return runPublicInstructionSeatResume({
866
1050
  runId: admitted.runId, message: readableGateItem(decision.receipt ?? decision.violations),
867
- }, { ...env, autoResumeLimit: 0 }, io);
1051
+ }, { ...env, unreadableReasksSpent: 0 }, io);
1052
+ }
1053
+ if (decision.status === "received") {
1054
+ if (lastSummon?.terminal === undefined) {
1055
+ throw new Error("doctor audit kept an unreadable reply with no terminal");
1056
+ }
1057
+ if (turn.terminal === undefined)
1058
+ throw new Error("doctor submission has no terminal");
1059
+ const terminal = presentUnsettledAudit(turn.terminal, lastSummon.terminal, io);
1060
+ return { exitCode: 0, admitted, terminal };
868
1061
  }
869
1062
  if (decision.status !== "converged") {
870
1063
  if (lastSummon?.terminal !== undefined) {
@@ -886,13 +1079,14 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
886
1079
  ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
887
1080
  });
888
1081
  let chain;
889
- let officerRunId = passedRunId;
1082
+ let officerRunId = resumedOfficer?.runId;
890
1083
  const runGate = (subject) => requireSubmissionGate({
891
1084
  context,
892
1085
  subject,
893
1086
  toolCallId,
894
1087
  submission: accepted,
895
1088
  summonOfficer,
1089
+ ...(env.autoResumeLimit === undefined ? {} : { autoResumeLimit: env.autoResumeLimit }),
896
1090
  ...(env.signal === undefined ? {} : { signal: env.signal }),
897
1091
  hostActions: {
898
1092
  failInfrastructure(error) { throw error; },
@@ -901,8 +1095,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
901
1095
  });
902
1096
  if (admitted.role === "judge") {
903
1097
  chain = await runJudgeGates({
904
- gateAlreadyConverged: async (subject) => passedOfficer === "auditor"
905
- || (passedOfficer === "notary" && subject.kind === "judge_draft"),
1098
+ gateAlreadyConverged: async (subject) => passedThisSubmission(gateOfficerForSubject(subject)),
906
1099
  runGate,
907
1100
  });
908
1101
  }
@@ -913,7 +1106,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
913
1106
  ? { kind: "secretariat_verdict" }
914
1107
  : { kind: "countersign_verdict" };
915
1108
  const officer = gateOfficerForSubject(subject);
916
- if (passedOfficer === officer) {
1109
+ if (await passedThisSubmission(officer)) {
917
1110
  chain = { status: "converged", passes: [] };
918
1111
  }
919
1112
  else {
@@ -929,10 +1122,20 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
929
1122
  receipt: pass.receipt,
930
1123
  ...(pass.runId === undefined ? {} : { runId: pass.runId }),
931
1124
  ...(pass.runDirectory === undefined ? {} : { runDirectory: pass.runDirectory }),
1125
+ ...(pass.terminal === undefined ? {} : { terminal: pass.terminal }),
932
1126
  }],
933
1127
  };
934
1128
  }
935
1129
  }
1130
+ if (chain.status === "needs_reask") {
1131
+ const officerTerminal = chain.passes.at(-1)?.terminal;
1132
+ if (officerTerminal === undefined)
1133
+ throw new Error("unreadable audit has no terminal result");
1134
+ if (turn.terminal === undefined)
1135
+ throw new Error("audited submission has no terminal");
1136
+ const terminal = presentUnsettledAudit(turn.terminal, officerTerminal, io);
1137
+ return { exitCode: 0, admitted, terminal };
1138
+ }
936
1139
  if (chain.status === "escalate") {
937
1140
  const escalation = chain.passes.at(-1);
938
1141
  const escalatedRunId = escalation?.runId
@@ -941,7 +1144,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
941
1144
  if (escalatedRunId === undefined)
942
1145
  throw new Error("escalated audit has no resumable run identity");
943
1146
  const officer = await loadResumablePublicRole(env.home, escalatedRunId, env.principalAuthority);
944
- const terminal = await trySettlePublicSeat(officer.admitted, env.principalAuthority, undefined);
1147
+ const terminal = await trySettlePublicSeat(officer.admitted, env.principalAuthority, await readCurrentCourt(officer.admitted.runDirectory));
945
1148
  if (terminal === undefined)
946
1149
  throw new Error("escalated audit has no terminal result");
947
1150
  io.stdout(formatTerminalResult(terminal));
@@ -951,7 +1154,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
951
1154
  return runPublicInstructionSeatResume({
952
1155
  runId: admitted.runId,
953
1156
  message: readableGateItem(chain.passes.at(-1)?.receipt),
954
- }, { ...env, autoResumeLimit: 0 }, io);
1157
+ }, { ...env, unreadableReasksSpent: 0 }, io);
955
1158
  }
956
1159
  if (admitted.role === "secretariat") {
957
1160
  const pass = chain.passes.at(-1);