@akagilnc/pi-workflow-roles 0.1.3565 → 0.1.3621

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 (142) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +18477 -8099
  4. package/dist/analyst-gate-cycles-read.js +387 -0
  5. package/dist/archivist-record-entry.js +22 -1
  6. package/dist/audit-escalation.js +6 -0
  7. package/dist/auditor-soul.js +74 -0
  8. package/dist/collector-config.js +86 -0
  9. package/dist/collector-evidence.js +316 -0
  10. package/dist/collector-github.js +527 -0
  11. package/dist/collector-ledger.js +853 -0
  12. package/dist/collector-tool-schemas.js +33 -0
  13. package/dist/compliance-transport.js +104 -52
  14. package/dist/diarist-contracts.js +72 -0
  15. package/dist/diarist-mechanical.js +411 -0
  16. package/dist/diarist-ticket-resolution.js +177 -0
  17. package/dist/diarist.js +311 -0
  18. package/dist/doctor-auditor.js +25 -0
  19. package/dist/doctor-contracts.js +2 -0
  20. package/dist/doctor-evidence.js +142 -0
  21. package/dist/gatekeeper-role.js +104 -81
  22. package/dist/host-transition-prior-native.js +78 -0
  23. package/dist/institutional-resolution.js +1 -95
  24. package/dist/judge-auditor.js +26 -0
  25. package/dist/ledger-session-read.js +227 -0
  26. package/dist/merger-git-state.js +78 -0
  27. package/dist/navigator-attendance.js +15 -7
  28. package/dist/navigator-public-session.js +170 -0
  29. package/dist/navigator-session-contracts.js +36 -40
  30. package/dist/notary-source-run.js +122 -0
  31. package/dist/package-contracts/auditor-output.js +66 -0
  32. package/dist/package-contracts/evidence-child-output.js +34 -0
  33. package/dist/package-contracts/judge-output.js +2 -0
  34. package/dist/package-contracts/terminating-tools.js +44 -3
  35. package/dist/package-resources/method-skill.js +254 -0
  36. package/dist/packaged-role-registry.js +56 -0
  37. package/dist/pi/durable-principal.js +61 -0
  38. package/dist/pi/in-process-session.js +25 -6
  39. package/dist/pi/known-failure.js +52 -0
  40. package/dist/pi/role-turn-host.js +430 -0
  41. package/dist/public-cli/auto-resume.js +414 -0
  42. package/dist/public-cli/cli-errors.js +8 -0
  43. package/dist/public-cli/cli-io.js +1 -0
  44. package/dist/public-cli/command-renderer.js +5 -0
  45. package/dist/public-cli/doctor-run.js +84 -0
  46. package/dist/public-cli/inspector-run.js +136 -0
  47. package/dist/public-cli/instruction-seat-run.js +256 -0
  48. package/dist/public-cli/invocation.js +2225 -0
  49. package/dist/public-cli/judge-run.js +118 -0
  50. package/dist/public-cli/load-production-acp-host.js +40 -0
  51. package/dist/public-cli/main.js +1511 -1124
  52. package/dist/public-cli/notary-run.js +161 -0
  53. package/dist/public-cli/option-definitions.js +1192 -0
  54. package/dist/public-cli/post-admission.js +667 -0
  55. package/dist/public-cli/public-run-credentials.js +49 -0
  56. package/dist/public-cli/registry.js +8 -2
  57. package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
  58. package/dist/public-cli/run-lifecycle.js +1344 -0
  59. package/dist/public-cli/seat-ticket-binding.js +113 -0
  60. package/dist/public-cli/settlement.js +3441 -0
  61. package/dist/public-cli/terminal.js +135 -0
  62. package/dist/public-cli/turn-request.js +25 -0
  63. package/dist/public-role-summons.js +301 -0
  64. package/dist/receipt-delivery-policy.js +10 -0
  65. package/dist/reviewer-child-executor.js +80 -8
  66. package/dist/reviewer-execution-ledger.js +2 -1
  67. package/dist/run-terminal-artifacts.js +195 -0
  68. package/dist/run-ticket-number.js +40 -0
  69. package/dist/session-assistant-usage.js +107 -0
  70. package/dist/shape-unreadable-failure.js +34 -0
  71. package/dist/submission-errors.js +3 -0
  72. package/dist/submission-ledger.js +388 -0
  73. package/dist/ticket-provenance-contracts.js +115 -0
  74. package/dist/ticket-provenance.js +340 -0
  75. package/extensions/role-runtime.ts +8 -0
  76. package/package.json +1 -1
  77. package/resources/diarist-collect.md +6 -10
  78. package/scripts/build-package.mjs +2 -1
  79. package/souls/diarist.md +9 -0
  80. package/souls/doctor-auditor.md +1 -0
  81. package/souls/evidence-child.md +1 -0
  82. package/src/acp-host/production-host.ts +4 -0
  83. package/src/acp-host/role-envelope.ts +4 -3
  84. package/src/acp-host/role-turn-host.ts +9 -1
  85. package/src/analyst-gate-cycles-read.ts +37 -2
  86. package/src/archivist-record-entry.ts +45 -1
  87. package/src/audit-escalation.ts +17 -0
  88. package/src/auditor-role.ts +22 -0
  89. package/src/auditor-soul.ts +42 -0
  90. package/src/compliance-transport.ts +177 -66
  91. package/src/diarist-contracts.ts +88 -0
  92. package/src/diarist-role.ts +60 -0
  93. package/src/diarist.ts +253 -179
  94. package/src/doctor-auditor.ts +10 -14
  95. package/src/doctor-contracts.ts +1 -0
  96. package/src/doctor-role.ts +2 -2
  97. package/src/evidence-child-role.ts +22 -0
  98. package/src/gatekeeper-pass-envelope.ts +109 -0
  99. package/src/gatekeeper-role.ts +145 -104
  100. package/src/host-contracts.ts +15 -2
  101. package/src/institutional-resolution.ts +8 -163
  102. package/src/judge-auditor.ts +10 -15
  103. package/src/judge-role.ts +8 -0
  104. package/src/navigator-attendance.ts +25 -9
  105. package/src/navigator-public-session.ts +217 -0
  106. package/src/navigator-session-contracts.ts +61 -54
  107. package/src/notary-source-run.ts +3 -1
  108. package/src/package-contracts/auditor-output.ts +82 -0
  109. package/src/package-contracts/evidence-child-output.ts +51 -0
  110. package/src/package-contracts/judge-output.ts +1 -0
  111. package/src/package-contracts/reviewer-output.ts +2 -2
  112. package/src/package-contracts/terminating-tools.ts +53 -3
  113. package/src/packaged-role-registry.ts +63 -0
  114. package/src/pi/adapter.ts +2 -1
  115. package/src/pi/in-process-session.ts +38 -12
  116. package/src/pi/role-turn-host.ts +42 -0
  117. package/src/public-cli/cli.ts +43 -6
  118. package/src/public-cli/countersign-run.ts +9 -129
  119. package/src/public-cli/diarist-run.ts +312 -0
  120. package/src/public-cli/instruction-seat-run.ts +289 -50
  121. package/src/public-cli/invocation.ts +87 -25
  122. package/src/public-cli/option-definitions.ts +79 -0
  123. package/src/public-cli/post-admission.ts +9 -3
  124. package/src/public-cli/registry.ts +7 -1
  125. package/src/public-cli/run-lifecycle.ts +53 -6
  126. package/src/public-cli/settlement.ts +170 -7
  127. package/src/public-cli/terminal.ts +4 -1
  128. package/src/public-role-summons.ts +431 -0
  129. package/src/receipt-delivery-policy.ts +10 -0
  130. package/src/reviewer-agent.ts +1 -1
  131. package/src/reviewer-child-executor.ts +114 -12
  132. package/src/reviewer-execution-ledger.ts +3 -2
  133. package/src/role-runtime.ts +275 -23
  134. package/src/session-assistant-usage.ts +119 -0
  135. package/src/session-opening-materials.ts +1 -1
  136. package/src/shape-unreadable-failure.ts +49 -0
  137. package/src/submission-errors.ts +3 -0
  138. package/src/ticket-provenance-contracts.ts +1 -0
  139. package/src/ticket-provenance.ts +0 -23
  140. package/dist/evidence-child-executor.js +0 -829
  141. package/src/diarist-llm-collector.ts +0 -324
  142. package/src/evidence-child-executor.ts +0 -1140
