@akagilnc/pi-workflow-roles 0.1.5153 → 0.1.5196

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 (52) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dist/acp-host/production-host.js +13086 -13034
  4. package/dist/archivist-record-entry.js +4 -20
  5. package/dist/archivist-record-pointer.js +28 -0
  6. package/dist/audit-escalation.js +3 -119
  7. package/dist/auditor-soul.js +32 -0
  8. package/dist/doctor-auditor.js +26 -0
  9. package/dist/headless-host/production-host.js +11218 -11166
  10. package/dist/judge-role.js +73 -0
  11. package/dist/migrate-book-topology.js +8 -0
  12. package/dist/package-contracts/judge-output.js +1 -18
  13. package/dist/package-contracts/terminating-tools.js +2 -2
  14. package/dist/public-cli/cli.js +2 -2
  15. package/dist/public-cli/countersign-run.js +11 -0
  16. package/dist/public-cli/instruction-seat-run.js +428 -48
  17. package/dist/public-cli/invocation.js +10 -5
  18. package/dist/public-cli/main.js +10049 -8911
  19. package/dist/public-cli/settlement.js +28 -8
  20. package/dist/public-role-summons.js +6 -4
  21. package/dist/submission-errors.js +1 -1
  22. package/dist/submission-gate.js +165 -0
  23. package/dist/submission-ledger.js +0 -27
  24. package/dist/worker-submission-contracts.js +8 -0
  25. package/package.json +1 -1
  26. package/src/archivist-record-entry.ts +5 -49
  27. package/src/archivist-record-pointer.ts +49 -0
  28. package/src/audit-escalation.ts +4 -217
  29. package/src/auditor-soul.ts +33 -0
  30. package/src/doctor-role.ts +11 -5
  31. package/src/gatekeeper-role.ts +2 -2
  32. package/src/host-contracts.ts +0 -25
  33. package/src/judge-role.ts +63 -90
  34. package/src/package-contracts/judge-output.ts +0 -17
  35. package/src/package-contracts/terminating-tools.ts +0 -2
  36. package/src/pi/adapter.ts +1 -33
  37. package/src/public-cli/cli.ts +2 -2
  38. package/src/public-cli/countersign-run.ts +11 -0
  39. package/src/public-cli/instruction-seat-run.ts +433 -16
  40. package/src/public-cli/invocation.ts +9 -3
  41. package/src/public-cli/post-admission.ts +0 -1
  42. package/src/public-cli/settlement.ts +32 -8
  43. package/src/public-role-summons.ts +9 -6
  44. package/src/role-envelope.ts +0 -32
  45. package/src/role-runtime-dependencies.ts +0 -4
  46. package/src/role-runtime.ts +15 -218
  47. package/src/submission-errors.ts +1 -1
  48. package/src/submission-gate.ts +46 -10
  49. package/src/submission-ledger.ts +0 -27
  50. package/src/worker-role.ts +23 -102
  51. package/src/worker-submission-contracts.ts +8 -0
  52. package/src/worker-submission-gates.ts +2 -2
@@ -199,6 +199,17 @@ export function courtTicketNumbersFromOutcome(
199
199
  return latest;
200
200
  }
201
201
 
202
+ /** Latest payload only. An earlier escalate must not block a later non-escalate submission. */
203
+ export function latestPayloadEscalated(
204
+ roleOutcome: TerminalRoleOutcome | undefined,
205
+ ): boolean {
206
+ if (roleOutcome === undefined) return false;
207
+ if (roleOutcome.kind === "audit_escalation") return true;
208
+ if (roleOutcome.kind !== "accepted") return false;
209
+ const payloads = roleOutcome.payloads ?? [];
210
+ return isEscalatePayload(payloads[payloads.length - 1]);
211
+ }
212
+
202
213
  /** Routing boolean over the child's own typed sequence — does not pick or rewrite a sole row. */
