@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
@@ -25,6 +25,7 @@ import { ExplicitInternalActivationError } from "../host-contracts.ts";
25
25
  import { applyEngineChildEnv, ENGINE_MODEL_FLAG_NAME, normalizeEngineName } from "../engine-detour.ts";
26
26
  import { projectActivationFlags } from "../role-activation-flags.ts";
27
27
  import { encodeUserDialogueStdin } from "../user-dialogue-stdin.ts";
28
+ import { RECEIPT_DELIVERY_LIMIT_ENV } from "../receipt-delivery-policy.ts";
28
29
 
29
30
  /** Package-relative Internal role entrypoint (ADR 0052; same path as public-cli registry). */
30
31
  const INTERNAL_ROLE_ENTRYPOINT_RELATIVE = "extensions/role-runtime.ts";
@@ -403,6 +404,12 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
403
404
  // Selected host axis (#537 / ADR 0082): omit must not inherit a parent env value.
404
405
  if (request.host === undefined || request.host.trim() === "") delete env.AK_ROLE_HOST;
405
406
  else env.AK_ROLE_HOST = request.host.trim();
407
+ // #1132: the one effective delivery-request ceiling travels to the child
408
+ // on its own env so the in-child role runtime and the worker submission
409
+ // gate count with the same configured number the AK seam resolved. Omit
410
+ // must not inherit a parent value (same rule as the other AK_ROLE_ axes).
411
+ if (request.deliveryRequestLimit === undefined) delete env[RECEIPT_DELIVERY_LIMIT_ENV];
412
+ else env[RECEIPT_DELIVERY_LIMIT_ENV] = String(request.deliveryRequestLimit);
406
413
  applyEngineChildEnv(env, request.engine);
407
414
  // Nested auditor dossier tool binds the parent run pointer when published.