@@ -70,6 +70,20 @@ import {
70
70
  type InspectorRuntimeDependencies,
71
71
  } from "./inspector-role.ts";
72
72
  import { INSPECTOR_ACCEPTED_TEXT } from "./inspector-contracts.ts";
73
+ import {
74
+ DIARIST_TOOL_SPEC,
75
+ type DiaristRuntimeDependencies,
76
+ } from "./diarist-role.ts";
77
+ import {
78
+ DIARIST_ACCEPTED_TEXT,
79
+ DIARIST_SOURCES_FLAG,
80
+ projectDiaristSelections,
81
+ } from "./diarist-contracts.ts";
82
+ import {
83
+ commitDiaristSelections,
84
+ loadDiaristSourceCatalog,
85
+ type DiaristSourceCatalog,
86
+ } from "./diarist.ts";
73
87
  import {
74
88
  GATEKEEPER_TOOL_SPEC,
75
89
  type GatekeeperRuntimeDependencies,
@@ -78,6 +92,16 @@ import {
78
92
  NAVIGATOR_TOOL_SPEC,
79
93
  type NavigatorRuntimeDependencies,
80
94
  } from "./navigator-role.ts";
95
+ import {
96
+ AUDITOR_TOOL_SPEC,
97
+ type AuditorRuntimeDependencies,
98
+ } from "./auditor-role.ts";
99
+ import { AUDITOR_ACCEPTED_TEXT } from "./package-contracts/auditor-output.ts";
100
+ import {
101
+ EVIDENCE_CHILD_TOOL_SPEC,
102
+ type EvidenceChildRuntimeDependencies,
103
+ } from "./evidence-child-role.ts";
104
+ import { EVIDENCE_CHILD_ACCEPTED_TEXT } from "./package-contracts/evidence-child-output.ts";
81
105
  import { GATEKEEPER_ACCEPTED_TEXT } from "./package-contracts/gatekeeper-output.ts";
82
106
  import { NAVIGATOR_ACCEPTED_TEXT } from "./package-contracts/navigator-output.ts";
83
107
  import { decorateSettlementWithNavigation, formatNavigatorReport, NAVIGATOR_EVENT_TYPE, navigatorSubjectKey, navigatorUnavailableError, subjectPath, type NavigatorAttendance, type NavigatorAttendanceOptions, type NavigatorEvent, type NavigatorPhase, type NavigatorReport, type NavigatorSettlement, type NavigatorSubjectProvenance, type NavigatorTargetRole, type NavigatorWorkContext } from "./navigator-attendance.ts";
@@ -159,6 +183,14 @@ const GLEANER_LEFT_TRANSPORT_FLAGS = Object.freeze([
159
183
  }),
160
184
  ] as const);