203
214
  export function courtDiaristEscalated(
204
215
  roleOutcome: TerminalRoleOutcome | undefined,
@@ -4,9 +4,26 @@
4
4
  * starts two ordinary single-axis runs here. Countersign keeps deferred
5
5
  * identity on this entry; the court diarist station stays in countersign-run.
6
6
  */
7
- import { resolve, sep } from "node:path";
7
+ import { dirname, resolve, sep } from "node:path";
8
+ import { AUDITOR_DOSSIER_PROMPT } from "../compliance-transport.ts";
9
+ import { bookDirectOfficerRunPointer } from "../archivist-record-pointer.ts";
10
+ import { createPiDoctorAuditor } from "../doctor-auditor.ts";
8
11
 
9
- import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
12
+ import type { DurablePrincipalAuthority, HostContext, RoleTurnRequest } from "../host-contracts.ts";
13
+ import { officerConclusionReask, gateOfficerForSubject } from "../gatekeeper-role.ts";
14
+ import { runJudgeGates } from "../judge-role.ts";
15
+ import { WORKER_DONE_STATUSES } from "../worker-submission-contracts.ts";
16
+ import { SECRETARIAT_GATE_OFFICER_ENTRY_TYPE } from "../secretariat-contracts.ts";
17
+ import { readableGateItem } from "../readable-gate-item.ts";
18
+ import { runIdFromRunDirectory } from "../run-terminal-artifacts.ts";
19
+ import { createDefaultGateOfficerSummon, requireSubmissionGate } from "../submission-gate.ts";
20
+ import { readRecordedSubmissionRows } from "../submission-ledger.ts";
21
+ import { summonPublicRole, type PublicSummonResult } from "../public-role-summons.ts";
22
+ import { sessionFileFromPublicSummon } from "../session-assistant-usage.ts";
23
+ import {
24
+ latestQueuePayload,
25
+ latestQueueStatus,
26
+ } from "../submission-gate.ts";
10
27
  import { isSafePositiveTicketNumber, readBoardTicketNumber } from "../run-ticket-number.ts";
11
28
  import { NotarySourceRunError, resolveNotarySourceRunLocator } from "../notary-source-run.ts";
12
29
  import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.ts";
@@ -17,7 +34,7 @@ import {
17
34
  packagedRebindSourceOnResume,
18
35
  packagedRoleMetadata,
19
36
  } from "../packaged-role-registry.ts";
20
- import { isAuditorSoulRole } from "../auditor-soul.ts";
37
+ import { isAuditorSoulRole, readAuditorResumeBinding } from "../auditor-soul.ts";
21
38
  import { CliUsageError } from "./cli-errors.ts";
22
39
  import {
23
40
  admitPublicRole,
@@ -59,7 +76,9 @@ import {
59
76
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts";
60
77
  import {
61
78
  presentStructuralRejection,
79
+ readBoundSessionEntries,
62
80
  seatKnownFailureResolver,
81
+ attachPostAuditProjection,
63
82
  trySettlePublicSeat,
64
83
  } from "./settlement.ts";
65
84
  import type { CliIo } from "./cli-io.ts";
@@ -68,6 +87,7 @@ import {
68
87
  formatTerminalResult,
69
88
  isLawfulTypedTerminalOutcome,
70
89
  type TerminalResult,
90
+ type TerminalRoleName,
71
91
  } from "./terminal.ts";
72
92
  import {
73
93
  admittedSeatTurnDetails,
@@ -77,10 +97,10 @@ import {
77
97
  } from "./turn-request.ts";
78
98
  import {
79
99
  invokeCourtDiarist,
100
+ latestPayloadEscalated,
80
101
  runCountersignCourtDiaristStation,
81
102
  type CountersignRunEnv,
82
103
  } from "./countersign-run.ts";
83
-
84
104
  export type InstructionSeatRunEnv = PostAdmissionEnv & Pick<
85
105
  CountersignRunEnv,
86
106
  "reviewReask" | "gateReviewInstruction" | "parentRunPath" | "runCourtDiaristStation"
@@ -92,6 +112,76 @@ type SeatRunResult = {
92
112
  terminal?: TerminalResult;
93
113
  };
94
114
 
115
+ const AUDITED_ROLES = new Set<PackagedRole>(["judge", "fixer", "coder", "secretariat", "countersign", "doctor"]);
116
+ const SECRETARIAT_STATUS_REASK = "secretariatStatus 不是 converged、escalate 之一。请重新交卷,secretariatStatus 写明其一。";
117
+ const COUNTERSIGN_STATUS_REASK = "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。";
118
+ const WORKER_ROUTING_STATUSES: ReadonlySet<string> = new Set([
119
+ "planned", "completed", "refused", "partially_completed", "unfinished",
120
+ ]);
121
+ const WORKER_STATUS_REASK =
122
+ "status 不是 planned、completed、refused、partially_completed、unfinished 之一。请重新交卷,status 写明其一。";
123
+ const DOCTOR_ROUTING_STATUSES: ReadonlySet<string> = new Set(["completed", "refused"]);
124
+ const DOCTOR_STATUS_REASK =
125
+ "status 不是 completed、refused 之一。请重新交卷,status 写明其一。";
126
+ const POST_SUBMISSION_ROUTING: Partial<Record<PackagedRole, {
127
+ readonly statuses: ReadonlySet<string>;
128
+ readonly reask: string;
129
+ }>> = {
130
+ judge: {
131
+ statuses: new Set(["converged", "continue", "escalate"]),
132
+ reask: "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。",
133
+ },
134
+ coder: {
135
+ statuses: WORKER_ROUTING_STATUSES,
136
+ reask: WORKER_STATUS_REASK,
137
+ },
138
+ fixer: {
139
+ statuses: WORKER_ROUTING_STATUSES,
140
+ reask: WORKER_STATUS_REASK,
141
+ },
142
+ diarist: {
143
+ statuses: new Set(["completed", "escalate"]),
144
+ reask: "status 不是 completed、escalate 之一。请重新交卷,status 写明其一。",
145
+ },
146
+ // Doctor contract domain is completed|refused; open-domain escalate is accepted
147
+ // by the tool then routed back here before mandatory auditor (#1057).
148
+ doctor: {
149
+ statuses: DOCTOR_ROUTING_STATUSES,
150
+ reask: DOCTOR_STATUS_REASK,
151
+ },
152
+ } as const;
153
+
154
+ function unreadablePostSubmissionStatus(
155
+ admitted: AdmittedRoleInvocation,
156
+ terminal: TerminalResult | undefined,
157
+ ): string | undefined {
158
+ if (terminal?.roleOutcome.kind !== "accepted") return undefined;
159
+ const route = POST_SUBMISSION_ROUTING[admitted.role];
160
+ if (route === undefined) return undefined;
161
+ const payload = terminal.roleOutcome.payloads?.at(-1);
162
+ const status = payload !== null && typeof payload === "object" && !Array.isArray(payload)
163
+ ? (payload as Record<string, unknown>).status
164
+ : undefined;
165
+ return typeof status === "string" && route.statuses.has(status)
166
+ ? undefined
167
+ : route.reask;
168
+ }
169
+
170
+ async function reaskUnreadablePostSubmissionStatus(
171
+ admitted: AdmittedRoleInvocation,
172
+ terminal: TerminalResult | undefined,
173
+ env: InstructionSeatRunEnv,
174
+ io: CliIo,
175
+ ): Promise<SeatRunResult | undefined> {
176
+ const message = unreadablePostSubmissionStatus(admitted, terminal);
177
+ if (message === undefined) return undefined;
178
+ return runPublicInstructionSeatResume(
179
+ { runId: admitted.runId, message },
180
+ { ...env, autoResumeLimit: 0 },
181
+ io,
182
+ );
183
+ }
184
+
95
185
  function roleRecord(role: PackagedRole) {
96
186
  const record = packagedRoleMetadata(role);
97
187
  if (record === undefined) {
@@ -231,13 +321,21 @@ async function dispatchAdmitted(
231
321
  const record = roleRecord(admitted.role);
232
322
  await warnMissingMethodSkills(env.home, env.host, admitted.role, requiredMethodSkills(admitted), io.stdout);
233
323
  const adapters = seatAdapters(admitted, env);
324
+ const held: string[] = [];
325
+ const turnIo: CliIo = AUDITED_ROLES.has(admitted.role)
326
+ || POST_SUBMISSION_ROUTING[admitted.role] !== undefined
327
+ ? { stdout: (value) => held.push(value), stderr: io.stderr }
328
+ : io;
234
329
  const execute = async (activeEnv: InstructionSeatRunEnv): Promise<SeatRunResult> => {
330
+ if (activeEnv.correlationId !== undefined && activeEnv.correlationId.trim() !== "") {
331
+ await recordAdmittedCorrelation(admitted, activeEnv.correlationId);
332
+ }
235
333
  const auto = "inCallAutoResume" in record && record.inCallAutoResume === true;
236
334
  if (auto) {
237
335
  return await runPostAdmissionResumable({
238
336
  admitted,
239
337
  env: activeEnv,
240
- io,
338
+ io: turnIo,
241
339
  buildInitialRequest: () => buildInstructionSeatTurnRequest(
242
340
  admitted,
243
341
  roleTurnOptions(activeEnv, admitted, { kind: "initial", prompt: initialPrompt(admitted, activeEnv) }),
@@ -259,7 +357,7 @@ async function dispatchAdmitted(
259
357
  return await runPostAdmissionOneShot({
260
358
  admitted,
261
359
  env: activeEnv,
262
- io,
360
+ io: turnIo,
263
361
  request: buildInstructionSeatTurnRequest(
264
362
  admitted,
265
363
  roleTurnOptions(activeEnv, admitted, { kind: "initial", prompt: initialPrompt(admitted, activeEnv) }),
@@ -268,7 +366,19 @@ async function dispatchAdmitted(
268
366
  ...(activeEnv.engine === undefined ? {} : { effectiveEngine: activeEnv.engine }),
269
367
  });
270
368
  };
271
- return execute(env);
369
+ const result = await execute(env);
370
+ const unreadableReask = await reaskUnreadablePostSubmissionStatus(
371
+ admitted,
372
+ result.terminal,
373
+ env,
374
+ io,
375
+ );
376
+ if (unreadableReask !== undefined) return unreadableReask;
377
+ if (AUDITED_ROLES.has(admitted.role) && result.terminal?.roleOutcome.kind === "accepted") {
378
+ return auditSubmittedRole(result, env, io);
379
+ }
380
+ for (const value of held) io.stdout(value);
381
+ return result;
272
382
  }
273
383
 
274
384
  /**
@@ -297,7 +407,7 @@ async function runOmittedLensBatch(
297
407
  packageRoot: env.packageRoot,
298
408
  ...(env.signal === undefined ? {} : { signal: env.signal }),
299
409
  ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
300
- roleTurnHost: env.roleTurnHost,
410
+ ...(env.hostAdapters === undefined ? { roleTurnHost: env.roleTurnHost } : {}),
301
411
  ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
302
412
  principalAuthority: env.principalAuthority,
303
413
  ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
@@ -560,7 +670,13 @@ export async function runPublicInstructionSeat(
560
670
  throw error;
561
671
  }
562
672
  if (packagedAdmitsCountersign(role)) {
563
- return runCountersignBody(parsed, env, io);
673
+ const held: string[] = [];
674
+ const result = await runCountersignBody(parsed, env, {
675
+ stdout: (value) => held.push(value), stderr: io.stderr,
676
+ });
677
+ if (result.terminal?.roleOutcome.kind === "accepted") return auditSubmittedRole(result, env, io);
678
+ for (const value of held) io.stdout(value);
679
+ return result;
564
680
  }
565
681
 
566
682
  if ("parallelLenses" in record && record.parallelLenses === true) {
@@ -713,7 +829,7 @@ export async function runPublicInstructionSeat(
713
829
  }
714
830
  }
715
831
  if (record.sameParent === "subject-source" && auditorSource !== undefined) {
716
- await persistAdmittedSourceRunPath(admitted, auditorSource);
832
+ await persistAdmittedSourceRunPath(admitted, auditorSource, auditorSubject);
717
833
  }
718
834
 
719
835
  const runAdmitted = async (): Promise<SeatRunResult> => {
@@ -765,10 +881,17 @@ export async function runPublicInstructionSeatResume(
765
881
  env: InstructionSeatRunEnv,
766
882
  io: CliIo,
767
883
  ): Promise<SeatRunResult> {
768
- return runPostAdmissionSeatResume<AdmittedRoleInvocation>({
884
+ const resume = async () => {
885
+ const held: string[] = [];
886
+ const turnIo: CliIo = {
887
+ stdout: (value) => held.push(value),
888
+ stderr: io.stderr,
889
+ };
890
+ const result = await runPostAdmissionSeatResume<AdmittedRoleInvocation>({
891
+
769
892
  request,
770
893
  env,
771
- io,
894
+ io: turnIo,
772
895
  load: async (effective) => {
773
896
  const loaded = await loadResumablePublicRole(env.home, effective.runId, env.principalAuthority);
774
897
  if (
@@ -804,10 +927,87 @@ export async function runPublicInstructionSeatResume(
804
927
  return { kind: "continue" as const, adapters: seatAdapters(admitted, env) };
805
928
  },
806
929
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
807
- });
930
+ });
931
+ const unreadableReask = result.admitted === undefined
932
+ ? undefined
933
+ : await reaskUnreadablePostSubmissionStatus(
934
+ result.admitted,
935
+ result.terminal,
936
+ env,
937
+ io,
938
+ );
939
+ if (unreadableReask !== undefined) return unreadableReask;
940
+ if (result.admitted !== undefined && AUDITED_ROLES.has(result.admitted.role)
941
+ && result.terminal?.roleOutcome.kind === "accepted") {
942
+ return auditSubmittedRole(result, env, io);
943
+ }
944
+ for (const value of held) io.stdout(value);
945
+ return result;
946
+ };
947
+ let binding: Awaited<ReturnType<typeof readAuditorResumeBinding>>;
948
+ try {
949
+ const loaded = await loadResumablePublicRole(env.home, request.runId, env.principalAuthority, true);
950
+ binding = loaded.admitted.role === "auditor"
951
+ ? await readAuditorResumeBinding(loaded.admitted.runDirectory)
952
+ : undefined;
953
+ } catch (error) {
954
+ if (!(error instanceof CliUsageError)) throw error;
955
+ }
956
+ if (binding !== undefined) {
957
+ return withAuditorSoulEnv({
958
+ subject: binding.subject,
959
+ sourceRunDirectory: binding.sourceRunDirectory,
960
+ run: resume,
961
+ });
962
+ }
963
+ return resume();
808
964
  }
809
965
 
810
- /** Continue the parent once any escalated child has submitted. */
966
+ const QUEUE_CONCLUSIONS = new Set(["converged", "continue", "escalate"]);
967
+ const GATE_CHILD_ROLES = new Set(["notary", "auditor", "inspector", "countersign"]);
968
+
969
+ /**
970
+ * A gate child's conclusion outside the three states goes back to that child.
971
+ * The words are the existing officer re-ask. A host failure stops here.
972
+ */
973
+ async function queueConclusionFromChild(
974
+ child: AdmittedRoleInvocation,
975
+ env: InstructionSeatRunEnv,
976
+ io: CliIo,
977
+ ): Promise<
978
+ | { readonly admitted: AdmittedRoleInvocation; readonly terminal: TerminalResult; readonly status: string }
979
+ | { readonly stop: SeatRunResult }
980
+ | undefined
981
+ > {
982
+ if (!GATE_CHILD_ROLES.has(child.role)) return undefined;
983
+ let current = child;
984
+ for (;;) {
985
+ const terminal = await trySettlePublicSeat(
986
+ current,
987
+ env.principalAuthority,
988
+ undefined,
989
+ );
990
+ const status = latestQueueStatus(terminal);
991
+ if (terminal !== undefined && status !== undefined && QUEUE_CONCLUSIONS.has(status)) {
992
+ return { admitted: current, terminal, status };
993
+ }
994
+ const reasked = await runPublicInstructionSeatResume({
995
+ runId: current.runId,
996
+ message: officerConclusionReask(status),
997
+ }, env, io);
998
+ if (reasked.exitCode !== 0 || reasked.admitted === undefined || reasked.terminal === undefined) {
999
+ return { stop: reasked };
1000
+ }
1001
+ const reaskedStatus = latestQueueStatus(reasked.terminal);
1002
+ if (reaskedStatus !== undefined && QUEUE_CONCLUSIONS.has(reaskedStatus)) {
1003
+ return { admitted: reasked.admitted, terminal: reasked.terminal, status: reaskedStatus };
1004
+ }
1005
+ if (latestPayloadEscalated(reasked.terminal.roleOutcome)) return { stop: reasked };
1006
+ current = reasked.admitted;
1007
+ }
1008
+ }
1009
+
1010
+ /** Continue the parent once a child has submitted. The child words ride the existing resume. */
811
1011
  export async function continueParentAfterChild(
812
1012
  parentRunId: string,
813
1013
  child: AdmittedRoleInvocation,
@@ -815,18 +1015,235 @@ export async function continueParentAfterChild(
815
1015
  io: CliIo,
816
1016
  ): Promise<SeatRunResult> {
817
1017
  const loaded = await loadResumablePublicRole(env.home, parentRunId, env.principalAuthority, true);
818
- if (loaded.run.state !== "admitted") {
1018
+ if (loaded.run.state !== "admitted" && !AUDITED_ROLES.has(loaded.admitted.role)) {
819
1019
  return runPublicInstructionSeatResume({ runId: parentRunId }, env, io);
820
1020
  }
821
1021
  const admitted = loaded.admitted;
822
- if (admitted.role === "countersign" && admitted.ticketNumber !== undefined) {
1022
+ // Court diarist refresh advances only after a diarist child; gate officers
1023
+ // (notary/auditor/…) resume straight into queueConclusionFromChild (#1057).
1024
+ if (admitted.role === "countersign" && admitted.ticketNumber !== undefined
1025
+ && child.role === "diarist") {
823
1026
  const refresh = await runCountersignCourtDiaristStation(admitted, env, io, child.ticketNumber);
824
1027
  if (refresh !== undefined) {
825
1028
  io.stdout(formatTerminalResult(refresh.terminal));
826
1029
  return { exitCode: 0, admitted, terminal: refresh.terminal };
827
1030
  }
828
1031
  }
1032
+ const resolved = await queueConclusionFromChild(child, env, io);
1033
+ if (resolved !== undefined && "stop" in resolved) return resolved.stop;
1034
+ if (resolved !== undefined && resolved.status === "escalate") {
1035
+ return { exitCode: 0, admitted: resolved.admitted, terminal: resolved.terminal };
1036
+ }
1037
+ if (resolved !== undefined && (resolved.status === "continue" || resolved.status === "converged")) {
1038
+ if (AUDITED_ROLES.has(admitted.role) && resolved.status === "converged") {
1039
+ const terminal = await trySettlePublicSeat(admitted, env.principalAuthority, undefined);
1040
+ if (terminal?.roleOutcome.kind !== "accepted") throw new Error("pending submission is not recorded");
1041
+ return auditSubmittedRole({ exitCode: 0, admitted, terminal }, env, io, resolved.admitted.role, latestQueuePayload(resolved.terminal), resolved.admitted.runId);
1042
+ }
1043
+ return runPublicInstructionSeatResume({
1044
+ runId: parentRunId,
1045
+ message: readableGateItem(latestQueuePayload(resolved.terminal)),
1046
+ }, { ...env, autoResumeLimit: 0 }, io);
1047
+ }
829
1048
  const result = await dispatchAdmitted(admitted, env, { ...io, omitFailureStderrDiagnostic: true });
830
1049
  showResumeErrorPointer(io, result.exitCode, result.terminal);
831
1050
  return result;
832
1051
  }
1052
+
1053
+ /** Finished submissions enter the existing audit gate after their tool call has returned. */
1054
+ async function auditSubmittedRole(
1055
+ turn: SeatRunResult,
1056
+ env: InstructionSeatRunEnv,
1057
+ io: CliIo,
1058
+ passedOfficer?: PackagedRole,
1059
+ passedReceipt?: unknown,
1060
+ passedRunId?: string,
1061
+ ): Promise<SeatRunResult> {
1062
+ const admitted = turn.admitted;
1063
+ if (admitted === undefined || !AUDITED_ROLES.has(admitted.role)
1064
+ || turn.terminal?.roleOutcome.kind !== "accepted") return turn;
1065
+ const accepted = turn.terminal.roleOutcome.payloads?.at(-1);
1066
+ if (accepted === undefined) throw new Error("accepted submission has no payload");
1067
+ const record = accepted !== null && typeof accepted === "object" && !Array.isArray(accepted)
1068
+ ? accepted as Record<string, unknown> : undefined;
1069
+ const status = admitted.role === "secretariat" ? record?.secretariatStatus : record?.status;
1070
+ if (admitted.role === "secretariat" && status !== "converged" && status !== "escalate") {
1071
+ return runPublicInstructionSeatResume({ runId: admitted.runId, message: SECRETARIAT_STATUS_REASK },
1072
+ { ...env, autoResumeLimit: 0 }, io);
1073
+ }
1074
+ if (admitted.role === "countersign" && (typeof status !== "string" || !QUEUE_CONCLUSIONS.has(status))) {
1075
+ return runPublicInstructionSeatResume({ runId: admitted.runId, message: COUNTERSIGN_STATUS_REASK },
1076
+ { ...env, autoResumeLimit: 0 }, io);
1077
+ }
1078
+ // Self-escalation is a valid open routing state for these seats only.
1079
+ // Doctor's declared domain is completed|refused — an open "escalate" must
1080
+ // not skip its mandatory auditor (#1057 / PR #1075).
1081
+ const selfEscalationSkipsAudit = status === "escalate"
1082
+ && (admitted.role === "judge" || admitted.role === "secretariat"
1083
+ || admitted.role === "countersign");
1084
+ const skipAudit = selfEscalationSkipsAudit
1085
+ || ((admitted.role === "fixer" || admitted.role === "coder")
1086
+ && (typeof status !== "string" || !WORKER_DONE_STATUSES.has(status)));
1087
+ if (skipAudit) {
1088
+ io.stdout(formatTerminalResult(turn.terminal));
1089
+ return turn;
1090
+ }
1091
+ const rows = await readRecordedSubmissionRows(admitted.projectRoot, admitted.runId, env.home);
1092
+ const acceptedRow = [...rows].reverse().find((row) => row.role === admitted.role && row.kind === "accepted");
1093
+ const toolCallId = acceptedRow?.toolCallId;
1094
+ if (toolCallId === undefined) throw new Error("accepted submission has no tool call identity");
1095
+ const sessionFile = env.principalAuthority.decode(admitted.principal).sessionFile;
1096
+ const entries = admitted.role === "doctor" ? await readBoundSessionEntries(sessionFile) : [];
1097
+ const context: HostContext = {
1098
+ cwd: admitted.projectRoot,
1099
+ mode: "print",
1100
+ model: undefined,
1101
+ runDirectory: admitted.runDirectory,
1102
+ sessionManager: {
1103
+ getSessionFile: () => sessionFile,
1104
+ getSessionDir: () => dirname(sessionFile),
1105
+ getEntries: () => entries as unknown as Iterable<{ type: string }>,
1106
+ getLeafEntry: () => undefined,
1107
+ getLeafId: () => admitted.runId,
1108
+ getHeader: () => ({ type: "session", id: admitted.runId }),
1109
+ },
1110
+ abort() {},
1111
+ };
1112
+ if (admitted.role === "doctor") {
1113
+ if (passedOfficer !== "auditor") {
1114
+ let lastSummon: PublicSummonResult | undefined;
1115
+ const decision = await createPiDoctorAuditor()({
1116
+ context, submission: accepted,
1117
+ ...(env.signal === undefined ? {} : { signal: env.signal }),
1118
+ summonAuditor: async (subject, sourceRunDirectory, signal, reask, submission) => {
1119
+ const summoned = await summonPublicRole({
1120
+ role: "auditor",
1121
+ argv: ["--subject", subject, "--source-run", sourceRunDirectory,
1122
+ `${AUDITOR_DOSSIER_PROMPT}${sourceRunDirectory}`],
1123
+ cwd: admitted.projectRoot, home: env.home, packageRoot: env.packageRoot,
1124
+ principalAuthority: env.principalAuthority,
1125
+ correlationId: admitted.runId,
1126
+ ...(env.credentials === undefined ? {} : { credentials: env.credentials }),
1127
+ ...(env.hostAdapters === undefined ? { roleTurnHost: env.roleTurnHost } : { hostAdapters: env.hostAdapters }),
1128
+ ...(signal === undefined ? {} : { signal }),
1129
+ ...(reask === undefined ? {} : { reviewReask: reask }),
1130
+ ...(submission === undefined ? {} : { gateReviewInstruction: submission }),
1131
+ });
1132
+ const officerSession = sessionFileFromPublicSummon(summoned);
1133
+ if (officerSession !== undefined) {
1134
+ bookDirectOfficerRunPointer({
1135
+ parentSessionFile: sessionFile, officer: "auditor", sessionFile: officerSession,
1136
+ ...(summoned.runDirectory === undefined ? {} : { runDirectory: summoned.runDirectory }),
1137
+ });
1138
+ }
1139
+ lastSummon = summoned;
1140
+ return summoned;
1141
+ },
1142
+ });
1143
+ if (decision.status === "continue") {
1144
+ return runPublicInstructionSeatResume({
1145
+ runId: admitted.runId, message: readableGateItem(decision.receipt ?? decision.violations),
1146
+ }, { ...env, autoResumeLimit: 0 }, io);
1147
+ }
1148
+ if (decision.status !== "converged") {
1149
+ if (lastSummon?.terminal !== undefined) {
1150
+ io.stdout(formatTerminalResult(lastSummon.terminal));
1151
+ return { exitCode: lastSummon.exitCode,
1152
+ ...(lastSummon.admitted === undefined ? {} : { admitted: lastSummon.admitted }),
1153
+ terminal: lastSummon.terminal };
1154
+ }
1155
+ throw new Error(`doctor audit did not converge: ${decision.status}`);
1156
+ }
1157
+ }
1158
+ } else {
1159
+ const summonOfficer = createDefaultGateOfficerSummon({
1160
+ cwd: admitted.projectRoot,
1161
+ home: env.home,
1162
+ packageRoot: env.packageRoot,
1163
+ roleTurnHost: env.roleTurnHost,
1164
+ ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
1165
+ });
1166
+ let chain: Awaited<ReturnType<typeof runJudgeGates>>;
1167
+ let officerRunId = passedRunId;
1168
+ const runGate = (subject: Parameters<typeof requireSubmissionGate>[0]["subject"]) =>
1169
+ requireSubmissionGate({
1170
+ context,
1171
+ subject,
1172
+ toolCallId,
1173
+ submission: accepted,
1174
+ summonOfficer,
1175
+ ...(env.signal === undefined ? {} : { signal: env.signal }),
1176
+ hostActions: {
1177
+ failInfrastructure(error): never { throw error; },
1178
+ bindSubmissionNonPass() {},
1179
+ },
1180
+ });
1181
+ if (admitted.role === "judge") {
1182
+ chain = await runJudgeGates({
1183
+ gateAlreadyConverged: async (subject) => passedOfficer === "auditor"
1184
+ || (passedOfficer === "notary" && subject.kind === "judge_draft"),
1185
+ runGate,
1186
+ });
1187
+ } else {
1188
+ const subject = admitted.role === "fixer" || admitted.role === "coder"
1189
+ ? { kind: "worker_completion" as const }
1190
+ : admitted.role === "secretariat"
1191
+ ? { kind: "secretariat_verdict" as const }
1192
+ : { kind: "countersign_verdict" as const };
1193
+ const officer = gateOfficerForSubject(subject);
1194
+ if (passedOfficer === officer) {
1195
+ chain = { status: "converged", passes: [] };
1196
+ } else {
1197
+ const pass = await runGate(subject);
1198
+ if (pass === undefined) throw new Error("audit gate returned no conclusion");
1199
+ officerRunId = pass.runId;
1200
+ chain = {
1201
+ status: pass.status,
1202
+ passes: [{
1203
+ subject,
1204
+ status: pass.status,
1205
+ receipt: pass.receipt,
1206
+ ...(pass.runId === undefined ? {} : { runId: pass.runId }),
1207
+ ...(pass.runDirectory === undefined ? {} : { runDirectory: pass.runDirectory }),
1208
+ }],
1209
+ };
1210
+ }
1211
+ }
1212
+ if (chain.status === "escalate") {
1213
+ const escalation = chain.passes.at(-1);
1214
+ const escalatedRunId = escalation?.runId
1215
+ ?? (escalation?.runDirectory === undefined
1216
+ ? undefined : runIdFromRunDirectory(escalation.runDirectory));
1217
+ if (escalatedRunId === undefined) throw new Error("escalated audit has no resumable run identity");
1218
+ const officer = await loadResumablePublicRole(env.home, escalatedRunId, env.principalAuthority, true);
1219
+ const terminal = await trySettlePublicSeat(officer.admitted, env.principalAuthority, undefined);
1220
+ if (terminal === undefined) throw new Error("escalated audit has no terminal result");
1221
+ io.stdout(formatTerminalResult(terminal));
1222
+ return { exitCode: 0, admitted: officer.admitted, terminal };
1223
+ }
1224
+ if (chain.status === "continue") {
1225
+ return runPublicInstructionSeatResume({
1226
+ runId: admitted.runId,
1227
+ message: readableGateItem(chain.passes.at(-1)?.receipt),
1228
+ }, { ...env, autoResumeLimit: 0 }, io);
1229
+ }
1230
+ if (admitted.role === "secretariat") {
1231
+ const pass = chain.passes.at(-1);
1232
+ const receipt = pass?.receipt ?? passedReceipt;
1233
+ if (receipt !== undefined) {
1234
+ await env.sessionAppender(env.principalAuthority, admitted.principal, SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, {
1235
+ officer: "countersign", receipt,
1236
+ ...(officerRunId === undefined ? {} : { runId: officerRunId }),
1237
+ });
1238
+ }
1239
+ }
1240
+ }
1241
+ // The turn already settled this court (payloads, usage, autoResumeCount, history).
1242
+ // Audit only adds gate rounds and, for 中书省, the officer fact. A second
1243
+ // settle would republish the same attempt.
1244
+ if (turn.terminal === undefined) throw new Error(`audited ${admitted.role} submission did not settle`);
1245
+ const terminal = await attachPostAuditProjection(admitted, env.principalAuthority, turn.terminal);
1246
+ turn = { ...turn, terminal };
1247
+ io.stdout(formatTerminalResult(terminal));
1248
+ return turn;
1249
+ }
@@ -519,6 +519,7 @@ async function mergeInvocationIdentityPage(
519
519
  export async function persistAdmittedSourceRunPath(
520
520
  admitted: AdmittedRoleInvocation,
521
521
  sourceRunPath: string,
522
+ auditorSubject?: "judge" | "doctor",
522
523
  ): Promise<void> {
523
524
  if (sourceRunPath.trim() === "") {
524
525
  throw new Error("persistAdmittedSourceRunPath requires a non-empty sourceRunPath");
@@ -528,15 +529,20 @@ export async function persistAdmittedSourceRunPath(
528
529
  string,
529
530
  unknown
530
531
  >;
531
- if (typeof current.sourceRunPath === "string") {
532
- if (current.sourceRunPath === sourceRunPath) return;
532
+ if (typeof current.sourceRunPath === "string" && current.sourceRunPath !== sourceRunPath) {
533
533
  throw new Error(
534
534
  `persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`,
535
535
  );
536
536
  }
537
+ const sameSubject = auditorSubject === undefined || current.auditorSubject === auditorSubject;
538
+ if (current.sourceRunPath === sourceRunPath && sameSubject) return;
537
539
  await writeFile(
538
540
  admittedPath,
539
- `${JSON.stringify({ ...current, sourceRunPath }, null, 2)}\n`,
541
+ `${JSON.stringify({
542
+ ...current,
543
+ sourceRunPath,
544
+ ...(auditorSubject === undefined ? {} : { auditorSubject }),
545
+ }, null, 2)}\n`,
540
546
  "utf8",
541
547
  );
542
548
  }
@@ -1084,7 +1084,6 @@ export async function dispatchPostAdmissionTurn<
1084
1084
  directHostFailureSignal
1085
1085
  || (result.code !== null && result.code !== 0)
1086
1086
  || resolution.knownFailure !== undefined;
1087
-
1088
1087
  settled = await adapters.trySettle(admitted, env.principalAuthority, courtScope);
1089
1088
  if (settled !== undefined) {
1090
1089
  settled = await attachRecordedSubmissions(admitted, settled, courtScope) as T;