@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
@@ -174,16 +174,18 @@ function roleOutcomeFromRows(
174
174
  (row) => row.kind === "accepted" || row.kind === "audit-escalation",
175
175
  );
176
176
  if (terminal.length === 0) return undefined;
177
- // Role-result block is the full recorded sequence for this seat, not a sole pick.
177
+ // Full sequence stays on payloads. The queue kind follows the latest terminal
178
+ // row only — an earlier escalation must not outrank a later acceptance or
179
+ // keep that earlier receipt (#1057).
178
180
  const payloads = mine.map((row) => row.accepted);
179
- if (terminal.some((row) => row.kind === "audit-escalation")) {
180
- const audited = terminal.slice().reverse().find((row) => row.kind === "audit-escalation" && Object.hasOwn(row, "auditReceipt"));
181
+ const latest = terminal[terminal.length - 1]!;
182
+ if (latest.kind === "audit-escalation") {
181
183
  return {
182
184
  kind: "audit_escalation", role, status: "audit_escalation", payloads,
183
- ...(audited === undefined ? {} : { decisiveFacts: {
184
- auditEscalationReceipt: audited.auditReceipt,
185
- ...(Object.hasOwn(audited, "auditOfficer") ? { auditEscalationOfficer: audited.auditOfficer } : {}),
186
- } }),
185
+ ...(Object.hasOwn(latest, "auditReceipt") ? { decisiveFacts: {
186
+ auditEscalationReceipt: latest.auditReceipt,
187
+ ...(Object.hasOwn(latest, "auditOfficer") ? { auditEscalationOfficer: latest.auditOfficer } : {}),
188
+ } } : {}),
187
189
  };
188
190
  }
189
191
  return { kind: "accepted", role, payloads };
@@ -914,7 +916,7 @@ function sessionReadFailure(
914
916
  * Read the exact bound Pi session file principal.
915
917
  * Does not scan the session directory for "latest" — resume identity is the file.
916
918
  */
917
- async function readBoundSessionEntries(
919
+ export async function readBoundSessionEntries(
918
920
  sessionFile: string,
919
921
  ): Promise<SessionEntry[]> {
920
922
  const text = await readFile(sessionFile, "utf8");
@@ -1847,6 +1849,8 @@ function snapshotAuditDetails(details: Record<string, unknown>): Record<string,
1847
1849
  * terminal identity; the retained response must be this seat's real escalate
1848
1850
  * decision and its projected audit-owned fields must agree with it.
1849
1851
  */
1852
+ // Historical pre-#1057 session artifacts may still contain this old projection;
1853
+ // no current submission path writes it, but resume/settlement must read it.
1850
1854
  function boundAuditEscalationForResult(
1851
1855
  entries: readonly SessionEntry[],
1852
1856
  resultIndex: number,
@@ -3089,6 +3093,26 @@ async function applySecretariatCountersignTerminal(
3089
3093
  return settled;
3090
3094
  }
3091
3095
 
3096
+ /**
3097
+ * After the audit gate returns, attach gate rounds and the secretariat officer
3098
+ * fact onto the terminal this turn already settled. Does not publish again —
3099
+ * a second publish would append another attempt-history row for the same attempt.
3100
+ */
3101
+ export async function attachPostAuditProjection(
3102
+ admitted: AdmittedRoleInvocation,
3103
+ authority: DurablePrincipalAuthority,
3104
+ terminal: TerminalResult,
3105
+ ): Promise<TerminalResult> {
3106
+ const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
3107
+ const gate = await extractGateFactFromSessionDirectory(sessionDirectory, {
3108
+ runDirectory: admitted.runDirectory,
3109
+ parentSessionFile: sessionFile,
3110
+ });
3111
+ const withGate = gate === undefined ? terminal : { ...terminal, gate };
3112
+ if (admitted.role !== "secretariat") return withGate;
3113
+ return applySecretariatCountersignTerminal(admitted, authority, withGate);
3114
+ }
3115
+
3092
3116
  /** One failed attempt to place a durable failure artifact (path is private layout). */
3093
3117
  type PublicationAttempt = {
3094
3118
  readonly path: string;
@@ -535,8 +535,8 @@ export async function summonPublicRole(
535
535
  }
536
536
 
537
537
  const stderr = captured?.stderrText();
538
- // A parked officer can return its court diarist's escalation verbatim. The
539
- // terminal's independent run, not the parked officer, is the gate pointer.
538
+ // An escalated officer can return its court diarist's escalation verbatim.
539
+ // The terminal's independent run, not the officer, is the gate pointer.
540
540
  const runDirectory = result.terminal !== undefined
541
541
  && result.terminal.roleOutcome.role !== options.role
542
542
  && typeof result.terminal.runId === "string"
@@ -805,6 +805,8 @@ export async function summonGateOfficer(options: {
805
805
  const { readableGateItem } = await import("./readable-gate-item.ts");
806
806
  gateReviewInstruction = readableGateItem(options.submission);
807
807
  }
808
+ const { runIdFromRunDirectory } = await import("./run-terminal-artifacts.ts");
809
+ const parentRunId = runIdFromRunDirectory(options.sourceRunDirectory);
808
810
  const common = {
809
811
  cwd: options.cwd,
810
812
  ...(home === undefined ? {} : { home }),
@@ -818,6 +820,9 @@ export async function summonGateOfficer(options: {
818
820
  ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
819
821
  ...(options.hostAdapters === undefined ? {} : { hostAdapters: options.hostAdapters }),
820
822
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
823
+ ...(parentRunId === undefined || parentRunId.trim() === ""
824
+ ? {}
825
+ : { correlationId: parentRunId }),
821
826
  } as const;
822
827
  const { packagedGateSummon } = await import("./packaged-role-registry.ts");
823
828
  const gateSummon = packagedGateSummon(options.officer);
@@ -851,15 +856,13 @@ export async function summonGateOfficer(options: {
851
856
  // no code-authored summons prose (#924). Gate resume key = parentRunPath
852
857
  // (sourceRunDirectory); board ticket handoff is bind-only, never a resume
853
858
  // lookup (#987 Result 7). Receipt ticketNumber stays payload content.
854
- // correlationId only on this officer — notary/auditor/inspector stay untouched.
855
859
  // Gate resume key is the parent run directory. The board ticket only binds.
856
- const { runIdFromRunDirectory } = await import("./run-terminal-artifacts.ts");
857
- const correlationId = runIdFromRunDirectory(options.sourceRunDirectory);
858
- if (correlationId === undefined || correlationId.trim() === "") {
860
+ if (parentRunId === undefined || parentRunId.trim() === "") {
859
861
  throw new Error(
860
862
  `countersign gate summon requires parent runId from sourceRunDirectory: ${options.sourceRunDirectory}`,
861
863
  );
862
864
  }
865
+ const correlationId = parentRunId;
863
866
  const { isSafePositiveTicketNumber, readBoardTicketNumber } = await import("./run-ticket-number.ts");
864
867
  const parentTicket = await readBoardTicketNumber(options.sourceRunDirectory);
865
868
  const submittedTicket = options.submission !== null && typeof options.submission === "object" && !Array.isArray(options.submission)
@@ -9,10 +9,6 @@ import {
9
9
  ENGINE_MODEL_FLAG_NAME,
10
10
  normalizeEngineName,
11
11
  } from "./engine-detour.ts";
12
- import {
13
- createDefaultGateOfficerSummon,
14
- requireSubmissionGate,
15
- } from "./submission-gate.ts";
16
12
  import type {
17
13
  HostContext,
18
14
  HostEventRegistration,
@@ -228,34 +224,6 @@ export async function prepareRoleEnvelope(options: {
228
224
  // seat remains reachable through MCP.
229
225
  setActiveTools(names) { preferredTools = [...names]; },
230
226
  getActiveTools() { return [...preferredTools]; },
231
- async requireSubmissionGate(gateOptions) {
232
- const packageRoot =
233
- typeof options.dependencies.packageRoot === "string"
234
- ? options.dependencies.packageRoot
235
- : undefined;
236
- return requireSubmissionGate({
237
- context: gateOptions.context,
238
- subject: gateOptions.subject,
239
- ...(gateOptions.signal === undefined ? {} : { signal: gateOptions.signal }),
240
- hostActions: {
241
- failInfrastructure: (error, _context, toolCallId) =>
242
- gateOptions.hostActions.failInfrastructure(error, gateOptions.context, toolCallId),
243
- bindSubmissionNonPass: gateOptions.hostActions.bindSubmissionNonPass,
244
- },
245
- toolCallId: gateOptions.toolCallId,
246
- ...(gateOptions.submission === undefined ? {} : { submission: gateOptions.submission }),
247
- // #969: package root + home reach nested 给事中/符宝郎 summons.
248
- // hostAdapters (when set on deps) forward nested seat selection — production unset.
249
- summonOfficer: createDefaultGateOfficerSummon({
250
- cwd: gateOptions.context.cwd ?? request.cwd,
251
- home: request.home,
252
- ...(packageRoot === undefined ? {} : { packageRoot }),
253
- ...(options.dependencies.hostAdapters === undefined
254
- ? {}
255
- : { hostAdapters: options.dependencies.hostAdapters }),
256
- }),
257
- });
258
- },
259
227
  on(...registration: HostEventRegistration) {
260
228
  const [event, handler] = registration;
261
229
  const list = handlers.get(event) ?? [];
@@ -2,7 +2,6 @@ import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { createGhCollectorGitHubTransport } from "./collector-github.ts";
5
- import { createPiDoctorAuditor } from "./doctor-auditor.ts";
6
5
  import { loadDoctorCase } from "./doctor-evidence.ts";
7
6
  import { createNativeNavigatorSessionFactory, createNavigatorAttendance } from "./navigator-attendance.ts";
8
7
  import { loadNavigatorWorkContext } from "./navigator-work-context.ts";
@@ -43,7 +42,6 @@ export function createRoleRuntimeDependencies(packageRoot: string): RoleRuntimeD
43
42
  // import.meta.url — headless/acp production-host bundles live under dist/*/
44
43
  // and would otherwise look for dist/resources/ (#962).
45
44
  const collectorHandbookSeedPath = join(packageRoot, "resources/collector-bot-handbook.md");
46
- const doctorAuditor = createPiDoctorAuditor();
47
45
  const navigatorSessionFactory = createNativeNavigatorSessionFactory();
48
46
  return {
49
47
  packageRoot,
@@ -56,8 +54,6 @@ export function createRoleRuntimeDependencies(packageRoot: string): RoleRuntimeD
56
54
  loadDoctorCase,
57
55
  loadNotarySourceRun: loadNotarySourceRunLocator,
58
56
  loadMergerInput: async (path) => JSON.parse(await readFile(path, "utf8")),
59
- // #590: doctor compliance still on disposeCompliance path; judge→auditor is gate queue (#756).
60
- auditDoctorCompliance: (options) => doctorAuditor(options),
61
57
  loadNavigatorWorkContext: (options) => loadNavigatorWorkContext({
62
58
  context: options.context,
63
59
  role: options.role,
@@ -1,7 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFileSync, writeSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { readableGateItem } from "./readable-gate-item.ts";
5
4
  import {
6
5
  ExplicitInternalActivationError,
7
6
 
@@ -110,8 +109,7 @@ import {
110
109
  } from "./navigator-invocation-identity.ts";
111
110
  import { recordTypedProviderHttpStatus } from "./typed-provider-http.ts";
112
111
  import { NAVIGATOR_POST_ROLE_GRACE_MS, raceNavigatorGrace } from "./public-cli/settlement.ts";
113
- import { PACKAGED_ROLE_REGISTRY, isOfficerReviewSeat, packagedRoleActivationFlags, packagedRoleMetadata, packagedRoleOutputTool, packagedRolePhaseFlag, type PackagedRole } from "./packaged-role-registry.ts";
114
- import { isAuditEscalationProjection } from "./audit-escalation.ts";
112
+ import { PACKAGED_ROLE_REGISTRY, isOfficerReviewSeat, packagedRoleActivationFlags, packagedRoleInputFlag, packagedRoleMetadata, packagedRoleOutputTool, packagedRolePhaseFlag, type PackagedRole } from "./packaged-role-registry.ts";
115
113
  import {
116
114
  createJudgeRoleRuntime,
117
115
  } from "./judge-role.ts";
@@ -346,8 +344,6 @@ export { fixerOutputSchema, validateFixerOutput } from "./package-contracts/fixe
346
344
  export type { FixerBlocker, FixerClassResult, FixerPhase, FixerTestEvidence } from "./package-contracts/fixer-output.ts";
347
345
  export { fixerPrerequisiteSchema, fixerPrerequisitesSchema, parseFixerPrerequisites, validateFixerPrerequisites } from "./package-contracts/fixer-packet.ts";
348
346
  export type { FixerInvocationInput, FixerPrerequisite } from "./package-contracts/fixer-packet.ts";
349
- export { AUDIT_ESCALATION_KIND, buildAuditEscalationResult, disposeComplianceDecision, isAuditEscalationResult, projectAuditEscalation } from "./audit-escalation.ts";
350
- export type { AuditEscalationResult, AuditEscalationToolResult, ComplianceDecisionHandlers } from "./audit-escalation.ts";
351
347
  export {
352
348
  AUDITOR_SOUL_ROLES,
353
349
  AK_ROLE_AUDITOR_SUBJECT_ENV,
@@ -460,8 +456,8 @@ export type RoleRuntimeDependencies = {
460
456
  /**
461
457
  * Composition-root adapter table for nested gate officer summons (#969).
462
458
  * Production leaves unset (default pi + packaged externals). Tests inject
463
- * faux nested hosts so prepareRoleEnvelope.requireSubmissionGate is bitten
464
- * without reimplementing the envelope summon closure.
459
+ * faux nested hosts so the public entry's reviewer summons use the same
460
+ * host resolution as production.
465
461
  */
466
462
  hostAdapters?: readonly import("./public-cli/role-turn-host-resolution.ts").NamedRoleTurnHostAdapter[];
467
463
  /** Non-identity opening materials delivered in the ordinary role brief. */
@@ -476,7 +472,6 @@ export type RoleRuntimeDependencies = {
476
472
  loadNotarySourceRun?(path: string): Promise<import("./notary-contracts.ts").NotarySourceRunLocator>;
477
473
  loadDoctorCase?(path: string): Promise<import("./doctor-contracts.ts").DoctorCase>;
478
474
  loadMergerInput?(path: string): Promise<unknown>;
479
- auditDoctorCompliance?(options: { context: HostContext; signal?: AbortSignal; submission: unknown }): Promise<ComplianceDecision>;
480
475
  createCollectorClock?(): CollectorClock;
481
476
  createNavigatorAttendance?(options: { context: HostContext; role: string; phase: NavigatorPhase; subjectKey: string; subject: string; authority: string; contextError?: unknown; invocationId: string; onEvent: (event: import("./navigator-attendance.ts").NavigatorEvent, report: import("./navigator-attendance.ts").NavigatorReport) => void | Promise<void> }): NavigatorAttendanceDependency | Promise<NavigatorAttendanceDependency>;
482
477
  loadNavigatorWorkContext?(options: { context: HostContext; role: string; phase: NavigatorPhase; getFlag?: (name: string) => unknown }): Promise<NavigatorWorkContext>;
@@ -550,11 +545,6 @@ export function publicNavigatorSettlement(role: string, phase: NavigatorPhase, e
550
545
  const details = typeof event.details === "object" && event.details !== null && !Array.isArray(event.details)
551
546
  ? event.details as Record<string, unknown>
552
547
  : {};
553
- // Live Navigator consumes only the audit-owned projection; persisted/replayed
554
- // records are re-authenticated by settlement against retained audit evidence.
555
- if (isAuditEscalationProjection(event.details)) {
556
- return { kind: "human_decision", role, phase, status: "audit_escalation" };
557
- }
558
548
  const status = typeof details.status === "string"
559
549
  ? details.status
560
550
  : receiptStatusFromRegistry(details);
@@ -597,7 +587,7 @@ type FiledOfficerBeforeAccept = (input: {
597
587
  * Shared registration envelope for filed officers (ADR 0018 / #572):
598
588
  * activate, tool register, before_agent_start prompt, inventory check.
599
589
  * Role module keeps label/soul/spec shape only; sole-final barrier is ledger-owned.
600
- * Optional beforeAccept is the sole extension seam (e.g. countersign Notary gate).
590
+ * Optional beforeAccept projects local submission facts before the tool returns.
601
591
  */
602
592
  function createFiledOfficerRuntime(
603
593
  roleHost: RoleHost,
@@ -606,7 +596,6 @@ function createFiledOfficerRuntime(
606
596
  tool: { name: string; label: string; description: string; promptSnippet: string; parameters: unknown };
607
597
  soulTag: string;
608
598
  beforeAccept?: FiledOfficerBeforeAccept;
609
- relayGatePass?: true;
610
599
  },
611
600
  dependencies: { loadSoul(): Promise<string> },
612
601
  ) {
@@ -631,25 +620,11 @@ function createFiledOfficerRuntime(
631
620
  spec.beforeAccept === undefined
632
621
  ? undefined
633
622
  : await spec.beforeAccept({ toolCallId, parameters, signal, ctx });
634
- if (isAuditEscalationProjection(projected)) {
635
- return { content: [], details: projected, terminate: true as const };
636
- }
637
- if (projected !== null && typeof projected === "object" && "status" in projected
638
- && projected.status === "continue" && "receipt" in projected) {
639
- return {
640
- content: [{ type: "text" as const, text: readableGateItem(projected.receipt) }],
641
- details: projected,
642
- terminate: false,
643
- };
644
- }
645
- const pass = spec.relayGatePass === true && projected !== null && typeof projected === "object" && "officer" in projected && "receipt" in projected
646
- ? projected
647
- : undefined;
648
623
  // Accept-as-is + terminate only. Shape is not an admission gate
649
624
  // (第 0 条 / ADR 0055); sole-final barrier is ledger-owned (#575).
650
625
  return {
651
- content: pass === undefined || pass.receipt === undefined ? [] : [{ type: "text" as const, text: readableGateItem(pass.receipt) }],
652
- details: pass === undefined ? (projected === undefined ? parameters : projected) : parameters,
626
+ content: [],
627
+ details: projected === undefined ? parameters : projected,
653
628
  terminate: true as const,
654
629
  };
655
630
  },
@@ -887,8 +862,6 @@ function readDiaristRunCoordinates(ctx: HostContext): {
887
862
  /** Plain-language re-ask when diarist bounds cannot be used (#901 / reask-not-explode). */
888
863
  const DIARIST_BOUNDS_REASK =
889
864
  "边界无法使用。请重交 sessions:每卷 path + ranges,每端以原生 id 或本轮行号二选一指名。" as const;
890
- const DIARIST_ROUTING_STATUSES = new Set(["completed", "escalate"]);
891
-
892
865
  /**
893
866
  * #708 / #779 / #901: 起居郎 public seat on the shared filed-officer envelope.
894
867
  * LLM judges ticket + dialogue bounds; mechanical layer reprojects the unique
@@ -910,13 +883,11 @@ export function createDiaristRoleRuntime(
910
883
  parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
911
884
  ? (parameters as Record<string, unknown>)
912
885
  : undefined;
913
- // This status selects package-owned continuation vs escalation. Check it
914
- // before interpreting any other submitted field; caller-owned data stays open.
886
+ // Routing belongs to the public seam after this tool call has returned.
887
+ // Only completed receipts need local diary projection; other statuses
888
+ // pass through verbatim for that seam to route.
915
889
  const status = submitted?.status;
916
- if (typeof status !== "string" || !DIARIST_ROUTING_STATUSES.has(status)) {
917
- throw new ParentQueueReaskError(unreadableDiscriminatorNotice("status", submitted?.status));
918
- }
919
- if (status === "escalate") return parameters;
890
+ if (status !== "completed") return parameters;
920
891
  const assertion = readDiaristTicketAssertion(submitted);
921
892
  const coords = readDiaristRunCoordinates(ctx);
922
893
  // #836 7.3: pre-bound ticket is material for the LLM, not an override.
@@ -957,126 +928,17 @@ export function createDiaristRoleRuntime(
957
928
  );
958
929
  }
959
930
 
960
- /** Secretariat status words the submission gate reads. Not the review three-state field. */
961
- const SECRETARIAT_QUEUE_STATUSES = new Set(["converged", "escalate"]);
962
-
963
- /**
964
- * Secretariat output is terminal; converged submissions enter the shared
965
- * submission gate on every host that mounts gatekeeper actions.
966
- */
931
+ /** Secretariat files before the public post-submission audit starts. */
967
932
  export function createSecretariatRoleRuntime(
968
933
  roleHost: RoleHost,
969
934
  dependencies: SecretariatRuntimeDependencies,
970
- hostActions?: import("./host-contracts.ts").HostGatekeeperActions,
971
935
  ) {
972
- const beforeAccept: FiledOfficerBeforeAccept | undefined =
973
- hostActions !== undefined
974
- ? async ({ toolCallId, parameters, signal, ctx }) => {
975
- if (roleHost.requireSubmissionGate === undefined) {
976
- const error = new Error("host cannot mount the secretariat submission gate");
977
- error.name = "InfrastructureFailure";
978
- return hostActions.failInfrastructure(error, ctx, toolCallId);
979
- }
980
- const record =
981
- parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
982
- ? (parameters as Record<string, unknown>)
983
- : undefined;
984
- const status =
985
- record !== undefined && typeof record.secretariatStatus === "string"
986
- ? record.secretariatStatus
987
- : undefined;
988
- if (status === undefined || !SECRETARIAT_QUEUE_STATUSES.has(status)) {
989
- throw new ParentQueueReaskError(
990
- unreadableDiscriminatorNotice("secretariatStatus", record?.secretariatStatus),
991
- );
992
- }
993
- if (status === "escalate") {
994
- // Parent escalate → throw to caller as-is; countersign does not attend.
995
- return undefined;
996
- }
997
- try {
998
- const pass = await roleHost.requireSubmissionGate!({
999
- context: ctx,
1000
- subject: { kind: "secretariat_verdict" },
1001
- ...(signal === undefined ? {} : { signal }),
1002
- hostActions,
1003
- toolCallId,
1004
- // #879: this-turn typed payload — identity-bound at submit site.
1005
- submission: parameters,
1006
- });
1007
- // Book 给事中 署 snapshot for seat settlement projection (receipt + runId).
1008
- // Ledger accepted stays LLM params (#836); public terminal reads this entry.
1009
- if (
1010
- pass !== undefined
1011
- && pass !== null
1012
- && typeof pass === "object"
1013
- && pass.status === "converged"
1014
- && "receipt" in pass
1015
- ) {
1016
- const {
1017
- SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
1018
- } = await import("./secretariat-contracts.ts");
1019
- const runId =
1020
- typeof pass.runId === "string" && pass.runId.trim() !== ""
1021
- ? pass.runId
1022
- : undefined;
1023
- ctx.sessionManager.appendCustomEntry?.(SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, {
1024
- officer: "countersign",
1025
- receipt: pass.receipt,
1026
- ...(runId === undefined ? {} : { runId }),
1027
- });
1028
- }
1029
- return pass;
1030
- } catch (error) {
1031
- // 给事中上呈 ends parent with officer receipt (no retry / 不擅改).
1032
- if (
1033
- error instanceof GatekeeperDecisionError
1034
- && error.result.status === "escalate"
1035
- ) {
1036
- const receipt = error.result.receipt;
1037
- const receiptRecord =
1038
- receipt !== null && typeof receipt === "object" && !Array.isArray(receipt)
1039
- ? (receipt as Record<string, unknown>)
1040
- : undefined;
1041
- const runId =
1042
- typeof error.result.runId === "string" && error.result.runId.trim() !== ""
1043
- ? error.result.runId
1044
- : undefined;
1045
- // Envelope persists custom entries only
1046
- // (toolResult rows are memory-only on headless/ACP — #617/#959).
1047
- const {
1048
- SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
1049
- } = await import("./secretariat-contracts.ts");
1050
- ctx.sessionManager.appendCustomEntry?.(SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, {
1051
- officer: "countersign",
1052
- receipt,
1053
- ...(runId === undefined ? {} : { runId }),
1054
- });
1055
- const { buildAuditEscalationResult } = await import("./audit-escalation.ts");
1056
- return buildAuditEscalationResult(
1057
- {
1058
- status: "escalate",
1059
- officer: "countersign",
1060
- ...(receiptRecord !== undefined
1061
- && Object.prototype.hasOwnProperty.call(receiptRecord, "decisionGate")
1062
- ? { decisionGate: receiptRecord.decisionGate }
1063
- : {}),
1064
- },
1065
- receipt,
1066
- );
1067
- }
1068
- throw error;
1069
- }
1070
- }
1071
- : undefined;
1072
936
  const base = createFiledOfficerRuntime(
1073
937
  roleHost,
1074
938
  {
1075
939
  role: "secretariat",
1076
- relayGatePass: true,
1077
940
  tool: SECRETARIAT_OUTPUT_TOOL_SPEC,
1078
941
  soulTag: "secretariat",
1079
- ...(beforeAccept === undefined ? {} : { beforeAccept }),
1080
942
  },
1081
943
  dependencies,
1082
944
  );
@@ -1114,64 +976,13 @@ export function createSecretariatRoleRuntime(
1114
976
  export function createCountersignRoleRuntime(
1115
977
  roleHost: RoleHost,
1116
978
  dependencies: CountersignRuntimeDependencies,
1117
- hostActions?: import("./host-contracts.ts").HostGatekeeperActions,
1118
979
  ) {
1119
- // Notary inner gate difference only — lifecycle stays on the shared envelope.
1120
- // Pointer-only summons: officer self-fetches from run dossier (#632 / ADR 0079).
1121
- // #1028 queue: read shared status — escalate skips gate (thrown to caller);
1122
- // unreadable status returns to countersign; else notary inner gate.
1123
- const beforeAccept: FiledOfficerBeforeAccept | undefined =
1124
- hostActions !== undefined
1125
- ? async ({ toolCallId, parameters, signal, ctx }) => {
1126
- const record =
1127
- parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
1128
- ? (parameters as Record<string, unknown>)
1129
- : undefined;
1130
- const status =
1131
- record !== undefined && typeof record.status === "string"
1132
- ? record.status
1133
- : undefined;
1134
- if (status === undefined || !REVIEW_QUEUE_STATUSES.has(status)) {
1135
- // Parent status unreadable → back to countersign itself; do not summon notary,
1136
- // do not forge an officer bounce face (#753).
1137
- throw new ParentQueueReaskError(unreadableDiscriminatorNotice("status", record?.status));
1138
- }
1139
- if (status === "escalate") {
1140
- // Parent escalate → throw to caller as-is; notary does not attend (#753).
1141
- return undefined;
1142
- }
1143
- if (roleHost.requireSubmissionGate === undefined) {
1144
- const error = new Error("host cannot mount the countersign gate");
1145
- error.name = "InfrastructureFailure";
1146
- return hostActions.failInfrastructure(error, ctx, toolCallId);
1147
- }
1148
- try {
1149
- return await roleHost.requireSubmissionGate({
1150
- context: ctx,
1151
- subject: { kind: "countersign_verdict" },
1152
- ...(signal === undefined ? {} : { signal }),
1153
- hostActions,
1154
- toolCallId,
1155
- // #879: this-turn typed payload — identity-bound at submit site.
1156
- submission: parameters,
1157
- });
1158
- } catch (error) {
1159
- if (error instanceof GatekeeperDecisionError && error.result.status === "escalate") {
1160
- const { buildAuditEscalationResult } = await import("./audit-escalation.ts");
1161
- return buildAuditEscalationResult({ status: "escalate", officer: "notary", conflicts: error.result.receipt }, parameters);
1162
- }
1163
- throw error;
1164
- }
1165
- }
1166
- : undefined;
1167
980
  return createFiledOfficerRuntime(
1168
981
  roleHost,
1169
982
  {
1170
983
  role: "countersign",
1171
- relayGatePass: true,
1172
984
  tool: COUNTERSIGN_TOOL_SPEC,
1173
985
  soulTag: "countersign",
1174
- ...(beforeAccept === undefined ? {} : { beforeAccept }),
1175
986
  },
1176
987
  dependencies,
1177
988
  );
@@ -1265,7 +1076,6 @@ export function createRoleRuntimeExtension(
1265
1076
  const pendingInfrastructureFailures = new Map<string, PendingInfrastructureFailure>();
1266
1077
  // Envelope-owned execute→tool_result bridge for submission non-pass (ADR 0018 / #525).
1267
1078
  const pendingSubmissionNonPassByToolCallId = new Map<string, SubmissionGateNonPassResult>();
1268
- const pendingPriorGatePassByToolCallId = new Map<string, unknown>();
1269
1079
  let engineDetourRegistered = false;
1270
1080
  // #288 primary-session thin adapter. The policy is the sole budget owner;
1271
1081
  // terminating-tool rejections and mechanical delivery requests share two turns.
@@ -1483,11 +1293,6 @@ export function createRoleRuntimeExtension(
1483
1293
  return { readingMaterial: caseDossier };
1484
1294
  });
1485
1295
  roleHost.on("tool_result", async (event) => {
1486
- const priorGatePass = pendingPriorGatePassByToolCallId.get(event.toolCallId);
1487
- pendingPriorGatePassByToolCallId.delete(event.toolCallId);
1488
- const priorPassContent = priorGatePass !== undefined && event.isError
1489
- ? [{ type: "text" as const, text: readableGateItem(priorGatePass) }, ...(event.content ?? [])]
1490
- : undefined;
1491
1296
  const role = selectedRole;
1492
1297
  if (role === undefined) return;
1493
1298
  // #676 E / J1: collector operational bookkeeping on the shared tool_result seam.
@@ -1522,16 +1327,15 @@ export function createRoleRuntimeExtension(
1522
1327
  // Persist typed infrastructure-failure fact onto the role session toolResult so
1523
1328
  // exact-session restart shares the same durable completion classification.
1524
1329
  if (infrastructureDetails !== undefined) {
1525
- return { isError: true, ...(priorPassContent === undefined ? {} : { content: priorPassContent }) };
1330
+ return { isError: true };
1526
1331
  }
1527
1332
  // Submission non-pass: throw kept message text for the model; project the
1528
1333
  // envelope-bound structured result onto session details at this tool_result seam.
1529
1334
  const submissionNonPass = pendingSubmissionNonPassByToolCallId.get(event.toolCallId);
1530
1335
  if (submissionNonPass !== undefined) {
1531
1336
  pendingSubmissionNonPassByToolCallId.delete(event.toolCallId);
1532
- return { details: submissionNonPass, isError: true, ...(priorPassContent === undefined ? {} : { content: priorPassContent }) };
1337
+ return { details: submissionNonPass, isError: true };
1533
1338
  }
1534
- if (priorPassContent !== undefined) return { content: priorPassContent, isError: true };
1535
1339
  });
1536
1340
  // Queue receipt delivery before `agent_settled`: that event means Pi has
1537
1341
  // already decided no queued continuation will run, so a triggerTurn there is
@@ -1694,7 +1498,6 @@ export function createRoleRuntimeExtension(
1694
1498
  disposeNavigatorAttendanceNonBlocking(attendanceToDispose);
1695
1499
  pendingInfrastructureFailures.clear();
1696
1500
  pendingSubmissionNonPassByToolCallId.clear();
1697
- pendingPriorGatePassByToolCallId.clear();
1698
1501
  observationFace.reset();
1699
1502
  });
1700
1503
 
@@ -1708,9 +1511,6 @@ export function createRoleRuntimeExtension(
1708
1511
  bindSubmissionNonPass(toolCallId: string, result: SubmissionGateNonPassResult): void {
1709
1512
  pendingSubmissionNonPassByToolCallId.set(toolCallId, result);
1710
1513
  },
1711
- bindPriorGatePass(toolCallId: string, receipt: unknown): void {
1712
- pendingPriorGatePassByToolCallId.set(toolCallId, receipt);
1713
- },
1714
1514
  };
1715
1515
  const requireRoleSoul = (role: PackagedRole): Promise<string> => dependencies.loadRoleSoul(role);
1716
1516
  const judge = createJudgeRoleRuntime(
@@ -1718,7 +1518,6 @@ export function createRoleRuntimeExtension(
1718
1518
  {
1719
1519
  loadSoul: () => requireRoleSoul("judge"),
1720
1520
  },
1721
- hostActions,
1722
1521
  );
1723
1522
  const fixer = createFixerRoleRuntime(
1724
1523
  roleHost,
@@ -1756,7 +1555,6 @@ export function createRoleRuntimeExtension(
1756
1555
  const doctor = createDoctorRoleRuntime(roleHost, {
1757
1556
  loadSoul: () => requireRoleSoul("doctor"),
1758
1557
  async loadCase(path) { if (!dependencies.loadDoctorCase) throw new Error("Doctor runtime dependencies are not configured"); return dependencies.loadDoctorCase(path); },
1759
- async auditCompliance(options) { if (!dependencies.auditDoctorCompliance) throw new Error("Doctor runtime dependencies are not configured"); return dependencies.auditDoctorCompliance(options); },
1760
1558
  }, hostActions);
1761
1559
  const notary = createNotaryRoleRuntime(roleHost, {
1762
1560
  loadSoul: () => requireRoleSoul("notary"),
@@ -1767,7 +1565,7 @@ export function createRoleRuntimeExtension(
1767
1565
  }, hostActions);
1768
1566
  const countersign = createCountersignRoleRuntime(roleHost, {
1769
1567
  loadSoul: () => requireRoleSoul("countersign"),
1770
- }, hostActions);
1568
+ });
1771
1569
  const gleanerLeftRuntime = createGleanerLeftRoleRuntime(roleHost, {
1772
1570
  loadSoul: () => requireRoleSoul("gleaner-left"),
1773
1571
  });
@@ -1802,7 +1600,7 @@ export function createRoleRuntimeExtension(
1802
1600
  );
1803
1601
  const secretariat = createSecretariatRoleRuntime(roleHost, {
1804
1602
  loadSoul: () => requireRoleSoul("secretariat"),
1805
- }, hostActions);
1603
+ });
1806
1604
  const merger = createMergerRoleRuntime(roleHost, {
1807
1605
  loadSoul: () => requireRoleSoul("merger"),
1808
1606
  async loadInput(path) { if (!dependencies.loadMergerInput) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerInput(path); },
@@ -2049,7 +1847,6 @@ export function createRoleRuntimeExtension(
2049
1847
  pendingNavigatorSettlement = undefined;
2050
1848
  pendingInfrastructureFailures.clear();
2051
1849
  pendingSubmissionNonPassByToolCallId.clear();
2052
- pendingPriorGatePassByToolCallId.clear();
2053
1850
  navigatorWorkContext = undefined;
2054
1851
  // #351: OAuth keepalive is orthogonal to --ak-role; start before role early-return
2055
1852
  // so role-less sessions (and reload after shutdown stop) still keep tokens alive.
@@ -12,7 +12,7 @@ function serializeReceipt(receipt: unknown): string {
12
12
  }
13
13
 
14
14
  function gatekeeperNonPassMessage(result: SubmissionGateNonPassResult): string {
15
- if (result.status === "continue" || result.status === "escalate") {
15
+ if (result.status === "continue") {
16
16
  return serializeReceipt(result.receipt);
17
17
  }
18
18
  if (result.status === "transport_failure") {