161
185
 
186
+ /** Diarist private transport: frozen source catalog path (ADR 0018 / #708). */
187
+ const DIARIST_TRANSPORT_FLAGS = Object.freeze([
188
+ Object.freeze({
189
+ name: DIARIST_SOURCES_FLAG.name,
190
+ definition: DIARIST_SOURCES_FLAG.definition,
191
+ }),
192
+ ] as const);
193
+
162
194
  /**
163
195
  * Decode private transport flags into frozen admitted inputs.
164
196
  * Envelope-owned; necessary JSON decode only (public --authority-ref owns grammar).
@@ -327,14 +359,11 @@ export {
327
359
  writeToolExecutionObservationRecord,
328
360
  } from "./tool-execution-observation.ts";
329
361
  export type { ToolExecutionObservationRecord, ToolExecutionObservationWriter } from "./tool-execution-observation.ts";
330
- export { executeAuditorChild } from "./evidence-child-executor.ts";
331
- export type { AuditorDecisionTool } from "./evidence-child-executor.ts";
332
362
  export {
333
363
  NOTARY_OUTPUT_TOOL,
334
364
  INSPECTOR_OUTPUT_TOOL,
335
365
  GatekeeperDecisionError,
336
366
  createGatekeeperOutputTool,
337
- createOfficerDecisionTool,
338
367
  runGatekeeper,
339
368
  } from "./gatekeeper-role.ts";
340
369
  export type { GatekeeperResult, GatekeeperSubject, GatekeeperNonPassResult, RunGatekeeperOptions } from "./gatekeeper-role.ts";
@@ -375,7 +404,13 @@ export { fixerPrerequisiteSchema, fixerPrerequisitesSchema, parseFixerPrerequisi
375
404
  export type { FixerInvocationInput, FixerPrerequisite } from "./package-contracts/fixer-packet.ts";
376
405
  export { AUDIT_ESCALATION_KIND, buildAuditEscalationResult, disposeComplianceDecision, isAuditEscalationResult, projectAuditEscalation } from "./audit-escalation.ts";
377
406
  export type { AuditEscalationResult, AuditEscalationToolResult, ComplianceDecisionHandlers } from "./audit-escalation.ts";
378
- export { AUDITOR_SOUL_ROLES, loadAuditorSoul } from "./auditor-soul.ts";
407
+ export {
408
+ AUDITOR_SOUL_ROLES,
409
+ AK_ROLE_AUDITOR_SUBJECT_ENV,
410
+ loadAuditorSoul,
411
+ loadAuditorSoulFromSubjectInput,
412
+ resolveAuditorSubject,
413
+ } from "./auditor-soul.ts";
379
414
  export type { AuditorSoulRole } from "./auditor-soul.ts";
380
415
  export { JUDGE_AUDIT_TOOL_NAME, SOUL_AUDIT_TOOL_NAME, createPiJudgeAuditor } from "./judge-auditor.ts";
381
416
  export { DOCTOR_AUDIT_TOOL_NAME, createPiDoctorAuditor } from "./doctor-auditor.ts";
@@ -439,6 +474,9 @@ type ActivationRuntime = {
439
474
  inspector: { activate(): Promise<void> };
440
475
  gatekeeper: { activate(): Promise<void> };
441
476
  navigator: { activate(): Promise<void> };
477
+ auditor: { activate(): Promise<void> };
478
+ evidenceChild: { activate(): Promise<void> };
479
+ diarist: { activate(): Promise<void> };
442
480
  merger(): Promise<void>;
443
481
  };
444
482
 
@@ -487,6 +525,9 @@ function activationStage(role: PackagedRole, runtime: ActivationRuntime): { id:
487
525
  case "inspector": return { id: "load-and-install", run: async () => runtime.inspector.activate() };
488
526
  case "gatekeeper": return { id: "load-and-install", run: async () => runtime.gatekeeper.activate() };
489
527
  case "navigator": return { id: "load-and-install", run: async () => runtime.navigator.activate() };
528
+ case "auditor": return { id: "load-and-install", run: async () => runtime.auditor.activate() };
529
+ case "evidence-child": return { id: "load-and-install", run: async () => runtime.evidenceChild.activate() };
530
+ case "diarist": return { id: "load-and-install", run: async () => runtime.diarist.activate() };
490
531
  case "merger": return { id: "prepare-git-and-install", run: async () => runtime.merger() };
491
532
  }
492
533
  }
@@ -594,6 +635,9 @@ export type RoleRuntimeDependencies = {
594
635
  loadInspectorSoul?(): Promise<string>;
595
636
  loadGatekeeperSoul?(): Promise<string>;
596
637
  loadNavigatorSoul?(): Promise<string>;
638
+ loadAuditorSoul?(): Promise<string>;
639
+ loadEvidenceChildSoul?(): Promise<string>;
640
+ loadDiaristSoul?(): Promise<string>;
597
641
  loadDoctorCase?(path: string): Promise<import("./doctor-contracts.ts").DoctorCase>;
598
642
  loadMergerSoul?(): Promise<string>;
599
643
  loadMergerInput?(path: string): Promise<unknown>;
@@ -707,13 +751,17 @@ export async function projectClosedSubmissionLifecycle(
707
751
  await settle(publicNavigatorSettlement(projection.role, phase, closure));
708
752
  }
709
753
 
710
- /** Optional pre-accept hook on the shared filed-officer envelope (ADR 0075). */
754
+ /**
755
+ * Optional pre-accept hook on the shared filed-officer envelope (ADR 0075).
756
+ * May return a details projection (envelope-owned machine facts recorded next to
757
+ * the submitted parameters); undefined keeps the parameters as submitted.
758
+ */
711
759
  type FiledOfficerBeforeAccept = (input: {
712
760
  readonly toolCallId: string;
713
761
  readonly parameters: unknown;
714
762
  readonly signal: AbortSignal | undefined;
715
763
  readonly ctx: HostContext;
716
- }) => Promise<void>;
764
+ }) => Promise<unknown>;
717
765
 