408
415
  if (
@@ -1234,6 +1234,7 @@ export async function runAkRole(
1234
1234
  while (
1235
1235
  current.exitCode === 0 && current.admitted?.correlationId !== undefined
1236
1236
  && current.terminal?.roleOutcome.kind === "accepted"
1237
+ && current.terminal.roleOutcome.decisiveFacts?.directionUnsettled !== true
1237
1238
  && !latestPayloadEscalated(current.terminal.roleOutcome)
1238
1239
  ) {
1239
1240
  const parentRunId = current.admitted.correlationId;
@@ -6,15 +6,18 @@
6
6
  import { dirname, resolve } from "node:path";
7
7
  import { isUnboundRunDirectory } from "../role-run-placement.ts";
8
8
  import { AUDITOR_DOSSIER_PROMPT } from "../compliance-transport.ts";
9
- import { bookDirectOfficerRunPointer } from "../archivist-record-pointer.ts";
9
+ import { bookDirectOfficerRunPointer, readDirectOfficerRunPointer } from "../archivist-record-pointer.ts";
10
10
  import { createPiDoctorAuditor } from "../doctor-auditor.ts";
11
11
 
12
12
  import type { DurablePrincipalAuthority, HostContext, RoleTurnRequest } from "../host-contracts.ts";
13
- import { officerConclusionReask, gateOfficerForSubject } from "../gatekeeper-role.ts";
13
+ import { officerConclusionReask, gateOfficerForSubject, type GateOfficer } from "../gatekeeper-role.ts";
14
+ import { receivedDiscriminator } from "../submission-errors.ts";
15
+ import { REVIEW_QUEUE_STATUSES } from "../review-submission.ts";
14
16
  import { runJudgeGates } from "../judge-role.ts";
15
17
  import { WORKER_DONE_STATUSES } from "../worker-submission-contracts.ts";
16
18
  import { SECRETARIAT_GATE_OFFICER_ENTRY_TYPE } from "../secretariat-contracts.ts";
17
19
  import { readableGateItem } from "../readable-gate-item.ts";
20
+ import { deliveryLimitFromConfig } from "../receipt-delivery-policy.ts";
18
21
  import { runIdFromRunDirectory } from "../run-terminal-artifacts.ts";
19
22
  import { createDefaultGateOfficerSummon, requireSubmissionGate } from "../submission-gate.ts";
20
23
  import { readRecordedSubmissionRows } from "../submission-ledger.ts";
@@ -69,6 +72,7 @@ import {
69
72
  loadResumablePublicRole,
70
73
  markRunAdmitted,
71
74
  parentRunPathFromGatePointerInstruction,
75
+ readCurrentCourt,
72
76
  type PublicResumeRequest,
73
77
  type RunWriterLease,
74
78
  type SameTicketSummonsMaterials,
@@ -84,6 +88,7 @@ import {
84
88
  import type { CliIo } from "./cli-io.ts";
85
89
  import { warnMissingMethodSkills } from "./machine-method-skills.ts";
86
90
  import {
91
+ coalesceSubmissionRows,
87
92
  formatTerminalResult,
88
93
  isLawfulTypedTerminalOutcome,
89
94
  type TerminalResult,
@@ -91,7 +96,6 @@ import {
91
96
  } from "./terminal.ts";
92
97
  import {
93
98
  admittedSeatTurnDetails,
94
- projectPublicTurnAxes,
95
99
  projectRoleTurnRequest,
96
100
  requiredMethodSkills,
97
101
  type RoleTurnRequestProjectionOptions,
@@ -116,7 +120,6 @@ type SeatRunResult = {
116
120
 
117
121
  const AUDITED_ROLES = new Set<PackagedRole>(["judge", "fixer", "coder", "secretariat", "countersign", "doctor"]);
118
122
  const SECRETARIAT_STATUS_REASK = "secretariatStatus 不是 converged、escalate 之一。请重新交卷,secretariatStatus 写明其一。";
119
- const COUNTERSIGN_STATUS_REASK = "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。";
120
123
  const WORKER_ROUTING_STATUSES: ReadonlySet<string> = new Set([
121
124
  "planned", "completed", "refused", "partially_completed", "unfinished",
122
125
  ]);
@@ -127,11 +130,10 @@ const DOCTOR_STATUS_REASK =
127
130
  "status 不是 completed、refused 之一。请重新交卷,status 写明其一。";
128
131
  const POST_SUBMISSION_ROUTING: Partial<Record<PackagedRole, {
129
132
  readonly statuses: ReadonlySet<string>;
130
- readonly reask: string;
133
+ readonly reask?: string;
131
134
  }>> = {
132
135
  judge: {
133
- statuses: new Set(["converged", "continue", "escalate"]),
134
- reask: "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。",
136
+ statuses: REVIEW_QUEUE_STATUSES,
135
137
  },
136
138
  coder: {
137
139
  statuses: WORKER_ROUTING_STATUSES,
@@ -161,12 +163,96 @@ function unreadablePostSubmissionStatus(
161
163
  const route = POST_SUBMISSION_ROUTING[admitted.role];
162
164
  if (route === undefined) return undefined;
163
165
  const payload = terminal.roleOutcome.payloads?.at(-1);
164
- const status = isRecord(payload)
165
- ? (payload as Record<string, unknown>).status
166
- : undefined;
166
+ const status = receivedDiscriminator(payload, "status");
167
167
  return typeof status === "string" && route.statuses.has(status)
168
168
  ? undefined
169
- : route.reask;
169
+ : route.reask ?? officerConclusionReask(status);
170
+ }
171
+
172
+ /**
173
+ * One more unreadable-status reask. This loop has its own counter and reads
174
+ * the same autoResumeLimit as the other loops. Exhaustion is not a readable
175
+ * status: the caller presents the volumes already in hand and does not advance
176
+ * review. ADR 0007 audit continues are not this shape.
177
+ */
178
+ function withUnreadableReask(env: InstructionSeatRunEnv): InstructionSeatRunEnv | undefined {
179
+ const spent = env.unreadableReasksSpent ?? 0;
180
+ if (spent >= deliveryLimitFromConfig(env.autoResumeLimit)) return undefined;
181
+ return { ...env, unreadableReasksSpent: spent + 1 };
182
+ }
183
+
184
+ /** Budget spent on an unreadable status. Distinct from "status is readable". */
185
+ const UNREADABLE_REASK_EXHAUSTED = { unreadableReaskExhausted: true as const };
186
+
187
+ function isUnreadableReaskExhausted(
188
+ value: SeatRunResult | typeof UNREADABLE_REASK_EXHAUSTED | undefined,
189
+ ): value is typeof UNREADABLE_REASK_EXHAUSTED {
190
+ return value === UNREADABLE_REASK_EXHAUSTED;
191
+ }
192
+
193
+ function presentOriginalVolume(
194
+ held: readonly string[],
195
+ terminal: TerminalResult | undefined,
196
+ io: CliIo,
197
+ ): void {
198
+ // The held buffer was formatted before direction facts were attached.
199
+ if (terminal?.roleOutcome.decisiveFacts?.directionUnsettled === true) {
200
+ io.stdout(formatTerminalResult(terminal));
201
+ return;
202
+ }
203
+ if (held.length > 0) {
204
+ for (const value of held) io.stdout(value);
205
+ return;
206
+ }
207
+ if (terminal !== undefined) io.stdout(formatTerminalResult(terminal));
208
+ }
209
+
210
+ /** Runtime fact beside the original volume. Payloads stay as submitted. */
211
+ function withUnsettledDirection(
212
+ parent: TerminalResult,
213
+ officer?: TerminalResult,
214
+ ): TerminalResult {
215
+ const outcome = parent.roleOutcome;
216
+ if (outcome.kind !== "accepted" && outcome.kind !== "audit_escalation") return parent;
217
+ const officerOutcome = officer?.roleOutcome;
218
+ const officerPayloads = coalesceSubmissionRows(
219
+ officer?.submissions,
220
+ officerOutcome !== undefined && officerOutcome.kind !== "no_receipt"
221
+ ? officerOutcome.payloads
222
+ : undefined,
223
+ );
224
+ return {
225
+ ...parent,
226
+ roleOutcome: {
227
+ ...outcome,
228
+ decisiveFacts: {
229
+ ...(outcome.decisiveFacts ?? {}),
230
+ directionUnsettled: true,
231
+ subsequentAudit: "incomplete",
232
+ ...(officerOutcome === undefined ? {} : { officerRole: officerOutcome.role }),
233
+ ...(officer?.runId === undefined ? {} : { officerRunId: officer.runId }),
234
+ ...(officerPayloads.length === 0 ? {} : { officerPayloads }),
235
+ },
236
+ },
237
+ };
238
+ }
239
+
240
+ function heldUnreadableTerminal(terminal: TerminalResult | undefined): boolean {
241
+ if (terminal?.roleOutcome.kind !== "accepted") return false;
242
+ if (latestPayloadEscalated(terminal.roleOutcome)) return false;
243
+ const status = latestQueueStatus(terminal);
244
+ return typeof status !== "string" || !REVIEW_QUEUE_STATUSES.has(status);
245
+ }
246
+
247
+ function presentUnsettledAudit(
248
+ parent: TerminalResult,
249
+ officer: TerminalResult | undefined,
250
+ io: CliIo,
251
+ ): TerminalResult {
252
+ const terminal = withUnsettledDirection(parent, officer);
253
+ io.stdout(formatTerminalResult(terminal));
254
+ if (officer !== undefined) io.stdout(formatTerminalResult(officer));
255
+ return terminal;
170
256
  }
171
257
 
172
258
  async function reaskUnreadablePostSubmissionStatus(
@@ -174,12 +260,14 @@ async function reaskUnreadablePostSubmissionStatus(
174
260
  terminal: TerminalResult | undefined,
175
261
  env: InstructionSeatRunEnv,
176
262
  io: CliIo,
177
- ): Promise<SeatRunResult | undefined> {
178
- const message = unreadablePostSubmissionStatus(admitted, terminal);
263
+ message = unreadablePostSubmissionStatus(admitted, terminal),
264
+ ): Promise<SeatRunResult | typeof UNREADABLE_REASK_EXHAUSTED | undefined> {
179
265
  if (message === undefined) return undefined;
266
+ const next = withUnreadableReask(env);
267
+ if (next === undefined) return UNREADABLE_REASK_EXHAUSTED;
180
268
  return runPublicInstructionSeatResume(
181
269
  { runId: admitted.runId, message },
182
- { ...env, autoResumeLimit: 0 },
270
+ next,
183
271
  io,
184
272
  );
185
273
  }
@@ -375,6 +463,13 @@ async function dispatchAdmitted(
375
463
  env,
376
464
  io,
377
465
  );
466
+ if (isUnreadableReaskExhausted(unreadableReask)) {
467
+ const terminal = result.terminal === undefined
468
+ ? undefined
469
+ : withUnsettledDirection(result.terminal);
470
+ presentOriginalVolume(held, terminal, io);
471
+ return terminal === undefined ? result : { ...result, terminal };
472
+ }
378
473
  if (unreadableReask !== undefined) return unreadableReask;
379
474
  if (AUDITED_ROLES.has(admitted.role) && result.terminal?.roleOutcome.kind === "accepted") {
380
475
  return auditSubmittedRole(result, env, io);
@@ -561,23 +656,17 @@ async function runCountersignBody(
561
656
  await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
562
657
  admitted = { ...admitted, sourceRunPath: gateParentRunPath };
563
658
  }
564
- const turnProjection: RoleTurnRequestProjectionOptions = {
565
- ...projectPublicTurnAxes(env),
566
- ...(env.correlationId === undefined || env.correlationId.trim() === ""
567
- ? {}
568
- : { correlationId: env.correlationId }),
569
- continuation: {
570
- kind: "initial",
571
- prompt: (env.reviewReask ?? env.gateReviewInstruction)
572
- ?? buildInstructionTransportPrompt(
573
- admitted,
574
- engineSessionMaterialFromOptions({
575
- ...pickEngineAxis(env),
576
- packageRoot: env.packageRoot,
577
- }),
578
- ),
579
- },
580
- };
659
+ const turnProjection = roleTurnOptions(env, admitted, {
660
+ kind: "initial",
661
+ prompt: (env.reviewReask ?? env.gateReviewInstruction)
662
+ ?? buildInstructionTransportPrompt(
663
+ admitted,
664
+ engineSessionMaterialFromOptions({
665
+ ...pickEngineAxis(env),
666
+ packageRoot: env.packageRoot,
667
+ }),
668
+ ),
669
+ });
581
670
  const turnRequest = buildInstructionSeatTurnRequest(admitted, turnProjection);
582
671
  const result = await runPostAdmissionOneShot({
583
672
  admitted,
@@ -869,7 +958,27 @@ export async function runPublicInstructionSeatResume(
869
958
  },
870
959
  afterAdmittedLoad: async (admitted) => {
871
960
  await warnMissingMethodSkills(env.home, env.host, admitted.role, requiredMethodSkills(admitted), io.stdout);
872
- return { kind: "continue" as const, adapters: seatAdapters(admitted, env) };
961
+ const adapters = seatAdapters(admitted, env);
962
+ if ((env.unreadableReasksSpent ?? 0) === 0 && env.reviewReask === undefined) {
963
+ return { kind: "continue" as const, adapters };
964
+ }
965
+ const original = await adapters.trySettle(
966
+ admitted, env.principalAuthority, await readCurrentCourt(admitted.runDirectory),
967
+ );
968
+ const unsettled = POST_SUBMISSION_ROUTING[admitted.role] === undefined
969
+ ? heldUnreadableTerminal(original)
970
+ : unreadablePostSubmissionStatus(admitted, original) !== undefined;
971
+ // Select this reask's existing subject before dispatch. The common
972
+ // settlement then keeps it only when no fresh receipt supersedes it;
973
+ // actual failures still take their normal failure path.
974
+ return {
975
+ kind: "continue" as const,
976
+ adapters: original === undefined || !unsettled ? adapters : {
977
+ ...adapters,
978
+ trySettle: async (seat, authority, scope) =>
979
+ await adapters.trySettle(seat, authority, scope) ?? original,
980
+ },
981
+ };
873
982
  },
874
983
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
875
984
  });
@@ -881,6 +990,13 @@ export async function runPublicInstructionSeatResume(
881
990
  env,
882
991
  io,
883
992
  );
993
+ if (isUnreadableReaskExhausted(unreadableReask)) {
994
+ const terminal = result.terminal === undefined
995
+ ? undefined
996
+ : withUnsettledDirection(result.terminal);
997
+ presentOriginalVolume(held, terminal, io);
998
+ return terminal === undefined ? result : { ...result, terminal };
999
+ }
884
1000
  if (unreadableReask !== undefined) return unreadableReask;
885
1001
  if (result.admitted !== undefined && AUDITED_ROLES.has(result.admitted.role)
886
1002
  && result.terminal?.roleOutcome.kind === "accepted") {
@@ -908,12 +1024,15 @@ export async function runPublicInstructionSeatResume(
908
1024
  return resume();
909
1025
  }
910
1026
 
911
- const QUEUE_CONCLUSIONS = new Set(["converged", "continue", "escalate"]);
912
1027
  const GATE_CHILD_ROLES = new Set(["notary", "auditor", "inspector", "countersign"]);
913
1028
 
914
1029
  /**
915
1030
  * A gate child's conclusion outside the three states goes back to that child.
916
- * The words are the existing officer re-ask. A host failure stops here.
1031
+ * The words are the existing officer re-ask. Each reask spends this loop's own
1032
+ * copy of the configured ceiling; exhaustion keeps the terminal already in hand.
1033
+ * A host failure stops here.
1034
+ * Countersign already spends that ceiling inside its own seat, or the summoning
1035
+ * gate does. This loop does not open a second one for the same conclusion.
917
1036
  */
918
1037
  async function queueConclusionFromChild(
919
1038
  child: AdmittedRoleInvocation,
@@ -925,26 +1044,45 @@ async function queueConclusionFromChild(
925
1044
  | undefined
926
1045
  > {
927
1046
  if (!GATE_CHILD_ROLES.has(child.role)) return undefined;
928
- let current = child;
1047
+ if (child.role === "countersign") {
1048
+ const terminal = await trySettlePublicSeat(
1049
+ child, env.principalAuthority, await readCurrentCourt(child.runDirectory),
1050
+ );
1051
+ const status = latestQueueStatus(terminal);
1052
+ if (terminal !== undefined && typeof status === "string" && REVIEW_QUEUE_STATUSES.has(status)) {
1053
+ return { admitted: child, terminal, status };
1054
+ }
1055
+ return { stop: { exitCode: 0, ...(terminal === undefined ? {} : { terminal }) } };
1056
+ }
1057
+ let current: AdmittedRoleInvocation = child;
1058
+ // Notary, auditor, and inspector have no seat-local status reask. This loop
1059
+ // is their one budget. The child resume keeps the caller's env so delivery,
1060
+ // failure recovery, and other reask loops stay separate.
1061
+ let budgetEnv: InstructionSeatRunEnv = { ...env, unreadableReasksSpent: 0 };
929
1062
  for (;;) {
930
1063
  const terminal = await trySettlePublicSeat(
931
1064
  current,
932
1065
  env.principalAuthority,
933
- undefined,
1066
+ await readCurrentCourt(current.runDirectory),
934
1067
  );
935
1068
  const status = latestQueueStatus(terminal);
936
- if (terminal !== undefined && status !== undefined && QUEUE_CONCLUSIONS.has(status)) {
1069
+ if (terminal !== undefined && typeof status === "string" && REVIEW_QUEUE_STATUSES.has(status)) {
937
1070
  return { admitted: current, terminal, status };
938
1071
  }
939
- const reasked = await runPublicInstructionSeatResume({
940
- runId: current.runId,
941
- message: officerConclusionReask(status),
942
- }, env, io);
943
- if (reasked.exitCode !== 0 || reasked.admitted === undefined || reasked.terminal === undefined) {
1072
+ const reasked = await reaskUnreadablePostSubmissionStatus(
1073
+ current, terminal, budgetEnv, io, officerConclusionReask(status),
1074
+ );
1075
+ if (reasked === undefined || isUnreadableReaskExhausted(reasked)) {
1076
+ return { stop: { exitCode: 0, ...(terminal === undefined ? {} : { terminal }) } };
1077
+ }
1078
+ budgetEnv = withUnreadableReask(budgetEnv)!;
1079
+ if (reasked.exitCode !== 0 || reasked.admitted === undefined || reasked.terminal === undefined
1080
+ || reasked.terminal.roleOutcome.kind === "no_receipt"
1081
+ || reasked.terminal.roleOutcome.decisiveFacts?.directionUnsettled === true) {
944
1082
  return { stop: reasked };
945
1083
  }
946
1084
  const reaskedStatus = latestQueueStatus(reasked.terminal);
947
- if (reaskedStatus !== undefined && QUEUE_CONCLUSIONS.has(reaskedStatus)) {
1085
+ if (typeof reaskedStatus === "string" && REVIEW_QUEUE_STATUSES.has(reaskedStatus)) {
948
1086
  return { admitted: reasked.admitted, terminal: reasked.terminal, status: reaskedStatus };
949
1087
  }
950
1088
  if (latestPayloadEscalated(reasked.terminal.roleOutcome)) return { stop: reasked };
@@ -965,7 +1103,24 @@ export async function continueParentAfterChild(
965
1103
  }
966
1104
  const admitted = loaded.admitted;
967
1105
  const resolved = await queueConclusionFromChild(child, env, io);
968
- if (resolved !== undefined && "stop" in resolved) return resolved.stop;
1106
+ if (resolved !== undefined && "stop" in resolved) {
1107
+ const stopped = resolved.stop;
1108
+ if (
1109
+ stopped.exitCode === 0
1110
+ && heldUnreadableTerminal(stopped.terminal)
1111
+ && stopped.terminal !== undefined
1112
+ ) {
1113
+ const parentTerminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
1114
+ if (
1115
+ parentTerminal?.roleOutcome.kind === "accepted"
1116
+ || parentTerminal?.roleOutcome.kind === "audit_escalation"
1117
+ ) {
1118
+ const terminal = presentUnsettledAudit(parentTerminal, stopped.terminal, io);
1119
+ return { exitCode: 0, admitted, terminal };
1120
+ }
1121
+ }
1122
+ return stopped;
1123
+ }
969
1124
  if (resolved !== undefined && resolved.status === "escalate") {
970
1125
  return { exitCode: 0, admitted: resolved.admitted, terminal: resolved.terminal };
971
1126
  }
@@ -973,26 +1128,61 @@ export async function continueParentAfterChild(
973
1128
  if (AUDITED_ROLES.has(admitted.role) && resolved.status === "converged") {
974
1129
  const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
975
1130
  if (terminal?.roleOutcome.kind !== "accepted") throw new Error("pending submission is not recorded");
976
- return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted.role, latestQueuePayload(resolved.terminal), resolved.admitted.runId);
1131
+ return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted, latestQueuePayload(resolved.terminal));
977
1132
  }
978
1133
  return runPublicInstructionSeatResume({
979
1134
  runId: parentRunId,
980
1135
  message: readableGateItem(latestQueuePayload(resolved.terminal)),
981
- }, { ...env, autoResumeLimit: 0 }, io);
1136
+ }, { ...env, unreadableReasksSpent: 0 }, io);
982
1137
  }
983
1138
  const result = await dispatchAdmitted(admitted, env, { ...io, omitFailureStderrDiagnostic: true });
984
1139
  showResumeErrorPointer(io, result.exitCode, result.terminal);
985
1140
  return result;
986
1141
  }
987
1142
 
1143
+ /**
1144
+ * A gate is already passed only when an officer run reviewed this submission
1145
+ * and its latest queue word is converged. The resumed seat's role is not itself
1146
+ * a pass. Unknown or unreadable evidence does not skip the gate.
1147
+ */
1148
+ async function officerPassedCurrentSubmission(input: {
1149
+ readonly env: InstructionSeatRunEnv;
1150
+ readonly parentSessionFile: string;
1151
+ readonly officer: GateOfficer;
1152
+ readonly toolCallId: string;
1153
+ }): Promise<boolean> {
1154
+ const { env, officer } = input;
1155
+ const pointer = await readDirectOfficerRunPointer(input.parentSessionFile, officer);
1156
+ if (pointer?.runDirectory === undefined) return false;
1157
+ if (pointer.submissionToolCallId !== input.toolCallId) return false;
1158
+ const officerRunId = runIdFromRunDirectory(pointer.runDirectory);
1159
+ if (officerRunId === undefined) return false;
1160
+ let admitted: AdmittedRoleInvocation;
1161
+ try {
1162
+ const loaded = await loadResumablePublicRole(
1163
+ env.home,
1164
+ officerRunId,
1165
+ env.principalAuthority,
1166
+ );
1167
+ admitted = loaded.admitted;
1168
+ } catch {
1169
+ return false;
1170
+ }
1171
+ if (admitted.role !== officer) return false;
1172
+ // A summon binding identifies the requested review, not its outcome. An open
1173
+ // court (including a failed host turn) must not borrow a previous court's seal.
1174
+ const court = await readCurrentCourt(admitted.runDirectory);
1175
+ const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, court);
1176
+ return latestQueueStatus(terminal) === "converged";
1177
+ }
1178
+
988
1179
  /** Finished submissions enter the existing audit gate after their tool call has returned. */
989
1180
  async function auditSubmittedRole(
990
1181
  turn: SeatRunResult,
991
1182
  env: InstructionSeatRunEnv,
992
1183
  io: CliIo,
993
- passedOfficer?: PackagedRole,
1184
+ resumedOfficer?: AdmittedRoleInvocation,
994
1185
  passedReceipt?: unknown,
995
- passedRunId?: string,
996
1186
  ): Promise<SeatRunResult> {
997
1187
  const admitted = turn.admitted;
998
1188
  if (admitted === undefined || !AUDITED_ROLES.has(admitted.role)
@@ -1002,13 +1192,28 @@ async function auditSubmittedRole(
1002
1192
  const record = isRecord(accepted)
1003
1193
  ? accepted as Record<string, unknown> : undefined;
1004
1194
  const status = admitted.role === "secretariat" ? record?.secretariatStatus : record?.status;
1195
+ // A gate summon already owns this conclusion's reasks. The seat must not
1196
+ // start another budget on the same run (parentRunPath is that summon).
1197
+ const gateOwnsStatusReask = typeof env.parentRunPath === "string" && env.parentRunPath.trim() !== "";
1005
1198
  if (admitted.role === "secretariat" && status !== "converged" && status !== "escalate") {
1006
- return runPublicInstructionSeatResume({ runId: admitted.runId, message: SECRETARIAT_STATUS_REASK },
1007
- { ...env, autoResumeLimit: 0 }, io);
1199
+ if (gateOwnsStatusReask) return turn;
1200
+ const next = withUnreadableReask(env);
1201
+ if (next === undefined) {
1202
+ const terminal = withUnsettledDirection(turn.terminal);
1203
+ io.stdout(formatTerminalResult(terminal));
1204
+ return { ...turn, terminal };
1205
+ }
1206
+ return runPublicInstructionSeatResume({ runId: admitted.runId, message: SECRETARIAT_STATUS_REASK }, next, io);
1008
1207
  }
1009
- if (admitted.role === "countersign" && (typeof status !== "string" || !QUEUE_CONCLUSIONS.has(status))) {
1010
- return runPublicInstructionSeatResume({ runId: admitted.runId, message: COUNTERSIGN_STATUS_REASK },
1011
- { ...env, autoResumeLimit: 0 }, io);
1208
+ if (admitted.role === "countersign" && (typeof status !== "string" || !REVIEW_QUEUE_STATUSES.has(status))) {
1209
+ if (gateOwnsStatusReask) return turn;
1210
+ const next = withUnreadableReask(env);
1211
+ if (next === undefined) {
1212
+ const terminal = withUnsettledDirection(turn.terminal);
1213
+ io.stdout(formatTerminalResult(terminal));
1214
+ return { ...turn, terminal };
1215
+ }
1216
+ return runPublicInstructionSeatResume({ runId: admitted.runId, message: officerConclusionReask(status) }, next, io);
1012
1217
  }
1013
1218
  // Self-escalation is a valid open routing state for these seats only.
1014
1219
  // Doctor's declared domain is completed|refused — an open "escalate" must
@@ -1044,11 +1249,18 @@ async function auditSubmittedRole(
1044
1249
  },
1045
1250
  abort() {},
1046
1251
  };
1252
+ const passedThisSubmission = (officer: GateOfficer) => officerPassedCurrentSubmission({
1253
+ env,
1254
+ parentSessionFile: sessionFile,
1255
+ officer,
1256
+ toolCallId,
1257
+ });
1047
1258
  if (admitted.role === "doctor") {
1048
- if (passedOfficer !== "auditor") {
1259
+ if (!await passedThisSubmission("auditor")) {
1049
1260
  let lastSummon: PublicSummonResult | undefined;
1050
1261
  const decision = await createPiDoctorAuditor()({
1051
1262
  context, submission: accepted,
1263
+ ...(env.autoResumeLimit === undefined ? {} : { autoResumeLimit: env.autoResumeLimit }),
1052
1264
  ...(env.signal === undefined ? {} : { signal: env.signal }),
1053
1265
  summonAuditor: async (subject, sourceRunDirectory, signal, reask, submission) => {
1054
1266
  const summoned = await summonPublicRole({
@@ -1068,6 +1280,7 @@ async function auditSubmittedRole(
1068
1280
  if (officerSession !== undefined) {
1069
1281
  bookDirectOfficerRunPointer({
1070
1282
  parentSessionFile: sessionFile, officer: "auditor", sessionFile: officerSession,
1283
+ submissionToolCallId: toolCallId,
1071
1284
  ...(summoned.runDirectory === undefined ? {} : { runDirectory: summoned.runDirectory }),
1072
1285
  });
1073
1286
  }
@@ -1078,7 +1291,15 @@ async function auditSubmittedRole(
1078
1291
  if (decision.status === "continue") {
1079
1292
  return runPublicInstructionSeatResume({
1080
1293
  runId: admitted.runId, message: readableGateItem(decision.receipt ?? decision.violations),
1081
- }, { ...env, autoResumeLimit: 0 }, io);
1294
+ }, { ...env, unreadableReasksSpent: 0 }, io);
1295
+ }
1296
+ if (decision.status === "received") {
1297
+ if (lastSummon?.terminal === undefined) {
1298
+ throw new Error("doctor audit kept an unreadable reply with no terminal");
1299
+ }
1300
+ if (turn.terminal === undefined) throw new Error("doctor submission has no terminal");
1301
+ const terminal = presentUnsettledAudit(turn.terminal, lastSummon.terminal, io);
1302
+ return { exitCode: 0, admitted, terminal };
1082
1303
  }
1083
1304
  if (decision.status !== "converged") {
1084
1305
  if (lastSummon?.terminal !== undefined) {
@@ -1099,7 +1320,7 @@ async function auditSubmittedRole(
1099
1320
  ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
1100
1321
  });
1101
1322
  let chain: Awaited<ReturnType<typeof runJudgeGates>>;
1102
- let officerRunId = passedRunId;
1323
+ let officerRunId = resumedOfficer?.runId;
1103
1324
  const runGate = (subject: Parameters<typeof requireSubmissionGate>[0]["subject"]) =>
1104
1325
  requireSubmissionGate({
1105
1326
  context,
@@ -1107,6 +1328,7 @@ async function auditSubmittedRole(
1107
1328
  toolCallId,
1108
1329
  submission: accepted,
1109
1330
  summonOfficer,
1331
+ ...(env.autoResumeLimit === undefined ? {} : { autoResumeLimit: env.autoResumeLimit }),
1110
1332
  ...(env.signal === undefined ? {} : { signal: env.signal }),
1111
1333
  hostActions: {
1112
1334
  failInfrastructure(error): never { throw error; },
@@ -1115,8 +1337,7 @@ async function auditSubmittedRole(
1115
1337
  });
1116
1338
  if (admitted.role === "judge") {
1117
1339
  chain = await runJudgeGates({
1118
- gateAlreadyConverged: async (subject) => passedOfficer === "auditor"
1119
- || (passedOfficer === "notary" && subject.kind === "judge_draft"),
1340
+ gateAlreadyConverged: async (subject) => passedThisSubmission(gateOfficerForSubject(subject)),
1120
1341
  runGate,
1121
1342
  });
1122
1343
  } else {
@@ -1126,7 +1347,7 @@ async function auditSubmittedRole(
1126
1347
  ? { kind: "secretariat_verdict" as const }
1127
1348
  : { kind: "countersign_verdict" as const };
1128
1349
  const officer = gateOfficerForSubject(subject);
1129
- if (passedOfficer === officer) {
1350
+ if (await passedThisSubmission(officer)) {
1130
1351
  chain = { status: "converged", passes: [] };
1131
1352
  } else {
1132
1353
  const pass = await runGate(subject);
@@ -1140,10 +1361,18 @@ async function auditSubmittedRole(
1140
1361
  receipt: pass.receipt,
1141
1362
  ...(pass.runId === undefined ? {} : { runId: pass.runId }),
1142
1363
  ...(pass.runDirectory === undefined ? {} : { runDirectory: pass.runDirectory }),
1364
+ ...(pass.terminal === undefined ? {} : { terminal: pass.terminal }),
1143
1365
  }],
1144
1366
  };
1145
1367
  }
1146
1368
  }
1369
+ if (chain.status === "needs_reask") {
1370
+ const officerTerminal = chain.passes.at(-1)?.terminal;
1371
+ if (officerTerminal === undefined) throw new Error("unreadable audit has no terminal result");
1372
+ if (turn.terminal === undefined) throw new Error("audited submission has no terminal");
1373
+ const terminal = presentUnsettledAudit(turn.terminal, officerTerminal, io);
1374
+ return { exitCode: 0, admitted, terminal };
1375
+ }
1147
1376
  if (chain.status === "escalate") {
1148
1377
  const escalation = chain.passes.at(-1);
1149
1378
  const escalatedRunId = escalation?.runId
@@ -1151,7 +1380,9 @@ async function auditSubmittedRole(
1151
1380
  ? undefined : runIdFromRunDirectory(escalation.runDirectory));
1152
1381
  if (escalatedRunId === undefined) throw new Error("escalated audit has no resumable run identity");
1153
1382
  const officer = await loadResumablePublicRole(env.home, escalatedRunId, env.principalAuthority);
1154
- const terminal = await trySettlePublicSeat(officer.admitted, env.principalAuthority, undefined);
1383
+ const terminal = await trySettlePublicSeat(
1384
+ officer.admitted, env.principalAuthority, await readCurrentCourt(officer.admitted.runDirectory),
1385
+ );
1155
1386
  if (terminal === undefined) throw new Error("escalated audit has no terminal result");
1156
1387
  io.stdout(formatTerminalResult(terminal));
1157
1388
  return { exitCode: 0, admitted: officer.admitted, terminal };
@@ -1160,7 +1391,7 @@ async function auditSubmittedRole(
1160
1391
  return runPublicInstructionSeatResume({
1161
1392
  runId: admitted.runId,
1162
1393
  message: readableGateItem(chain.passes.at(-1)?.receipt),
1163
- }, { ...env, autoResumeLimit: 0 }, io);
1394
+ }, { ...env, unreadableReasksSpent: 0 }, io);
1164
1395
  }
1165
1396
  if (admitted.role === "secretariat") {
1166
1397
  const pass = chain.passes.at(-1);