@akagilnc/pi-workflow-roles 0.1.5382 → 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 (51) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/dist/acp-host/production-host.js +2523 -1880
  4. package/dist/archivist-record-pointer.js +38 -0
  5. package/dist/compliance-transport.js +5 -0
  6. package/dist/doctor-auditor.js +1 -0
  7. package/dist/engine-detour-usage.js +6 -6
  8. package/dist/headless-host/production-host.js +1097 -454
  9. package/dist/judge-role.js +14 -1
  10. package/dist/migrate-book-topology.js +190 -74
  11. package/dist/navigator-attendance.js +10 -9
  12. package/dist/navigator-invocation-identity.js +18 -0
  13. package/dist/pi/role-turn-host.js +9 -0
  14. package/dist/public-cli/cli.js +1 -0
  15. package/dist/public-cli/instruction-seat-run.js +250 -44
  16. package/dist/public-cli/main.js +929 -323
  17. package/dist/public-cli/post-admission.js +427 -244
  18. package/dist/public-cli/settlement.js +111 -48
  19. package/dist/public-cli/terminal.js +12 -1
  20. package/dist/public-cli/turn-request.js +3 -0
  21. package/dist/receipt-delivery-policy.js +152 -17
  22. package/dist/submission-errors.js +11 -1
  23. package/dist/submission-gate.js +27 -4
  24. package/package.json +1 -1
  25. package/src/archivist-record-pointer.ts +48 -0
  26. package/src/compliance-transport.ts +10 -2
  27. package/src/doctor-auditor.ts +3 -0
  28. package/src/engine-detour-usage.ts +7 -6
  29. package/src/external-host-turn-loop.ts +24 -12
  30. package/src/gatekeeper-role.ts +2 -2
  31. package/src/host-contracts.ts +7 -0
  32. package/src/judge-role.ts +24 -5
  33. package/src/navigator-attendance.ts +17 -12
  34. package/src/navigator-invocation-identity.ts +31 -0
  35. package/src/pi/adapter.ts +33 -8
  36. package/src/pi/role-turn-host.ts +7 -0
  37. package/src/public-cli/cli.ts +1 -0
  38. package/src/public-cli/instruction-seat-run.ts +283 -49
  39. package/src/public-cli/post-admission.ts +556 -324
  40. package/src/public-cli/settlement.ts +112 -50
  41. package/src/public-cli/terminal.ts +16 -4
  42. package/src/public-cli/turn-request.ts +10 -1
  43. package/src/receipt-delivery-policy.ts +209 -17
  44. package/src/role-envelope.ts +5 -1
  45. package/src/role-runtime-dependencies.ts +1 -0
  46. package/src/role-runtime.ts +65 -21
  47. package/src/submission-correctable-error.ts +5 -2
  48. package/src/submission-errors.ts +12 -1
  49. package/src/submission-gate.ts +34 -4
  50. package/src/worker-role.ts +19 -7
  51. package/src/worker-submission-gates.ts +29 -7
@@ -31,7 +31,10 @@ import {
31
31
  } from "./navigator-session-contracts.js";
32
32
  import { sitianReport } from "./sitian-facade.js";
33
33
  import { issueRoot, subjectPath } from "./work-subject-identity.js";
34
- import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE } from "./receipt-delivery-policy.js";
34
+ import {
35
+ createReceiptDeliveryPolicy,
36
+ NO_RECEIPT_LIFECYCLE_ENTRY_TYPE
37
+ } from "./receipt-delivery-policy.js";
35
38
  import { navigatorProseFromUnknown } from "./package-contracts/navigator-output.js";
36
39
  import { persistNavigatorWorkBase } from "./navigator-work-base.js";
37
40
  import { sha256Hex } from "./sha256.js";