718
766
  /**
719
767
  * Shared registration envelope for filed officers (ADR 0018 / #572):
@@ -749,14 +797,15 @@ function createFiledOfficerRuntime(
749
797
  parameters: spec.tool.parameters as never,
750
798
  async execute(toolCallId, parameters, signal, _onUpdate, ctx): Promise<HostToolResult<unknown>> {
751
799
  if (soul === undefined) throw new Error(`${spec.role} 职分未装载`);
752
- if (spec.beforeAccept !== undefined) {
753
- await spec.beforeAccept({ toolCallId, parameters, signal, ctx });
754
- }
800
+ const projected =
801
+ spec.beforeAccept === undefined
802
+ ? undefined
803
+ : await spec.beforeAccept({ toolCallId, parameters, signal, ctx });
755
804
  // Accept-as-is + terminate only. Shape is not an admission gate
756
805
  // (第 0 条 / ADR 0055); sole-final barrier is ledger-owned (#575).
757
806
  return {
758
807
  content: [{ type: "text" as const, text: spec.acceptedText }],
759
- details: parameters,
808
+ details: projected === undefined ? parameters : projected,
760
809
  terminate: true as const,
761
810
  };
762
811
  },
@@ -849,6 +898,125 @@ export function createNavigatorRoleRuntime(
849
898
  );
850
899
  }
851
900
 
901
+ /** #675: public 审刑院 seat on the shared filed-officer envelope. */
902
+ export function createAuditorRoleRuntime(
903
+ roleHost: RoleHost,
904
+ dependencies: AuditorRuntimeDependencies,
905
+ ) {
906
+ const base = createFiledOfficerRuntime(
907
+ roleHost,
908
+ {
909
+ role: "auditor",
910
+ tool: AUDITOR_TOOL_SPEC,
911
+ acceptedText: AUDITOR_ACCEPTED_TEXT,
912
+ soulTag: "auditor",
913
+ },
914
+ dependencies,
915
+ );
916
+ return {
917
+ async activate() {
918
+ await base.activate();
919
+ // Same tools whether nested or direct (#675): dossier tool always registered.
920
+ // Source run: only the shared --source-run input face (never own-run fallback).
921
+ const { createAuditorDossierTool, AUDITOR_DOSSIER_TOOL_NAME } =
922
+ await import("./auditor-dossier-tool.ts");
923
+ const { AK_ROLE_AUDITOR_SOURCE_RUN_ENV } = await import("./auditor-soul.ts");
924
+ const already = roleHost.getAllTools().some((tool) => tool.name === AUDITOR_DOSSIER_TOOL_NAME);
925
+ if (already) return;
926
+ const sourceRun =
927
+ typeof process.env[AK_ROLE_AUDITOR_SOURCE_RUN_ENV] === "string"
928
+ && process.env[AK_ROLE_AUDITOR_SOURCE_RUN_ENV].trim() !== ""
929
+ ? process.env[AK_ROLE_AUDITOR_SOURCE_RUN_ENV].trim()
930
+ : undefined;
931
+ roleHost.registerTool(createAuditorDossierTool(sourceRun) as never);
932
+ },
933
+ };
934
+ }
935
+
936
+ /**
937
+ * #708: 起居郎 public seat on the shared filed-officer envelope.
938
+ * Semantic collection happened in this role's own turn; the accept hook runs the
939
+ * mechanical band (verbatim reverse-verify → idempotent sitian append →
940
+ * watermark) and records the resulting sitian facts next to the receipt.
941
+ * Machine facts never come from model self-report (锚定宪法).
942
+ */
943
+ export function createDiaristRoleRuntime(
944
+ roleHost: RoleHost,
945
+ dependencies: DiaristRuntimeDependencies,
946
+ getSourcesFlag: () => unknown,
947
+ ) {
948
+ // This turn's catalog, snapshotted at activate: the candidateIndexes the role
949
+ // saw and the rows accept commits are the same bytes. Accept never re-reads
950
+ // the file, so anything written to it mid-turn cannot become a diary entry.
951
+ let catalog: DiaristSourceCatalog | undefined;
952
+ const runtime = createFiledOfficerRuntime(
953
+ roleHost,
954
+ {
955
+ role: "diarist",
956
+ tool: DIARIST_TOOL_SPEC,
957
+ acceptedText: DIARIST_ACCEPTED_TEXT,
958
+ soulTag: "diarist",
959
+ beforeAccept: async ({ parameters }) => {
960
+ const submitted =
961
+ parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
962
+ ? (parameters as Record<string, unknown>)
963
+ : undefined;
964
+ // True-unbound summons: no ticket, no catalog, no diary committed — so
965
+ // this turn produced no sitian facts. A self-reported `sitian` is not
966
+ // one (锚定宪法); drop it rather than let it ride into decisiveFacts.
967
+ // Carrying the field is not a reason to bounce the receipt (第 0 条).
968
+ if (catalog === undefined) {
969
+ if (submitted === undefined || !("sitian" in submitted)) return undefined;
970
+ const stripped = { ...submitted };
971
+ delete stripped.sitian;
972
+ return stripped;
973
+ }
974
+ const facts = await commitDiaristSelections({
975
+ catalog,
976
+ selections: projectDiaristSelections(parameters),
977
+ });
978
+ return { ...(submitted ?? { receipt: parameters }), sitian: facts };
979
+ },
980
+ },
981
+ dependencies,
982
+ );
983
+ return {
984
+ async activate() {
985
+ catalog = loadFrozenDiaristCatalog(getSourcesFlag);
986
+ return runtime.activate();
987
+ },
988
+ };
989
+ }
990
+
991
+ /** Envelope-owned decode + one-shot load of the frozen catalog (ADR 0018). */
992
+ function loadFrozenDiaristCatalog(
993
+ getFlag: () => unknown,
994
+ ): DiaristSourceCatalog | undefined {
995
+ const raw = getFlag();
996
+ if (raw === undefined) return undefined;
997
+ if (typeof raw !== "string" || raw.trim() === "") {
998
+ throw new Error("Diarist source catalog flag must be a nonempty path");
999
+ }
1000
+ return loadDiaristSourceCatalog(raw);
1001
+ }
1002
+
1003
+ /** #675: public evidence-child seat on the shared filed-officer envelope. */
1004
+ export function createEvidenceChildRoleRuntime(
1005
+ roleHost: RoleHost,
1006
+ dependencies: EvidenceChildRuntimeDependencies,
1007
+ ) {
1008
+ return createFiledOfficerRuntime(
1009
+ roleHost,
1010
+ {
1011
+ role: "evidence-child",
1012
+ tool: EVIDENCE_CHILD_TOOL_SPEC,
1013
+ acceptedText: EVIDENCE_CHILD_ACCEPTED_TEXT,
1014
+ soulTag: "evidence-child",
1015
+ },
1016
+ dependencies,
1017
+ );
1018
+ }
1019
+
852
1020
  export function createCountersignRoleRuntime(
853
1021
  roleHost: RoleHost,
854
1022
  dependencies: CountersignRuntimeDependencies,
@@ -905,6 +1073,9 @@ export function createRoleRuntimeExtension(
905
1073
  for (const flag of GLEANER_LEFT_TRANSPORT_FLAGS) {
906
1074
  roleHost.registerFlag(flag.name, flag.definition);
907
1075
  }
1076
+ for (const flag of DIARIST_TRANSPORT_FLAGS) {
1077
+ roleHost.registerFlag(flag.name, flag.definition);
1078
+ }
908
1079
 
909
1080
  let admitted = false;
910
1081
  let selectedRole: string | undefined;
@@ -927,6 +1098,9 @@ export function createRoleRuntimeExtension(
927
1098
  // terminating-tool rejections and mechanical delivery requests share two turns.
928
1099
  let receiptDelivery = createReceiptDeliveryPolicy();
929
1100
  let noReceiptRecorded = false;
1101
+ // Public-run fetch observation (in-process-session statusAwareFetch face).
1102
+ let priorFetch: typeof globalThis.fetch | undefined;
1103
+ let fetchWrapped = false;
930
1104
  const settleNavigatorProjection = async (settlement: NavigatorSettlement | undefined) => {
931
1105
  const attendance = navigatorAttendance;
932
1106
  if (settlement === undefined || attendance === undefined) return;
@@ -937,6 +1111,9 @@ export function createRoleRuntimeExtension(
937
1111
  return;
938
1112
  }
939
1113
  const settlePromise = attendance.settle(settlement);
1114
+ // Attach catch immediately so a late rejection after grace timeout cannot
1115
+ // surface as unhandledRejection / stale-ctx after session dispose (#675).
1116
+ void settlePromise.catch(() => undefined);
940
1117
  const raced = await raceNavigatorGrace(settlePromise, NAVIGATOR_POST_ROLE_GRACE_MS);
941
1118
  if (raced.status !== "timeout") return;
942
1119
  if (pendingNavigatorPresentation === undefined) {
@@ -963,7 +1140,6 @@ export function createRoleRuntimeExtension(
963
1140
  pendingNavigatorPresentation = { event, report };
964
1141
  }
965
1142
  await attendance.dispose();
966
- void settlePromise.catch(() => undefined);
967
1143
  })();
968
1144
  pendingNavigatorSettlement = pending;
969
1145
  await pending;
@@ -1190,6 +1366,11 @@ export function createRoleRuntimeExtension(
1190
1366
  roleHost.on("session_shutdown", async () => {
1191
1367
  // #351: stop OAuth keepalive first so shutdown yields zero further ticks.
1192
1368
  envelopeHost.stopKeepalive();
1369
+ if (fetchWrapped && priorFetch !== undefined) {
1370
+ globalThis.fetch = priorFetch;
1371
+ priorFetch = undefined;
1372
+ fetchWrapped = false;
1373
+ }
1193
1374
  // Flush any still-pending affirmative attendance before teardown. Accepted
1194
1375
  // grace-timeout paths normally emit on agent_settled; abort can skip that hook.
1195
1376
  const presentation = pendingNavigatorPresentation;
@@ -1358,6 +1539,28 @@ export function createRoleRuntimeExtension(
1358
1539
  return dependencies.loadNavigatorSoul();
1359
1540
  },
1360
1541
  });
1542
+ const auditor = createAuditorRoleRuntime(roleHost, {
1543
+ async loadSoul() {
1544
+ if (!dependencies.loadAuditorSoul) throw new Error("Auditor runtime dependencies are not configured");
1545
+ return dependencies.loadAuditorSoul();
1546
+ },
1547
+ });
1548
+ const evidenceChild = createEvidenceChildRoleRuntime(roleHost, {
1549
+ async loadSoul() {
1550
+ if (!dependencies.loadEvidenceChildSoul) throw new Error("Evidence-child runtime dependencies are not configured");
1551
+ return dependencies.loadEvidenceChildSoul();
1552
+ },
1553
+ });
1554
+ const diarist = createDiaristRoleRuntime(
1555
+ roleHost,
1556
+ {
1557
+ async loadSoul() {
1558
+ if (!dependencies.loadDiaristSoul) throw new Error("Diarist runtime dependencies are not configured");
1559
+ return dependencies.loadDiaristSoul();
1560
+ },
1561
+ },
1562
+ () => envelopeHost.host.getFlag(DIARIST_SOURCES_FLAG.name),
1563
+ );
1361
1564
  let sessionMergerGitState = dependencies.mergerGitState;
1362
1565
  const merger = createMergerRoleRuntime(roleHost, {
1363
1566
  async loadSoul() { if (!dependencies.loadMergerSoul) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerSoul(); },
@@ -1443,20 +1646,19 @@ export function createRoleRuntimeExtension(
1443
1646
  });
1444
1647
 
1445
1648
  // Public Role run: record typed non-success HTTP for error evidence + v1 resume.
1446
- // 2xx clears prior observation (see recordTypedProviderHttpStatus). Non-success
1447
- // write failure must surface — never silently drop authorized error evidence.
1448
- roleHost.on("after_provider_response", async (event, ctx) => {
1649
+ // Same observation owner as in-process-session statusAwareFetch → typed-provider-http
1650
+ // sidecar (settlement already merges observation.httpStatus into knownFailure).
1651
+ // after_provider_response covers the success-path onResponse face; fetch wrap covers
1652
+ // non-2xx Responses where openai-completions throws before onResponse (#675).
1653
+ const recordHttpObservation = async (
1654
+ status: number,
1655
+ provider: string,
1656
+ ctx: HostContext,
1657
+ ): Promise<void> => {
1449
1658
  const runDir = process.env.AK_ROLE_RUN_DIR;
1450
1659
  if (typeof runDir !== "string" || runDir.trim() === "") return;
1451
- const provider = ctx.model?.provider;
1452
- if (typeof provider !== "string" || provider.trim() === "") return;
1453
- const status = event.status;
1454
- if (typeof status !== "number") return;
1455
1660
  try {
1456
- await recordTypedProviderHttpStatus(runDir, {
1457
- httpStatus: status,
1458
- provider,
1459
- });
1661
+ await recordTypedProviderHttpStatus(runDir, { httpStatus: status, provider });
1460
1662
  } catch (error) {
1461
1663
  if (
1462
1664
  status >= 200 &&
@@ -1469,9 +1671,49 @@ export function createRoleRuntimeExtension(
1469
1671
  }
1470
1672
  failInfrastructure(error, ctx);
1471
1673
  }
1674
+ };
1675
+ roleHost.on("after_provider_response", async (event, ctx) => {
1676
+ const status = event.status;
1677
+ if (typeof status !== "number") return;
1678
+ const fromCtx = ctx.model?.provider;
1679
+ const provider =
1680
+ typeof fromCtx === "string" && fromCtx.trim() !== ""
1681
+ ? fromCtx
1682
+ : "unknown";
1683
+ await recordHttpObservation(status, provider, ctx);
1472
1684
  });
1473
1685
 
1474
1686
  roleHost.on("session_start", async (event, ctx) => {
1687
+ // Scope fetch observation to this public run (in-process-session statusAwareFetch face).
1688
+ if (!fetchWrapped && typeof globalThis.fetch === "function") {
1689
+ priorFetch = globalThis.fetch.bind(globalThis);
1690
+ const underlying = priorFetch;
1691
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
1692
+ const response = await underlying(input, init);
1693
+ const runDir = process.env.AK_ROLE_RUN_DIR;
1694
+ if (
1695
+ typeof runDir === "string"
1696
+ && runDir.trim() !== ""
1697
+ && typeof response?.status === "number"
1698
+ && (response.status < 200 || response.status >= 300)
1699
+ ) {
1700
+ const provider =
1701
+ typeof ctx.model?.provider === "string" && ctx.model.provider.trim() !== ""
1702
+ ? ctx.model.provider
1703
+ : "unknown";
1704
+ try {
1705
+ await recordTypedProviderHttpStatus(runDir, {
1706
+ httpStatus: response.status,
1707
+ provider,
1708
+ });
1709
+ } catch {
1710
+ // Observation must not break the provider stream (same as in-process-session).
1711
+ }
1712
+ }
1713
+ return response;
1714
+ }) as typeof globalThis.fetch;
1715
+ fetchWrapped = true;
1716
+ }
1475
1717
  admitted = false;
1476
1718
  selectedRole = undefined;
1477
1719
  activeReviewerParent = undefined;
@@ -1524,6 +1766,9 @@ export function createRoleRuntimeExtension(
1524
1766
  inspector,
1525
1767
  gatekeeper,
1526
1768
  navigator,
1769
+ auditor,
1770
+ evidenceChild,
1771
+ diarist,
1527
1772
  merger: async () => {
1528
1773
  if (dependencies.mergerGitState === undefined) {
1529
1774
  sessionMergerGitState = dependencies.createMergerGitState?.(ctx.cwd);
@@ -1542,7 +1787,14 @@ export function createRoleRuntimeExtension(
1542
1787
  const ledgerHome = resolveActivationLedgerHomeForPath(sessionFile);
1543
1788
  const session = durableSessionPointer(ctx.sessionManager);
1544
1789
 
1545
- if (dependencies.createNavigatorAttendance !== undefined) {
1790
+ // Same public activation face for every role (#675 / owner 09-06): no nested-env
1791
+ // skip, no work-role whitelist. Attendance attaches when the envelope supplies it.
1792
+ // Navigator seat never re-attaches itself — prepare turns already ARE the navigator
1793
+ // public activation (prevents summonPublicRole navigator ↔ attendance recursion).
1794
+ if (
1795
+ dependencies.createNavigatorAttendance !== undefined
1796
+ && entry.role !== "navigator"
1797
+ ) {
1546
1798
  let work: NavigatorWorkContext;
1547
1799
  let contextError: unknown;
1548
1800
  if (dependencies.loadNavigatorWorkContext === undefined) {
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Sum assistant-message usage rows from a session.jsonl (nested public summons).
3
+ * Accumulates real cost when rows carry it; never invents cost zeros (#675).
4
+ */
5
+ import type { Usage } from "@earendil-works/pi-ai";
6
+ import { join } from "node:path";
7
+
8
+ import type { PublicSummonResult } from "./public-role-summons.ts";
9
+
10
+ export async function readAssistantUsageFromSessionFile(
11
+ sessionFile: string,
12
+ ): Promise<Usage | undefined> {
13
+ const { readFile } = await import("node:fs/promises");
14
+ let text: string;
15
+ try {
16
+ text = await readFile(sessionFile, "utf8");
17
+ } catch (error) {
18
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined;
19
+ throw error;
20
+ }
21
+ let rows: Array<{ type?: string; message?: { role?: string; usage?: Usage } }>;
22
+ try {
23
+ rows = text
24
+ .trim()
25
+ .split("\n")
26
+ .filter(Boolean)
27
+ .map((line) => JSON.parse(line) as {
28
+ type?: string;
29
+ message?: { role?: string; usage?: Usage };
30
+ });
31
+ } catch (error) {
32
+ throw new Error(
33
+ `session usage parse failed (${sessionFile}): ${error instanceof Error ? error.message : String(error)}`,
34
+ { cause: error },
35
+ );
36
+ }
37
+ let total = 0;
38
+ let input = 0;
39
+ let output = 0;
40
+ let cacheRead = 0;
41
+ let cacheWrite = 0;
42
+ let costInput = 0;
43
+ let costOutput = 0;
44
+ let costCacheRead = 0;
45
+ let costCacheWrite = 0;
46
+ let costTotal = 0;
47
+ let sawCost = false;
48
+ for (const row of rows) {
49
+ if (row.type !== "message" || row.message?.role !== "assistant") continue;
50
+ const usage = row.message.usage;
51
+ if (usage === undefined) continue;
52
+ total += usage.totalTokens ?? 0;
53
+ input += usage.input ?? 0;
54
+ output += usage.output ?? 0;
55
+ cacheRead += usage.cacheRead ?? 0;
56
+ cacheWrite += usage.cacheWrite ?? 0;
57
+ if (usage.cost !== undefined) {
58
+ sawCost = true;
59
+ costInput += usage.cost.input ?? 0;
60
+ costOutput += usage.cost.output ?? 0;
61
+ costCacheRead += usage.cost.cacheRead ?? 0;
62
+ costCacheWrite += usage.cost.cacheWrite ?? 0;
63
+ costTotal += usage.cost.total ?? 0;
64
+ }
65
+ }
66
+ if (total <= 0 && input <= 0 && output <= 0) return undefined;
67
+ const base = {
68
+ input,
69
+ output,
70
+ cacheRead,
71
+ cacheWrite,
72
+ totalTokens: total > 0 ? total : input + output + cacheRead + cacheWrite,
73
+ };
74
+ // Cost only when session rows carried it — unknown stays absent (not zero).
75
+ if (!sawCost) return base as Usage;
76
+ return {
77
+ ...base,
78
+ cost: {
79
+ input: costInput,
80
+ output: costOutput,
81
+ cacheRead: costCacheRead,
82
+ cacheWrite: costCacheWrite,
83
+ total: costTotal,
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Resolve the independent role run's session.jsonl from a public summon result.
90
+ * Prefers explicit runDirectory, then terminal artifacts, then no_receipt runPointer.
91
+ */
92
+ export function sessionFileFromPublicSummon(
93
+ summoned: PublicSummonResult,
94
+ ): string | undefined {
95
+ if (typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== "") {
96
+ return join(summoned.runDirectory, "session", "session.jsonl");
97
+ }
98
+ const fromArtifacts = summoned.terminal?.artifacts
99
+ ?.map((a) => (a as { path?: string }).path)
100
+ .find((p): p is string => typeof p === "string" && p.endsWith("session.jsonl"));
101
+ if (fromArtifacts !== undefined) return fromArtifacts;
102
+ const outcome = summoned.terminal?.roleOutcome;
103
+ if (outcome === undefined) return undefined;
104
+ const facts = (outcome as { decisiveFacts?: Record<string, unknown> }).decisiveFacts;
105
+ const pointer = facts?.runPointer;
106
+ if (typeof pointer === "string" && pointer.trim() !== "") {
107
+ return join(pointer, "session", "session.jsonl");
108
+ }
109
+ return undefined;
110
+ }
111
+
112
+ /** Shared usage projection for nested public summons — undefined when unknown. */
113
+ export async function usageFromPublicSummon(
114
+ summoned: PublicSummonResult,
115
+ ): Promise<Usage | undefined> {
116
+ const sessionFile = sessionFileFromPublicSummon(summoned);
117
+ if (sessionFile === undefined) return undefined;
118
+ return readAssistantUsageFromSessionFile(sessionFile);
119
+ }
@@ -3,7 +3,7 @@
3
3
  * errors. Main-role materials derive from PUBLIC_ROLE_RECORDS (including
4
4
  * gatekeeper and navigator). Province map reuses that public gatekeeper record
5
5
  * plus officer material constants (notary shared). Auditor composition stays in
6
- * loadAuditorSoul; loaders share joinPackageMaterials.
6
+ * loadAuditorSoul(subject); loaders share joinPackageMaterials.
7
7
  */
8
8
  import { existsSync } from "node:fs";
9
9
  import { readFile } from "node:fs/promises";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Single authoritative shape-unreadable marker (#675 / ADR 0055).
3
+ * Settlement stamps it; consumers only read it — never re-derive from cause=output.
4
+ */
5
+
6
+ export const SHAPE_UNREADABLE_KEY = "shapeUnreadable" as const;
7
+
8
+ export type ShapeUnreadableStamp = {
9
+ readonly [SHAPE_UNREADABLE_KEY]: true;
10
+ readonly candidate: unknown;
11
+ readonly acceptedReceipt: false;
12
+ };
13
+
14
+ function isRecord(value: unknown): value is Record<string, unknown> {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+
18
+ /** Settlement seat path: stamp retained candidate as shape-unreadable (not infrastructure). */
19
+ export function stampShapeUnreadableDetails(candidate: unknown): ShapeUnreadableStamp {
20
+ return {
21
+ [SHAPE_UNREADABLE_KEY]: true,
22
+ candidate,
23
+ acceptedReceipt: false,
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Marker presence, decided independently of the candidate value: a stamped
29
+ * `undefined` candidate is a shape-unreadable decision, not an absent marker.
30
+ */
31
+ export type RetainedShapeUnreadable = { readonly candidate: unknown };
32
+
33
+ /**
34
+ * Read the settlement-stamped shape candidate from a failure decisiveFacts payload.
35
+ * Looks at root and secondaryEvidence only for the typed marker — no cause inference.
36
+ */
37
+ export function retainedShapeUnreadable(decisiveFacts: unknown): RetainedShapeUnreadable | undefined {
38
+ if (!isRecord(decisiveFacts)) return undefined;
39
+ if (decisiveFacts[SHAPE_UNREADABLE_KEY] === true) {
40
+ return { candidate: decisiveFacts.candidate };
41
+ }
42
+ const secondary = isRecord(decisiveFacts.secondaryEvidence)
43
+ ? decisiveFacts.secondaryEvidence
44
+ : undefined;
45
+ if (secondary !== undefined && secondary[SHAPE_UNREADABLE_KEY] === true) {
46
+ return { candidate: secondary.candidate };
47
+ }
48
+ return undefined;
49
+ }
@@ -5,6 +5,9 @@ function gatekeeperNonPassMessage(result: GatekeeperNonPassResult): string {
5
5
  const findings = result.findings.length === 0 ? "(无 findings)" : result.findings.join("; ");
6
6
  return `门下省打回重写,findings:${findings}`;
7
7
  }
8
+ if (result.status === "unreadable") {
9
+ return `门下省官回执形状不可读(${result.officer}):${result.reason}`;
10
+ }
8
11
  return `门下省 ${result.status}(${result.stage}):${result.reason}`;
9
12
  }
10
13
 
@@ -66,6 +66,7 @@ export type TicketProvenanceEntry = {
66
66
  * Separated by recordClass discriminator — never disguised as a source entry.
67
67
  */
68
68
  export type TicketProvenanceDiagnosticKind =
69
+ /** Historical rows only — the diarist turn's own failure settles the run (#708). */
69
70
  | "collector-failed"
70
71
  | "issue-source-failed"
71
72
  | "quote-verify-failed";