@akagilnc/pi-workflow-roles 0.1.3572 → 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 (129) hide show
  1. package/dist/acp-host/production-host.js +20239 -10556
  2. package/dist/analyst-gate-cycles-read.js +387 -0
  3. package/dist/archivist-record-entry.js +22 -1
  4. package/dist/audit-escalation.js +6 -0
  5. package/dist/auditor-soul.js +74 -0
  6. package/dist/collector-config.js +86 -0
  7. package/dist/collector-evidence.js +316 -0
  8. package/dist/collector-github.js +527 -0
  9. package/dist/collector-ledger.js +853 -0
  10. package/dist/collector-tool-schemas.js +33 -0
  11. package/dist/compliance-transport.js +104 -52
  12. package/dist/diarist-mechanical.js +411 -0
  13. package/dist/diarist-ticket-resolution.js +177 -0
  14. package/dist/diarist.js +311 -0
  15. package/dist/doctor-auditor.js +25 -0
  16. package/dist/doctor-contracts.js +2 -0
  17. package/dist/doctor-evidence.js +142 -0
  18. package/dist/gatekeeper-role.js +104 -81
  19. package/dist/host-transition-prior-native.js +78 -0
  20. package/dist/institutional-resolution.js +1 -95
  21. package/dist/judge-auditor.js +26 -0
  22. package/dist/ledger-session-read.js +227 -0
  23. package/dist/merger-git-state.js +78 -0
  24. package/dist/navigator-attendance.js +15 -7
  25. package/dist/navigator-public-session.js +170 -0
  26. package/dist/navigator-session-contracts.js +36 -40
  27. package/dist/notary-source-run.js +122 -0
  28. package/dist/package-contracts/auditor-output.js +66 -0
  29. package/dist/package-contracts/evidence-child-output.js +34 -0
  30. package/dist/package-contracts/judge-output.js +2 -0
  31. package/dist/package-contracts/terminating-tools.js +25 -2
  32. package/dist/package-resources/method-skill.js +254 -0
  33. package/dist/packaged-role-registry.js +36 -0
  34. package/dist/pi/durable-principal.js +61 -0
  35. package/dist/pi/in-process-session.js +25 -6
  36. package/dist/pi/known-failure.js +52 -0
  37. package/dist/pi/role-turn-host.js +430 -0
  38. package/dist/public-cli/auto-resume.js +414 -0
  39. package/dist/public-cli/cli-errors.js +8 -0
  40. package/dist/public-cli/cli-io.js +1 -0
  41. package/dist/public-cli/command-renderer.js +5 -0
  42. package/dist/public-cli/doctor-run.js +84 -0
  43. package/dist/public-cli/inspector-run.js +136 -0
  44. package/dist/public-cli/instruction-seat-run.js +256 -0
  45. package/dist/public-cli/invocation.js +2225 -0
  46. package/dist/public-cli/judge-run.js +118 -0
  47. package/dist/public-cli/load-production-acp-host.js +40 -0
  48. package/dist/public-cli/main.js +1140 -652
  49. package/dist/public-cli/notary-run.js +161 -0
  50. package/dist/public-cli/option-definitions.js +1192 -0
  51. package/dist/public-cli/post-admission.js +667 -0
  52. package/dist/public-cli/public-run-credentials.js +49 -0
  53. package/dist/public-cli/registry.js +4 -1
  54. package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
  55. package/dist/public-cli/run-lifecycle.js +1344 -0
  56. package/dist/public-cli/seat-ticket-binding.js +113 -0
  57. package/dist/public-cli/settlement.js +3441 -0
  58. package/dist/public-cli/terminal.js +135 -0
  59. package/dist/public-cli/turn-request.js +25 -0
  60. package/dist/public-role-summons.js +301 -0
  61. package/dist/receipt-delivery-policy.js +10 -0
  62. package/dist/reviewer-child-executor.js +80 -8
  63. package/dist/reviewer-execution-ledger.js +2 -1
  64. package/dist/run-terminal-artifacts.js +195 -0
  65. package/dist/run-ticket-number.js +40 -0
  66. package/dist/session-assistant-usage.js +107 -0
  67. package/dist/shape-unreadable-failure.js +34 -0
  68. package/dist/submission-errors.js +3 -0
  69. package/dist/submission-ledger.js +388 -0
  70. package/dist/ticket-provenance-contracts.js +115 -0
  71. package/dist/ticket-provenance.js +340 -0
  72. package/extensions/role-runtime.ts +7 -0
  73. package/package.json +1 -1
  74. package/scripts/build-package.mjs +2 -1
  75. package/souls/doctor-auditor.md +1 -0
  76. package/souls/evidence-child.md +1 -0
  77. package/src/acp-host/production-host.ts +3 -0
  78. package/src/acp-host/role-envelope.ts +2 -2
  79. package/src/acp-host/role-turn-host.ts +9 -1
  80. package/src/analyst-gate-cycles-read.ts +37 -2
  81. package/src/archivist-record-entry.ts +45 -1
  82. package/src/audit-escalation.ts +17 -0
  83. package/src/auditor-role.ts +22 -0
  84. package/src/auditor-soul.ts +42 -0
  85. package/src/compliance-transport.ts +177 -66
  86. package/src/doctor-auditor.ts +10 -14
  87. package/src/doctor-contracts.ts +1 -0
  88. package/src/doctor-role.ts +2 -2
  89. package/src/evidence-child-role.ts +22 -0
  90. package/src/gatekeeper-pass-envelope.ts +109 -0
  91. package/src/gatekeeper-role.ts +145 -104
  92. package/src/host-contracts.ts +9 -1
  93. package/src/institutional-resolution.ts +8 -163
  94. package/src/judge-auditor.ts +10 -15
  95. package/src/judge-role.ts +8 -0
  96. package/src/navigator-attendance.ts +25 -9
  97. package/src/navigator-public-session.ts +217 -0
  98. package/src/navigator-session-contracts.ts +61 -54
  99. package/src/notary-source-run.ts +3 -1
  100. package/src/package-contracts/auditor-output.ts +82 -0
  101. package/src/package-contracts/evidence-child-output.ts +51 -0
  102. package/src/package-contracts/judge-output.ts +1 -0
  103. package/src/package-contracts/reviewer-output.ts +2 -2
  104. package/src/package-contracts/terminating-tools.ts +31 -1
  105. package/src/packaged-role-registry.ts +43 -0
  106. package/src/pi/adapter.ts +2 -1
  107. package/src/pi/in-process-session.ts +38 -12
  108. package/src/pi/role-turn-host.ts +34 -0
  109. package/src/public-cli/cli.ts +14 -6
  110. package/src/public-cli/instruction-seat-run.ts +289 -50
  111. package/src/public-cli/invocation.ts +66 -25
  112. package/src/public-cli/option-definitions.ts +61 -0
  113. package/src/public-cli/post-admission.ts +7 -1
  114. package/src/public-cli/registry.ts +4 -1
  115. package/src/public-cli/run-lifecycle.ts +22 -3
  116. package/src/public-cli/settlement.ts +120 -5
  117. package/src/public-cli/terminal.ts +2 -0
  118. package/src/public-role-summons.ts +431 -0
  119. package/src/receipt-delivery-policy.ts +10 -0
  120. package/src/reviewer-agent.ts +1 -1
  121. package/src/reviewer-child-executor.ts +114 -12
  122. package/src/reviewer-execution-ledger.ts +3 -2
  123. package/src/role-runtime.ts +158 -17
  124. package/src/session-assistant-usage.ts +119 -0
  125. package/src/session-opening-materials.ts +1 -1
  126. package/src/shape-unreadable-failure.ts +49 -0
  127. package/src/submission-errors.ts +3 -0
  128. package/dist/evidence-child-executor.js +0 -829
  129. package/src/evidence-child-executor.ts +0 -1140