@@ -250,8 +253,8 @@ function createNavigatorAttendance(options) {
250
253
  try {
251
254
  try {
252
255
  if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
253
- const delivery = createReceiptDeliveryPolicy();
254
- const promptAllowingRejectedPrepare = async (text, deliveryRequest) => {
256
+ const delivery = createReceiptDeliveryPolicy(options.deliveryRequestLimit);
257
+ const promptAllowingRejectedPrepare = async (text) => {
255
258
  await persistNavigatorWorkBase(activeSession.recordPointer(), { subject, authority });
256
259
  const entryStart = activeSession.entries().length;
257
260
  prepareBatchRejected = false;
@@ -278,17 +281,15 @@ function createNavigatorAttendance(options) {
278
281
  delivery.recordNestedNoReceipt(sessionNoReceipt);
279
282
  return;
280
283
  }
281
- if (deliveryRequest && output === void 0) delivery.recordDeliveryRequest();
282
284
  };
283
- await promptAllowingRejectedPrepare(request, false);
285
+ await promptAllowingRejectedPrepare(request);
284
286
  if (boundSettlement !== void 0) {
285
287
  while (output === void 0 && prepareBatchRejected && delivery.nextAction() === "request-delivery") {
286
- await promptAllowingRejectedPrepare(JSON.stringify(delivery.deliveryState()), true);
288
+ delivery.recordDeliveryRequest();
289
+ await promptAllowingRejectedPrepare(JSON.stringify(delivery.deliveryState()));
287
290
  }
288
291
  if (output === void 0 && delivery.nextAction() === "request-delivery") {
289
- while (delivery.nextAction() === "request-delivery") {
290
- delivery.recordDeliveryRequest();
291
- }
292
+ delivery.closeBudget();
292
293
  }
293
294
  if (output === void 0 && delivery.nextAction() === "no-receipt" && activeSession.providerFailure?.() === void 0) {
294
295
  const facts = delivery.facts({ runPointer: activeSession.recordPointer(), attemptPointer: invocationId });
@@ -86,6 +86,23 @@ function markerMatchesExpectedIdentity(marker, expected) {
86
86
  }
87
87
  return true;
88
88
  }
89
+ function packagedRoleOutputRejectionReason(message) {
90
+ if (typeof message.toolName !== "string") return void 0;
91
+ if (!PACKAGED_ROLE_OUTPUT_TOOLS.has(message.toolName) && message.toolName !== "ak_submission_output") {
92
+ return void 0;
93
+ }
94
+ if (message.isError !== true) return void 0;
95
+ if (classifyPackagedRoleTerminalResult(message).kind !== "nonterminal") return void 0;
96
+ const content = message.content;
97
+ if (typeof content === "string") return content.trim();
98
+ if (!Array.isArray(content)) return "";
99
+ return content.map((part) => {
100
+ if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
101
+ return part.text;
102
+ }
103
+ return "";
104
+ }).join("").trim();
105
+ }
89
106
  function classifyPackagedRoleTerminalResult(message) {
90
107
  if (typeof message.toolName !== "string") return { kind: "nonterminal" };
91
108
  if (!PACKAGED_ROLE_OUTPUT_TOOLS.has(message.toolName) && message.toolName !== "ak_submission_output") return { kind: "nonterminal" };
@@ -181,6 +198,7 @@ export {
181
198
  isNavigatorInfrastructureFailureFact,
182
199
  markerMatchesExpectedIdentity,
183
200
  mintNavigatorInvocationId,
201
+ packagedRoleOutputRejectionReason,
184
202
  parseInvocationMarkerIdentity,
185
203
  resolveLifecycleInvocationPrincipal
186
204
  };
@@ -14,6 +14,7 @@ import { ExplicitInternalActivationError } from "../host-contracts.js";
14
14
  import { applyEngineChildEnv, ENGINE_MODEL_FLAG_NAME, normalizeEngineName } from "../engine-detour.js";
15
15
  import { projectActivationFlags } from "../role-activation-flags.js";
16
16
  import { encodeUserDialogueStdin } from "../user-dialogue-stdin.js";
17
+ import { RECEIPT_DELIVERY_LIMIT_ENV } from "../receipt-delivery-policy.js";
17
18
  /** Package-relative Internal role entrypoint (ADR 0052; same path as public-cli registry). */
18
19
  const INTERNAL_ROLE_ENTRYPOINT_RELATIVE = "extensions/role-runtime.ts";
19
20
  export function resolveInternalRoleEntrypoint(packageRoot) {
@@ -307,6 +308,14 @@ export function createPiRoleTurnHost(config) {
307
308
  delete env.AK_ROLE_HOST;
308
309
  else
309
310
  env.AK_ROLE_HOST = request.host.trim();
311
+ // #1132: the one effective delivery-request ceiling travels to the child
312
+ // on its own env so the in-child role runtime and the worker submission
313
+ // gate count with the same configured number the AK seam resolved. Omit
314
+ // must not inherit a parent value (same rule as the other AK_ROLE_ axes).
315
+ if (request.deliveryRequestLimit === undefined)
316
+ delete env[RECEIPT_DELIVERY_LIMIT_ENV];
317
+ else
318
+ env[RECEIPT_DELIVERY_LIMIT_ENV] = String(request.deliveryRequestLimit);
310
319
  applyEngineChildEnv(env, request.engine);
311
320
  // Nested auditor dossier tool binds the parent run pointer when published.
312
321
  if (typeof process.env.AK_ROLE_AUDITOR_SOURCE_RUN === "string"
@@ -846,6 +846,7 @@ export async function runAkRole(argv, env) {
846
846
  let current = result;
847
847
  while (current.exitCode === 0 && current.admitted?.correlationId !== undefined
848
848
  && current.terminal?.roleOutcome.kind === "accepted"
849
+ && current.terminal.roleOutcome.decisiveFacts?.directionUnsettled !== true
849
850
  && !latestPayloadEscalated(current.terminal.roleOutcome)) {
850
851
  const parentRunId = current.admitted.correlationId;
851
852
  resumeFailureContext = {
@@ -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";
@@ -29,12 +30,12 @@ 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
32
  import { presentControlledFailure, prepareSummonsResumeMaterials, roleTurnOptions, runPostAdmissionOneShot, runPostAdmissionResumable, runPostAdmissionSeatResume, showResumeErrorPointer, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
32
- import { buildAutoResumeContinuationPrompt, loadResumablePublicRole, markRunAdmitted, parentRunPathFromGatePointerInstruction, } from "./run-lifecycle.js";
33
+ import { buildAutoResumeContinuationPrompt, loadResumablePublicRole, markRunAdmitted, parentRunPathFromGatePointerInstruction, readCurrentCourt, } from "./run-lifecycle.js";
33
34
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
34
35
  import { presentStructuralRejection, readBoundSessionEntries, seatKnownFailureResolver, attachPostAuditProjection, trySettlePublicSeat, } from "./settlement.js";
35
36
  import { warnMissingMethodSkills } from "./machine-method-skills.js";
36
- import { formatTerminalResult, isLawfulTypedTerminalOutcome, } from "./terminal.js";
37
- 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";
38
39
  import { invokeCourtDiarist, latestPayloadEscalated, } from "./countersign-run.js";
39
40
  import { isRecord, errorText } from "../unknown-value.js";
40
41
  const AUDITED_ROLES = new Set(["judge", "fixer", "coder", "secretariat", "countersign", "doctor"]);
@@ -80,11 +81,83 @@ function unreadablePostSubmissionStatus(admitted, terminal) {
80
81
  ? undefined
81
82
  : route.reask ?? officerConclusionReask(status);
82
83
  }
83
- async function reaskUnreadablePostSubmissionStatus(admitted, terminal, env, io) {
84
- const message = unreadablePostSubmissionStatus(admitted, terminal);
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;
153
+ }
154
+ async function reaskUnreadablePostSubmissionStatus(admitted, terminal, env, io, message = unreadablePostSubmissionStatus(admitted, terminal)) {
85
155
  if (message === undefined)
86
156
  return undefined;
87
- 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);
88
161
  }
89
162
  function roleRecord(role) {
90
163
  const record = packagedRoleMetadata(role);
@@ -232,6 +305,13 @@ async function dispatchAdmitted(admitted, env, io) {
232
305
  };
233
306
  const result = await execute(env);
234
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
+ }
235
315
  if (unreadableReask !== undefined)
236
316
  return unreadableReask;
237
317
  if (AUDITED_ROLES.has(admitted.role) && result.terminal?.roleOutcome.kind === "accepted") {
@@ -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,
@@ -677,13 +751,37 @@ export async function runPublicInstructionSeatResume(request, env, io) {
677
751
  },
678
752
  afterAdmittedLoad: async (admitted) => {
679
753
  await warnMissingMethodSkills(env.home, env.host, admitted.role, requiredMethodSkills(admitted), io.stdout);
680
- 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
+ };
681
772
  },
682
773
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
683
774
  });
684
775
  const unreadableReask = result.admitted === undefined
685
776
  ? undefined
686
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
+ }
687
785
  if (unreadableReask !== undefined)
688
786
  return unreadableReask;
689
787
  if (result.admitted !== undefined && AUDITED_ROLES.has(result.admitted.role)
@@ -717,23 +815,42 @@ export async function runPublicInstructionSeatResume(request, env, io) {
717
815
  const GATE_CHILD_ROLES = new Set(["notary", "auditor", "inspector", "countersign"]);
718
816
  /**
719
817
  * 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.
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.
721
823
  */
722
824
  async function queueConclusionFromChild(child, env, io) {
723
825
  if (!GATE_CHILD_ROLES.has(child.role))
724
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
+ }
725
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 };
726
840
  for (;;) {
727
- const terminal = await trySettlePublicSeat(current, env.principalAuthority, undefined);
841
+ const terminal = await trySettlePublicSeat(current, env.principalAuthority, await readCurrentCourt(current.runDirectory));
728
842
  const status = latestQueueStatus(terminal);
729
843
  if (terminal !== undefined && typeof status === "string" && REVIEW_QUEUE_STATUSES.has(status)) {
730
844
  return { admitted: current, terminal, status };
731
845
  }
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) {
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) {
737
854
  return { stop: reasked };
738
855
  }
739
856
  const reaskedStatus = latestQueueStatus(reasked.terminal);
@@ -753,8 +870,20 @@ export async function continueParentAfterChild(parentRunId, child, env, io) {
753
870
  }
754
871
  const admitted = loaded.admitted;
755
872
  const resolved = await queueConclusionFromChild(child, env, io);
756
- if (resolved !== undefined && "stop" in resolved)
757
- 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
+ }
758
887
  if (resolved !== undefined && resolved.status === "escalate") {
759
888
  return { exitCode: 0, admitted: resolved.admitted, terminal: resolved.terminal };
760
889
  }
@@ -763,19 +892,50 @@ export async function continueParentAfterChild(parentRunId, child, env, io) {
763
892
  const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
764
893
  if (terminal?.roleOutcome.kind !== "accepted")
765
894
  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);
895
+ return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted, latestQueuePayload(resolved.terminal));
767
896
  }
768
897
  return runPublicInstructionSeatResume({
769
898
  runId: parentRunId,
770
899
  message: readableGateItem(latestQueuePayload(resolved.terminal)),
771
- }, { ...env, autoResumeLimit: 0 }, io);
900
+ }, { ...env, unreadableReasksSpent: 0 }, io);
772
901
  }
773
902
  const result = await dispatchAdmitted(admitted, env, { ...io, omitFailureStderrDiagnostic: true });
774
903
  showResumeErrorPointer(io, result.exitCode, result.terminal);
775
904
  return result;
776
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
+ }
777
937
  /** Finished submissions enter the existing audit gate after their tool call has returned. */
778
- async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, passedRunId) {
938
+ async function auditSubmittedRole(turn, env, io, resumedOfficer, passedReceipt) {
779
939
  const admitted = turn.admitted;
780
940
  if (admitted === undefined || !AUDITED_ROLES.has(admitted.role)
781
941
  || turn.terminal?.roleOutcome.kind !== "accepted")
@@ -786,11 +946,30 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
786
946
  const record = isRecord(accepted)
787
947
  ? accepted : undefined;
788
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() !== "";
789
952
  if (admitted.role === "secretariat" && status !== "converged" && status !== "escalate") {
790
- 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);
791
962
  }
792
963
  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);
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);
794
973
  }