@@ -15,6 +15,21 @@ export const AUDITOR_SOUL_ROLES = [
15
15
 
16
16
  export type AuditorSoulRole = (typeof AUDITOR_SOUL_ROLES)[number];
17
17
 
18
+ /**
19
+ * Audited-subject input for public 审刑院 (#675 owner):
20
+ * 「审的是谁」is an input selecting judge-auditor.md / doctor-auditor.md —
21
+ * not a caller-identity fork. Same env for direct `ak-role auditor --subject`
22
+ * and nested compliance summons.
23
+ */
24
+ export const AK_ROLE_AUDITOR_SUBJECT_ENV = "AK_ROLE_AUDITOR_SUBJECT" as const;
25
+
26
+ /**
27
+ * Audited source-run input for public 审刑院 (#675):
28
+ * same --source-run face for direct `ak-role auditor` and nested compliance summons.
29
+ * Never falls back to the auditor's own run directory.
30
+ */
31
+ export const AK_ROLE_AUDITOR_SOURCE_RUN_ENV = "AK_ROLE_AUDITOR_SOURCE_RUN" as const;
32
+
18
33
  function auditorSoulRelativePath(role: AuditorSoulRole): string {
19
34
  return `souls/${role}-auditor.md`;
20
35
  }
@@ -23,6 +38,7 @@ function auditorSoulRelativePath(role: AuditorSoulRole): string {
23
38
  * #470 auditor session materials. Judge carries audit-law + quality-law; doctor does
24
39
  * not (御批四: 参审席 = 大理寺主会话 + 其审计席; 太医线不动).
25
40
  * Reviewer auditor roster removed with #495 S6 gate retirement.
41
+ * #675 owner: no generic auditor.md — subject selects this table.
26
42
  */
27
43
  export const AUDITOR_SESSION_MATERIALS = {
28
44
  judge: [
@@ -37,9 +53,30 @@ export const AUDITOR_SESSION_MATERIALS = {
37
53
  readonly [string, string, ...(readonly string[])]
38
54
  >;
39
55
 
56
+ export function isAuditorSoulRole(value: unknown): value is AuditorSoulRole {
57
+ return value === "judge" || value === "doctor";
58
+ }
59
+
60
+ /** Resolve audited subject from explicit value or the subject-input env. */
61
+ export function resolveAuditorSubject(raw?: string): AuditorSoulRole {
62
+ const value =
63
+ typeof raw === "string" && raw.trim() !== ""
64
+ ? raw.trim()
65
+ : typeof process.env[AK_ROLE_AUDITOR_SUBJECT_ENV] === "string"
66
+ ? process.env[AK_ROLE_AUDITOR_SUBJECT_ENV].trim()
67
+ : "";
68
+ if (!isAuditorSoulRole(value)) {
69
+ throw new Error(
70
+ `auditor subject must be judge|doctor (input --subject / ${AK_ROLE_AUDITOR_SUBJECT_ENV}), got ${value === "" ? "(missing)" : value}`,
71
+ );
72
+ }
73
+ return value;
74
+ }
75
+
40
76
  /**
41
77
  * Load one complete auditor session afresh for each audit invocation.
42
78
  * Blank-soul identity stays owned here; composition reuses joinPackageMaterials.
79
+ * Subject selects the materials table (#675 owner — same for direct and nested).
43
80
  */
44
81
  export async function loadAuditorSoul(role: AuditorSoulRole): Promise<string> {
45
82
  const materials = AUDITOR_SESSION_MATERIALS[role];
@@ -50,3 +87,8 @@ export async function loadAuditorSoul(role: AuditorSoulRole): Promise<string> {
50
87
  }
51
88
  return joinPackageMaterials(materials);
52
89
  }
90
+
91
+ /** Runtime loader: subject input decides which soul file to assemble. */
92
+ export async function loadAuditorSoulFromSubjectInput(raw?: string): Promise<string> {
93
+ return loadAuditorSoul(resolveAuditorSubject(raw));
94
+ }
@@ -1,13 +1,12 @@
1
- import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
2
- import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
1
+ import type { Usage } from "@earendil-works/pi-ai";
3
2
  import { Type } from "typebox";
4
- import {
5
- executeAuditorChild,
6
- } from "./evidence-child-executor.ts";
7
- import { createAuditorDossierTool } from "./auditor-dossier-tool.ts";
3
+ import type { AuditorSoulRole } from "./auditor-soul.ts";
4
+ import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
8
5
  import type { DossierObservation } from "./dossier-resolution.ts";
9
6
  import type { HostContext } from "./host-contracts.ts";
10
7
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
8
+ import type { PublicSummonResult } from "./public-role-summons.ts";
9
+ import { retainedShapeUnreadable } from "./shape-unreadable-failure.ts";
11
10
 
12
11
  export type ComplianceArgumentRootType = "null" | "array" | "undefined" | "string" | "number" | "boolean" | "bigint" | "symbol" | "function";
13
12
  export type ComplianceAuditObservation =
@@ -15,9 +14,25 @@ export type ComplianceAuditObservation =
15
14
  | { kind: "object-status-unreadable"; status: "missing" | "unknown" }
16
15
  | DossierObservation;
17
16
  export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt"; usage?: Usage };
18
- export type ComplianceDecision = { status: "pass"; usage?: Usage } | { status: "revise"; violations: readonly unknown[]; usage?: Usage } | { status: "escalate"; conflicts?: unknown; decisionGate?: unknown; usage?: Usage } | ComplianceNoReceipt;
17
+ /** Shape-unreadable audit leg — parent work stands with typed fact, never forged pass (ADR 0055). */
18
+ export type ComplianceUnreadable = {
19
+ readonly status: "unreadable";
20
+ readonly observation: ComplianceAuditObservation;
21
+ readonly candidate: unknown;
22
+ readonly usage?: Usage;
23
+ };
24
+ export type ComplianceDecision =
25
+ | { status: "pass"; usage?: Usage }
26
+ | { status: "revise"; violations: readonly unknown[]; usage?: Usage }
27
+ | { status: "escalate"; conflicts?: unknown; decisionGate?: unknown; usage?: Usage }
28
+ | ComplianceNoReceipt
29
+ | ComplianceUnreadable;
19
30
 
20
- /** Unreadable compliance candidate — infrastructure failure, not a judgment status (#475). */
31
+ /**
32
+ * Unreadable compliance candidate observation carrier.
33
+ * Shape-unreadable must not abort the parent run (CLAUDE.md §0 / ADR 0055).
34
+ * Callers read observation+candidate; projection keeps typed unreadable — never forged pass.
35
+ */
21
36
  export class ComplianceCandidateUnreadableError extends Error {
22
37
  readonly observation: ComplianceAuditObservation;
23
38
  readonly candidate: unknown;
@@ -43,19 +58,19 @@ export const AUDITOR_DOSSIER_PROMPT = "本 run 卷宗已就绪。" as const;
43
58
 
44
59
  const nonblank = Type.String({ minLength: 1, pattern: "\\S" });
45
60
  const decisionGateSchema = Type.Object({ question: nonblank, options: Type.Array(nonblank, { minItems: 1 }) }, { additionalProperties: false });
46
- // Transport retains malformed candidates on ComplianceCandidateUnreadableError so
47
- // the existing failure channel can publish observation + candidate (#475).
48
- // Status values are guidance, not a schema gate.
49
61
  export const complianceDecisionSchema = Type.Object({ status: Type.Unknown({ description: "pass | revise | escalate — 形状指引,非 schema 闸" }), violations: Type.Array(nonblank, { description: "观察到的合规违规" }), conflicts: Type.Array(nonblank, { description: "未决权威或执行冲突" }), decisionGate: Type.Union([decisionGateSchema, Type.Null()], { description: "升级问题与可选选项" }) }, { additionalProperties: true, required: [] });
50
62
 
51
- export function createComplianceDecisionTool(name: string, description: string) {
52
- return { name, description, parameters: complianceDecisionSchema, async execute(_id: string, params: unknown): Promise<AgentToolResult<unknown>> { return { content: [{ type: "text", text: "审计决议已收" }], details: params, terminate: true }; } };
53
- }
54
-
55
63
  export const COMPLIANCE_RESPONSE_ENTRY_TYPE = "ak_compliance_response" as const;
56
64
  export const AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE = "ak_auditor_parent_attempt_binding" as const;
57
65
  export const AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE = "ak_auditor_compliance_failure" as const;
58
66
 
67
+ export class ComplianceResponseRetentionError extends Error {
68
+ constructor(message: string, options?: ErrorOptions) {
69
+ super(message, options);
70
+ this.name = "ComplianceResponseRetentionError";
71
+ }
72
+ }
73
+
59
74
  export type AuditorParentAttemptBinding = {
60
75
  readonly version: 1;
61
76
  readonly parent: {
@@ -64,34 +79,37 @@ export type AuditorParentAttemptBinding = {
64
79
  readonly attemptEntryId?: string;
65
80
  };
66
81
  };
67
- import { attachDirectErrnoCode, sitianReport } from "./sitian-facade.ts";
68
82
 
69
- export class ComplianceResponseRetentionError extends Error {
70
- constructor(message: string, options?: ErrorOptions) {
71
- super(message, options);
72
- this.name = "ComplianceResponseRetentionError";
73
- attachDirectErrnoCode(this, options?.cause);
83
+ function readListField(value: unknown): readonly unknown[] { return Array.isArray(value) ? value : value === undefined ? [] : [value]; }
84
+
85
+ /** Try to project a lawful compliance decision; undefined when shape is not a known release. */
86
+ export function tryReadComplianceCandidate(arguments_: unknown, usage?: Usage): ComplianceDecision | undefined {
87
+ if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
88
+ return undefined;
74
89
  }
75
- }
76
- function retainComplianceResponse(context: HostContext, response: AssistantMessage): void {
77
- try {
78
- sitianReport({
79
- level: "event",
80
- kind: "auditor",
81
- cwd: context.cwd,
82
- sessionParent: context.sessionManager.getSessionFile(),
83
- payload: { version: 1, response },
84
- source: "compliance-transport",
85
- });
86
- } catch (error) {
87
- throw new ComplianceResponseRetentionError(
88
- `compliance response retention failed: ${error instanceof Error ? error.message : String(error)}`,
89
- { cause: error },
90
- );
90
+ const args = arguments_ as Record<string, unknown>;
91
+ const status = args.status;
92
+ if (status === "pass") return { status, ...(usage === undefined ? {} : { usage }) };
93
+ if (status === "revise") return { status, violations: readListField(args.violations), ...(usage === undefined ? {} : { usage }) };
94
+ if (status === "escalate") {
95
+ return {
96
+ status,
97
+ ...(Object.hasOwn(args, "conflicts") ? { conflicts: args.conflicts } : {}),
98
+ ...(Object.hasOwn(args, "decisionGate") ? { decisionGate: args.decisionGate } : {}),
99
+ ...(usage === undefined ? {} : { usage }),
100
+ };
91
101
  }
102
+ return undefined;
92
103
  }
93
- function readListField(value: unknown): readonly unknown[] { return Array.isArray(value) ? value : value === undefined ? [] : [value]; }
104
+
105
+ /**
106
+ * Read a compliance candidate. Unreadable shape throws ComplianceCandidateUnreadableError
107
+ * with observation+candidate retained — callers must not map that throw onto parent abort
108
+ * (CLAUDE.md §0 / ADR 0055). Prefer tryReadComplianceCandidate at parent projection seams.
109
+ */
94
110
  export function readComplianceCandidate(arguments_: unknown, usage?: Usage): ComplianceDecision {
111
+ const projected = tryReadComplianceCandidate(arguments_, usage);
112
+ if (projected !== undefined) return projected;
95
113
  if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
96
114
  throw new ComplianceCandidateUnreadableError(
97
115
  { kind: "non-object-arguments", type: arguments_ === null ? "null" : Array.isArray(arguments_) ? "array" : typeof arguments_ as ComplianceArgumentRootType },
@@ -99,10 +117,7 @@ export function readComplianceCandidate(arguments_: unknown, usage?: Usage): Com
99
117
  usage,
100
118
  );
101
119
  }
102
- const args = arguments_ as Record<string, unknown>; const status = args.status;
103
- if (status === "pass") return { status, ...(usage === undefined ? {} : { usage }) };
104
- if (status === "revise") return { status, violations: readListField(args.violations), ...(usage === undefined ? {} : { usage }) };
105
- if (status === "escalate") return { status, ...(Object.hasOwn(args, "conflicts") ? { conflicts: args.conflicts } : {}), ...(Object.hasOwn(args, "decisionGate") ? { decisionGate: args.decisionGate } : {}), ...(usage === undefined ? {} : { usage }) };
120
+ const status = (arguments_ as Record<string, unknown>).status;
106
121
  throw new ComplianceCandidateUnreadableError(
107
122
  { kind: "object-status-unreadable", status: status === undefined ? "missing" : "unknown" },
108
123
  arguments_,
@@ -110,38 +125,134 @@ export function readComplianceCandidate(arguments_: unknown, usage?: Usage): Com
110
125
  );
111
126
  }
112
127
 
128
+ /**
129
+ * Public auditor summon for compliance (#675 / ADR 0062 / owner r11).
130
+ * 审刑院 is the independent audit role; subject (who is audited) selects soul files.
131
+ * Same public path whether nested or direct `ak-role auditor --subject … --source-run …`.
132
+ */
133
+ export type AuditorSummon = (
134
+ subject: AuditorSoulRole,
135
+ sourceRunDirectory: string,
136
+ /** Parent cancellation forwarded to the nested activation (#675). */
137
+ signal?: AbortSignal,
138
+ ) => Promise<PublicSummonResult>;
139
+
113
140
  export type RunComplianceAuditOptions = {
114
- tool: ReturnType<typeof createComplianceDecisionTool>;
115
- systemPrompt: string;
116
- /** @deprecated Fixer-lane hand-delivery only (#242 retires). Prefer omitting for zero-projection auditors. */
117
- serializedInput?: string;
118
- roleLabel: string;
119
- invalidDecisionLabel: string;
141
+ /** Who is being audited — selects judge-auditor.md / doctor-auditor.md. */
142
+ readonly subject: AuditorSoulRole;
120
143
  context: HostContext;
121
- /** Exact machine-owned run binding; never sourced from AK_ROLE_RUN_DIR. */
122
144
  runDirectory?: string | undefined;
123
145
  signal?: AbortSignal;
146
+ /** Test seam — production uses summonPublicRole({ role: "auditor", argv: ["--subject", subject, "--source-run", …] }). */
147
+ summonAuditor?: AuditorSummon;
124
148
  };
125
149
 
126
- export async function runComplianceAudit(options: RunComplianceAuditOptions): Promise<ComplianceDecision> {
127
- const prompt = options.serializedInput ?? AUDITOR_DOSSIER_PROMPT;
128
- const receipt = await executeAuditorChild({
129
- tool: options.tool,
130
- dossierTool: createAuditorDossierTool(options.runDirectory),
131
- systemPrompt: options.systemPrompt,
132
- prompt,
133
- roleLabel: options.roleLabel,
134
- context: options.context,
135
- retainResponse: (response) => retainComplianceResponse(options.context, response),
136
- ...(options.runDirectory === undefined ? {} : { runDirectory: options.runDirectory }),
137
- ...(options.signal === undefined ? {} : { signal: options.signal }),
138
- });
139
- if (receipt.noReceiptLifecycle !== undefined) {
150
+ async function usageFromSummonedSession(summoned: PublicSummonResult): Promise<Usage | undefined> {
151
+ const { usageFromPublicSummon } = await import("./session-assistant-usage.ts");
152
+ return usageFromPublicSummon(summoned);
153
+ }
154
+
155
+ function observationFromUnreadableCandidate(candidate: unknown): ComplianceAuditObservation {
156
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
157
+ return {
158
+ kind: "non-object-arguments",
159
+ type: candidate === null ? "null" : Array.isArray(candidate) ? "array" : typeof candidate as ComplianceArgumentRootType,
160
+ };
161
+ }
162
+ const status = (candidate as Record<string, unknown>).status;
163
+ return {
164
+ kind: "object-status-unreadable",
165
+ status: status === undefined ? "missing" : "unknown",
166
+ };
167
+ }
168
+
169
+ function unreadableDecision(
170
+ candidate: unknown,
171
+ usage: Usage | undefined,
172
+ ): ComplianceUnreadable {
173
+ return {
174
+ status: "unreadable",
175
+ observation: observationFromUnreadableCandidate(candidate),
176
+ candidate,
177
+ ...(usage === undefined ? {} : { usage }),
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Project a public auditor terminal onto the parent compliance decision.
183
+ * Lawful pass/revise/escalate/no-receipt flow through.
184
+ * Shape-unreadable keeps original candidate + typed observation (ADR 0055 / §0) —
185
+ * never forged pass, never parent abort. Real provider/engine/disk failures stay loud.
186
+ * Accepted audits always carry real session usage when present (#675 metering).
187
+ */
188
+ async function projectAuditorTerminal(summoned: PublicSummonResult): Promise<ComplianceDecision> {
189
+ const outcome = summoned.terminal?.roleOutcome;
190
+ if (outcome === undefined) {
191
+ throw new Error(`Auditor public summon produced no terminal (exit ${summoned.exitCode})`);
192
+ }
193
+ const usage = await usageFromSummonedSession(summoned);
194
+ if (outcome.kind === "no_receipt") {
195
+ const { status: _ignored, kind: _kind, role: _role, decisiveFacts: _facts, ...facts } = outcome;
140
196
  return {
141
197
  status: "no-receipt",
142
- ...receipt.noReceiptLifecycle,
143
- ...(receipt.response.usage === undefined ? {} : { usage: receipt.response.usage }),
198
+ ...facts,
199
+ ...(usage === undefined ? {} : { usage }),
144
200
  };
145
201
  }
146
- return readComplianceCandidate(receipt.decision, receipt.response.usage);
202
+ if (outcome.kind === "failure") {
203
+ // Single settlement marker only (ADR 0055 / #675) — no cause=output re-derivation.
204
+ const shape = retainedShapeUnreadable(outcome.decisiveFacts);
205
+ if (shape !== undefined) {
206
+ return unreadableDecision(shape.candidate, usage);
207
+ }
208
+ throw new Error(outcome.diagnostic);
209
+ }
210
+ if (outcome.kind === "accepted") {
211
+ const candidate = {
212
+ status: outcome.status,
213
+ ...outcome.decisiveFacts,
214
+ };
215
+ const projected = tryReadComplianceCandidate(candidate, usage);
216
+ if (projected !== undefined) return projected;
217
+ // Accepted-once but not a lawful release: retain candidate as typed unreadable.
218
+ return unreadableDecision(candidate, usage);
219
+ }
220
+ throw new Error("Auditor public summon returned unusable terminal kind");
221
+ }
222
+
223
+ export async function runComplianceAudit(options: RunComplianceAuditOptions): Promise<ComplianceDecision> {
224
+ const runDirectory = options.runDirectory ?? auditorRunDirectory(options.context);
225
+ if (runDirectory === undefined) {
226
+ throw new Error("Compliance audit requires a parent run directory pointer");
227
+ }
228
+ const subject = options.subject;
229
+ const summon =
230
+ options.summonAuditor
231
+ ?? (async (
232
+ auditSubject: AuditorSoulRole,
233
+ sourceRunDirectory: string,
234
+ auditSignal?: AbortSignal,
235
+ ) => {
236
+ // Dynamic import avoids compliance ↔ public-cli circular init (TDZ).
237
+ const { summonPublicRole } = await import("./public-role-summons.ts");
238
+ const { homeFromRunDirectory } = await import("./activation-ledger-topology.ts");
239
+ const home = homeFromRunDirectory(sourceRunDirectory);
240
+ // Same input surface as direct `ak-role auditor --subject … --source-run …`
241
+ // (no ambient env binding for nested-only source).
242
+ return await summonPublicRole({
243
+ role: "auditor",
244
+ argv: [
245
+ "--subject",
246
+ auditSubject,
247
+ "--source-run",
248
+ sourceRunDirectory,
249
+ AUDITOR_DOSSIER_PROMPT,
250
+ ],
251
+ cwd: options.context.cwd ?? process.cwd(),
252
+ home,
253
+ ...(auditSignal === undefined ? {} : { signal: auditSignal }),
254
+ });
255
+ });
256
+ const summoned = await summon(subject, runDirectory, options.signal);
257
+ return await projectAuditorTerminal(summoned);
147
258
  }
@@ -1,8 +1,7 @@
1
1
  import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
2
- import { loadAuditorSoul } from "./auditor-soul.ts";
3
2
  import {
4
- createComplianceDecisionTool,
5
3
  runComplianceAudit,
4
+ type AuditorSummon,
6
5
  type ComplianceDecision,
7
6
  } from "./compliance-transport.ts";
8
7
  import {
@@ -17,16 +16,13 @@ export const DOCTOR_AUDIT_TOOL_NAME = "ak_doctor_audit_decision";
17
16
  export type DoctorAuditOptions = {
18
17
  context: HostContext;
19
18
  signal?: AbortSignal;
19
+ /** Same seam as runComplianceAudit options — offline tracers only. */
20
+ summonAuditor?: AuditorSummon;
20
21
  };
21
22
 
22
- const tool = createComplianceDecisionTool(
23
- DOCTOR_AUDIT_TOOL_NAME,
24
- "提交 typed pass/revise/escalate 决议(太医署审刑)。",
25
- );
26
-
27
23
  /**
28
- * Doctor auditor: zero hand-delivered materials.
29
- * Candidate testimony must already be on the parent-session books.
24
+ * Doctor compliance via public auditor activation (#675 / ADR 0062 / owner r11).
25
+ * 审刑院 is an independent role; subject=doctor selects souls/doctor-auditor.md.
30
26
  */
31
27
  export function createPiDoctorAuditor(): (options: DoctorAuditOptions) => Promise<ComplianceDecision> {
32
28
  return async (options) => {
@@ -36,13 +32,13 @@ export function createPiDoctorAuditor(): (options: DoctorAuditOptions) => Promis
36
32
  requireAuditMaterials(subjects);
37
33
 
38
34
  return runComplianceAudit({
39
- tool,
40
- systemPrompt: await loadAuditorSoul("doctor"),
41
- roleLabel: "Doctor Soul compliance audit",
42
- invalidDecisionLabel: "invalid Doctor audit decision",
35
+ subject: "doctor",
43
36
  context: options.context,
44
- ...(auditorRunDirectory(options.context) === undefined ? {} : { runDirectory: auditorRunDirectory(options.context) }),
37
+ ...(auditorRunDirectory(options.context) === undefined
38
+ ? {}
39
+ : { runDirectory: auditorRunDirectory(options.context) }),
45
40
  ...(options.signal === undefined ? {} : { signal: options.signal }),
41
+ ...(options.summonAuditor === undefined ? {} : { summonAuditor: options.summonAuditor }),
46
42
  });
47
43
  };
48
44
  }
@@ -7,6 +7,7 @@ export const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
7
7
  export const DOCTOR_OUTPUT_TOOL_NAME = "ak_doctor_output";
8
8
  export const DOCTOR_ACCEPTED_TEXT = "太医署回执已接受";
9
9
  export const DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT = "太医署回执已接受;审计无回执";
10
+ export const DOCTOR_ACCEPTED_AUDIT_UNREADABLE_TEXT = "太医署回执已接受;审计形状不可读";
10
11
  export const DOCTOR_OUTPUT_TOOL_DESCRIPTION = "提交唯一终局单案证词;completed 允许空 findings;runtime 补记派生成本入回执。";
11
12
  export const DOCTOR_TARGET_KINDS = ["law", "gate", "template", "station", "seat"] as const;
12
13
  export type DoctorTargetKind = typeof DOCTOR_TARGET_KINDS[number];
@@ -2,7 +2,7 @@ import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts"
2
2
  import { disposeComplianceDecision } from "./audit-escalation.ts";
3
3
  import { ComplianceResponseRetentionError, type ComplianceDecision } from "./compliance-transport.ts";
4
4
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
5
- import { DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, DOCTOR_ACCEPTED_TEXT, DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
5
+ import { DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, DOCTOR_ACCEPTED_AUDIT_UNREADABLE_TEXT, DOCTOR_ACCEPTED_TEXT, DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
6
6
 
7
7
  import { sitianReport } from "./sitian-facade.ts";
8
8
 
@@ -35,7 +35,7 @@ export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDe
35
35
  let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
36
36
  return { async activate() { const path = pi.getFlag(DOCTOR_CASE_FLAG.name); if (typeof path !== "string" || !path.trim()) throw new Error("Doctor requires --ak-doctor-case"); const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Doctor soul is empty"); const patient = await dependencies.loadCase(path); activation = { soul, patient, store: new DoctorEvidenceStore(patient) };
37
37
  if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id: string, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
38
- pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> { if (!activation) throw new Error("太医署未激活"); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendCandidate(ctx, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`太医署回执违 soul:${violations.join("; ")}`); }, escalate: (result) => result }, acceptedDetails); } });
38
+ pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> { if (!activation) throw new Error("太医署未激活"); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendCandidate(ctx, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), unreadable: (auditUnreadable, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_UNREADABLE_TEXT }], details: { ...acceptedDetails, auditUnreadable }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`太医署回执违 soul:${violations.join("; ")}`); }, escalate: (result) => result }, acceptedDetails); } });
39
39
  pi.on("before_agent_start", (event) => { if (!activation) throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { provenance: "由留存 session 字节推导,封入受理回执。", cost: activation.patient.cost }, lawfulTargetKeys: ["case", ...activation.patient.cost.invocations.sources], evidence: activation.patient.evidence.map(({ id, kind, sha256, byteLength, contentLength }) => ({ id, kind, sha256, byteLength, contentLength })) }; return { systemPrompt: `${event.systemPrompt}\n\n<doctor_soul>\n${activation.soul}\n</doctor_soul>\n\n<doctor_case>\n${JSON.stringify(catalog)}\n</doctor_case>` }; }); }
40
40
  const required = [DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME]; const names = pi.getAllTools().map((tool) => tool.name); for (const name of required) if (names.filter((item) => item === name).length !== 1) throw new Error(`Doctor required tool collision or missing: ${name}`); pi.setActiveTools(required); const active = pi.getActiveTools?.() ?? required; if (active.length !== 2 || !required.every((name) => active.includes(name))) throw new Error("Doctor active tool narrowing failed"); } };
41
41
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Public Evidence-Child role — filed-officer envelope (#675).
3
+ */
4
+ import {
5
+ EVIDENCE_CHILD_ACCEPTED_TEXT,
6
+ EVIDENCE_CHILD_OUTPUT_TOOL_NAME,
7
+ evidenceChildOutputSchema,
8
+ } from "./package-contracts/evidence-child-output.ts";
9
+
10
+ export { EVIDENCE_CHILD_ACCEPTED_TEXT, EVIDENCE_CHILD_OUTPUT_TOOL_NAME };
11
+
12
+ export type EvidenceChildRuntimeDependencies = {
13
+ loadSoul(): Promise<string>;
14
+ };
15
+
16
+ export const EVIDENCE_CHILD_TOOL_SPEC = {
17
+ name: EVIDENCE_CHILD_OUTPUT_TOOL_NAME,
18
+ label: "取证输出",
19
+ description: "提交取证报告。",
20
+ promptSnippet: "提交取证报告",
21
+ parameters: evidenceChildOutputSchema,
22
+ } as const;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Shared submit-path envelope for 门下省 gates (ADR 0018 / #675).
3
+ * Owns officer-pointer book + host abort/non-pass faces.
4
+ * Role modules only project via projectGatekeeperRun / runGatekeeper — no book, no catch.
5
+ */
6
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import { bookDirectOfficerRunPointer } from "./archivist-record-entry.ts";
8
+ import type { HostContext } from "./host-contracts.ts";
9
+ import {
10
+ GatekeeperDecisionError,
11
+ GatekeeperEscalationError,
12
+ projectGatekeeperRun,
13
+ type GatekeeperPassHostActions,
14
+ type GatekeeperResult,
15
+ type GatekeeperSubject,
16
+ type GateOfficerSummon,
17
+ } from "./gatekeeper-role.ts";
18
+ import type { PublicSummonResult } from "./public-role-summons.ts";
19
+ import { sessionFileFromPublicSummon } from "./session-assistant-usage.ts";
20
+
21
+ /**
22
+ * Book a typed pointer under parent session/auditor-roles (archivist-owned write).
23
+ * Offline mocks without a real session leave no nested volume (lawful zero).
24
+ */
25
+ function bookDirectOfficerPointer(
26
+ context: ExtensionContext | HostContext,
27
+ officer: "inspector" | "notary",
28
+ result: GatekeeperResult,
29
+ summoned: PublicSummonResult,
30
+ ): void {
31
+ if (
32
+ result.status !== "pass"
33
+ && result.status !== "bounce"
34
+ && result.status !== "escalate"
35
+ && result.status !== "unreadable"
36
+ ) {
37
+ return;
38
+ }
39
+ const parentFile = context.sessionManager?.getSessionFile?.();
40
+ if (typeof parentFile !== "string" || parentFile.trim() === "") return;
41
+ const sessionFile = sessionFileFromPublicSummon(summoned);
42
+ if (sessionFile === undefined) {
43
+ // No independent 正本 to point at — do not synthesize a parallel session.
44
+ return;
45
+ }
46
+ bookDirectOfficerRunPointer({
47
+ parentSessionFile: parentFile,
48
+ officer,
49
+ sessionFile,
50
+ ...(typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== ""
51
+ ? { runDirectory: summoned.runDirectory }
52
+ : {}),
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Shared envelope: project gate, book officer pointer, map onto host actions.
58
+ * unreadable = parent stands (ADR 0055); never mechanical NonPass reject.
59
+ */
60
+ export async function requireGatekeeperPass(options: {
61
+ readonly context: ExtensionContext | HostContext;
62
+ readonly subject: GatekeeperSubject;
63
+ readonly signal?: AbortSignal;
64
+ readonly hostActions: GatekeeperPassHostActions;
65
+ readonly toolCallId: string;
66
+ /** Lowest seam: same as runGatekeeper options.summonOfficer — offline tracers only. */
67
+ readonly summonOfficer?: GateOfficerSummon;
68
+ }): Promise<void> {
69
+ const projected = await projectGatekeeperRun({
70
+ context: options.context,
71
+ subject: options.subject,
72
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
73
+ ...(options.summonOfficer === undefined ? {} : { summonOfficer: options.summonOfficer }),
74
+ });
75
+ const gatekeeper = projected.result;
76
+ // Envelope-owned pointer book. Failure is host infrastructure — single face.
77
+ if (projected.summoned !== undefined) {
78
+ try {
79
+ bookDirectOfficerPointer(
80
+ options.context,
81
+ projected.officer,
82
+ gatekeeper,
83
+ projected.summoned,
84
+ );
85
+ } catch (error) {
86
+ options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
87
+ }
88
+ }
89
+ if (gatekeeper.status === "pass") return;
90
+ // ADR 0055 / #675: shape-unreadable officer output must not mechanically reject parent.
91
+ if (gatekeeper.status === "unreadable") return;
92
+ if (gatekeeper.status === "transport_failure") {
93
+ const error = new Error(`交卷闸 transport_failure(${gatekeeper.stage}):${gatekeeper.reason}`) as Error & {
94
+ stage: typeof gatekeeper.stage;
95
+ reason: string;
96
+ submission?: unknown;
97
+ };
98
+ error.stage = gatekeeper.stage;
99
+ error.reason = gatekeeper.reason;
100
+ if (gatekeeper.submission !== undefined) error.submission = gatekeeper.submission;
101
+ options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
102
+ }
103
+ if (gatekeeper.status === "escalate") {
104
+ throw new GatekeeperEscalationError(gatekeeper);
105
+ }
106
+ // bounce / no_receipt: typed non-pass — envelope owns execute→tool_result bridge.
107
+ options.hostActions.bindSubmissionNonPass(options.toolCallId, gatekeeper);
108
+ throw new GatekeeperDecisionError(gatekeeper);
109
+ }