795
974
  // Self-escalation is a valid open routing state for these seats only.
796
975
  // Doctor's declared domain is completed|refused — an open "escalate" must
@@ -827,11 +1006,18 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
827
1006
  },
828
1007
  abort() { },
829
1008
  };
1009
+ const passedThisSubmission = (officer) => officerPassedCurrentSubmission({
1010
+ env,
1011
+ parentSessionFile: sessionFile,
1012
+ officer,
1013
+ toolCallId,
1014
+ });
830
1015
  if (admitted.role === "doctor") {
831
- if (passedOfficer !== "auditor") {
1016
+ if (!await passedThisSubmission("auditor")) {
832
1017
  let lastSummon;
833
1018
  const decision = await createPiDoctorAuditor()({
834
1019
  context, submission: accepted,
1020
+ ...(env.autoResumeLimit === undefined ? {} : { autoResumeLimit: env.autoResumeLimit }),
835
1021
  ...(env.signal === undefined ? {} : { signal: env.signal }),
836
1022
  summonAuditor: async (subject, sourceRunDirectory, signal, reask, submission) => {
837
1023
  const summoned = await summonPublicRole({
@@ -851,6 +1037,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
851
1037
  if (officerSession !== undefined) {
852
1038
  bookDirectOfficerRunPointer({
853
1039
  parentSessionFile: sessionFile, officer: "auditor", sessionFile: officerSession,
1040
+ submissionToolCallId: toolCallId,
854
1041
  ...(summoned.runDirectory === undefined ? {} : { runDirectory: summoned.runDirectory }),
855
1042
  });
856
1043
  }
@@ -861,7 +1048,16 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
861
1048
  if (decision.status === "continue") {
862
1049
  return runPublicInstructionSeatResume({
863
1050
  runId: admitted.runId, message: readableGateItem(decision.receipt ?? decision.violations),
864
- }, { ...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 };
865
1061
  }
866
1062
  if (decision.status !== "converged") {
867
1063
  if (lastSummon?.terminal !== undefined) {
@@ -883,13 +1079,14 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
883
1079
  ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
884
1080
  });
885
1081
  let chain;
886
- let officerRunId = passedRunId;
1082
+ let officerRunId = resumedOfficer?.runId;
887
1083
  const runGate = (subject) => requireSubmissionGate({
888
1084
  context,
889
1085
  subject,
890
1086
  toolCallId,
891
1087
  submission: accepted,
892
1088
  summonOfficer,
1089
+ ...(env.autoResumeLimit === undefined ? {} : { autoResumeLimit: env.autoResumeLimit }),
893
1090
  ...(env.signal === undefined ? {} : { signal: env.signal }),
894
1091
  hostActions: {
895
1092
  failInfrastructure(error) { throw error; },
@@ -898,8 +1095,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
898
1095
  });
899
1096
  if (admitted.role === "judge") {
900
1097
  chain = await runJudgeGates({
901
- gateAlreadyConverged: async (subject) => passedOfficer === "auditor"
902
- || (passedOfficer === "notary" && subject.kind === "judge_draft"),
1098
+ gateAlreadyConverged: async (subject) => passedThisSubmission(gateOfficerForSubject(subject)),
903
1099
  runGate,
904
1100
  });
905
1101
  }
@@ -910,7 +1106,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
910
1106
  ? { kind: "secretariat_verdict" }
911
1107
  : { kind: "countersign_verdict" };
912
1108
  const officer = gateOfficerForSubject(subject);
913
- if (passedOfficer === officer) {
1109
+ if (await passedThisSubmission(officer)) {
914
1110
  chain = { status: "converged", passes: [] };
915
1111
  }
916
1112
  else {
@@ -926,10 +1122,20 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
926
1122
  receipt: pass.receipt,
927
1123
  ...(pass.runId === undefined ? {} : { runId: pass.runId }),
928
1124
  ...(pass.runDirectory === undefined ? {} : { runDirectory: pass.runDirectory }),
1125
+ ...(pass.terminal === undefined ? {} : { terminal: pass.terminal }),
929
1126
  }],
930
1127
  };
931
1128
  }
932
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
+ }
933
1139
  if (chain.status === "escalate") {
934
1140
  const escalation = chain.passes.at(-1);
935
1141
  const escalatedRunId = escalation?.runId
@@ -938,7 +1144,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
938
1144
  if (escalatedRunId === undefined)
939
1145
  throw new Error("escalated audit has no resumable run identity");
940
1146
  const officer = await loadResumablePublicRole(env.home, escalatedRunId, env.principalAuthority);
941
- const terminal = await trySettlePublicSeat(officer.admitted, env.principalAuthority, undefined);
1147
+ const terminal = await trySettlePublicSeat(officer.admitted, env.principalAuthority, await readCurrentCourt(officer.admitted.runDirectory));
942
1148
  if (terminal === undefined)
943
1149
  throw new Error("escalated audit has no terminal result");
944
1150
  io.stdout(formatTerminalResult(terminal));
@@ -948,7 +1154,7 @@ async function auditSubmittedRole(turn, env, io, passedOfficer, passedReceipt, p
948
1154
  return runPublicInstructionSeatResume({
949
1155
  runId: admitted.runId,
950
1156
  message: readableGateItem(chain.passes.at(-1)?.receipt),
951
- }, { ...env, autoResumeLimit: 0 }, io);
1157
+ }, { ...env, unreadableReasksSpent: 0 }, io);
952
1158
  }
953
1159
  if (admitted.role === "secretariat") {
954
1160
  const pass = chain.passes.at(-1);