@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
@@ -1,220 +1,7 @@
1
- import type { Usage } from "@earendil-works/pi-ai";
2
-
3
- import type {
4
- ComplianceDecision,
5
- } from "./compliance-transport.ts";
6
- import { GatekeeperDecisionError } from "./submission-errors.ts";
7
- import type { SubmissionGateNonPassResult } from "./gatekeeper-role.ts";
8
-
1
+ /** Historical audit-escalation discriminator retained for reading pre-#1057 run artifacts. */
9
2
  export const AUDIT_ESCALATION_KIND = "audit_escalation" as const;
10
3
 
11
- // Live Navigator settlement may consume only the projection produced by this
12
- // owner. Its private WeakSet cannot be authored by role output; persisted/
13
- // replayed records are re-authenticated by the retained audit evidence binder.
14
- const AUDIT_ESCALATION_LIVE_REGISTRY = new WeakSet<object>();
15
-
16
- /**
17
- * Escalation delivery face.
18
- * `kind` / `conflicts` / `auditDecisionGate` are audit-owned fields.
19
- * Role-delivered fields ride beside them as open content (ADR 0055) — including
20
- * a role `decisionGate` when present. The index signature tells that truth so
21
- * callers never need a cast to retain role output.
22
- */
23
- export type AuditEscalationResult = {
24
- readonly kind: typeof AUDIT_ESCALATION_KIND;
25
- /** Raw audit-owned conflicts field, when the auditor supplied one. */
26
- readonly conflicts?: unknown;
27
- /** Raw audit-owned gate field, when the auditor supplied one. */
28
- readonly auditDecisionGate?: unknown;
29
- readonly [key: string]: unknown;
30
- };
31
-
32
- export type AuditEscalationToolResult = {
33
- content: [];
34
- details: AuditEscalationResult;
35
- terminate: true;
36
- usage?: Usage;
37
- };
38
-
39
- export type OfficerAuditEscalationDecision = {
40
- readonly status: "escalate";
41
- readonly officer?: "inspector" | "notary" | "auditor" | "countersign";
42
- readonly reason?: unknown;
43
- readonly findings?: unknown;
44
- readonly conflicts?: unknown;
45
- readonly decisionGate?: unknown;
46
- readonly usage?: Usage;
47
- };
48
-
49
- export type AuditEscalationDecision =
50
- | Extract<ComplianceDecision, { status: "escalate" }>
51
- | OfficerAuditEscalationDecision;
52
-
53
- /**
54
- * Build the escalation delivery face.
55
- * Role-delivered fields ride under the escalation discriminator (ADR 0055).
56
- * `kind` always wins so the discriminator cannot be laundered.
57
- * `conflicts` and `auditDecisionGate`, when present, always come from the
58
- * audit (why we escalated and its gate). Raw ancillary values are not repaired.
59
- * A role `decisionGate`, when present, stays at its own key via spread and is
60
- * never overwritten (not folded, not dropped, not swapped into the audit home).
61
- */
62
- export function buildAuditEscalationResult(
63
- decision: AuditEscalationDecision,
64
- deliveredOutput?: unknown,
65
- ): AuditEscalationResult {
66
- const auditOwned: Record<string, unknown> = {
67
- kind: AUDIT_ESCALATION_KIND,
68
- };
69
- if (Object.hasOwn(decision, "receipt")) {
70
- auditOwned.receipt = (decision as { receipt?: unknown }).receipt;
71
- }
72
- if (Object.hasOwn(decision, "conflicts")) {
73
- auditOwned.conflicts = (decision as { conflicts?: unknown }).conflicts;
74
- }
75
- if (Object.hasOwn(decision, "decisionGate")) {
76
- auditOwned.auditDecisionGate = (decision as { decisionGate?: unknown }).decisionGate;
77
- }
78
- if (Object.hasOwn(decision, "reason") && (decision as { reason?: unknown }).reason !== undefined) {
79
- auditOwned.reason = (decision as { reason?: unknown }).reason;
80
- }
81
- if (Object.hasOwn(decision, "officer") && (decision as { officer?: unknown }).officer !== undefined) {
82
- auditOwned.officer = (decision as { officer?: unknown }).officer;
83
- }
84
- if (Object.hasOwn(decision, "findings") && (decision as { findings?: unknown }).findings !== undefined) {
85
- auditOwned.findings = (decision as { findings?: unknown }).findings;
86
- }
87
- const result = {
88
- kind: AUDIT_ESCALATION_KIND,
89
- ...(deliveredOutput !== undefined ? { receipt: deliveredOutput } : {}),
90
- audit: auditOwned,
91
- } as AuditEscalationResult;
92
- AUDIT_ESCALATION_LIVE_REGISTRY.add(result);
93
- return result;
94
- }
95
-
96
- /** True only for the audit-owned live projection, never for role-shaped data. */
97
- export function isAuditEscalationProjection(
98
- value: unknown,
99
- ): value is AuditEscalationResult {
100
- if (!isAuditEscalationResult(value)) return false;
101
- return AUDIT_ESCALATION_LIVE_REGISTRY.has(value);
102
- }
103
-
104
- export function projectAuditEscalation(
105
- decision: AuditEscalationDecision,
106
- deliveredOutput?: unknown,
107
- ): AuditEscalationToolResult {
108
- const details = buildAuditEscalationResult(decision, deliveredOutput);
109
- return {
110
- content: [],
111
- details,
112
- terminate: true,
113
- ...(decision.usage === undefined ? {} : { usage: decision.usage }),
114
- };
115
- }
116
-
117
- /** Preserve the escalating officer's own gate alongside the raw receipt. */
118
- export function projectGatekeeperEscalation(
119
- result: Extract<SubmissionGateNonPassResult, { status: "escalate" }>,
120
- deliveredOutput?: unknown,
121
- ): AuditEscalationToolResult {
122
- const receipt = result.receipt;
123
- const record = receipt !== null && typeof receipt === "object" && !Array.isArray(receipt)
124
- ? receipt as Record<string, unknown>
125
- : undefined;
126
- return projectAuditEscalation({
127
- status: "escalate",
128
- officer: result.officer,
129
- conflicts: receipt,
130
- ...(record !== undefined && Object.hasOwn(record, "decisionGate")
131
- ? { decisionGate: record.decisionGate }
132
- : {}),
133
- }, deliveredOutput);
134
- }
135
-
136
- /**
137
- * Discriminator-only recognition (ADR 0040). Shape of conflicts/options/gate
138
- * is not a reject gate — element types and cardinality are delivery content.
139
- */
140
- export function isAuditEscalationResult(
141
- value: unknown,
142
- ): value is AuditEscalationResult {
143
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
144
- return false;
145
- }
146
- return (value as Record<string, unknown>).kind === AUDIT_ESCALATION_KIND;
147
- }
148
-
149
- export type ComplianceDecisionHandlers<T> = {
150
- converged: (usage: Usage | undefined) => T | PromiseLike<T>;
151
- /** Project the accepted parent candidate beside the typed audit-leg facts. */
152
- noReceipt?: (
153
- facts: Extract<ComplianceDecision, { status: "no-receipt" }>,
154
- usageProjection: { usage?: Usage },
155
- ) => T | PromiseLike<T>;
156
- /**
157
- * Parent work stands with the auditor's raw reply (#757).
158
- * Not a shape-unreadable judgment — the reply is what the auditor said.
159
- */
160
- received?: (
161
- facts: Extract<ComplianceDecision, { status: "received" }>,
162
- usageProjection: { usage?: Usage },
163
- ) => T | PromiseLike<T>;
164
- continue: (violations: readonly unknown[]) => T | PromiseLike<T>;
165
- escalate: (result: AuditEscalationToolResult) => T | PromiseLike<T>;
166
- /** Host failure beside any recorded auditor payloads — parent is not accepted. */
167
- transportFailure?: (
168
- facts: Extract<ComplianceDecision, { status: "transport_failure" }>,
169
- ) => T | PromiseLike<T>;
170
- };
171
-
172
- /**
173
- * Dispose a parsed audit decision without repeating status handling in roles.
174
- * Role output already delivered is preserved on the escalate face (ADR 0055).
175
- */
176
- export async function disposeComplianceDecision<T>(
177
- decision: ComplianceDecision,
178
- handlers: ComplianceDecisionHandlers<T>,
179
- deliveredOutput?: unknown,
180
- ): Promise<Awaited<T>> {
181
- switch (decision.status) {
182
- case "converged":
183
- return await handlers.converged(decision.usage);
184
- case "no-receipt":
185
- // The parent candidate remains accepted, but its parallel audit leg is a
186
- // public typed fact and must not be collapsed into an ordinary pass.
187
- if (handlers.noReceipt === undefined) {
188
- throw new Error("Compliance no-receipt projection handler is unavailable");
189
- }
190
- return await handlers.noReceipt(
191
- decision,
192
- decision.usage === undefined ? {} : { usage: decision.usage },
193
- );
194
- case "received":
195
- // Parent candidate stands; raw auditor reply rides beside — not judged unreadable (#757).
196
- if (handlers.received === undefined) {
197
- throw new Error("Compliance received projection handler is unavailable");
198
- }
199
- return await handlers.received(
200
- decision,
201
- decision.usage === undefined ? {} : { usage: decision.usage },
202
- );
203
- case "continue":
204
- return await handlers.continue(decision.violations);
205
- case "escalate":
206
- return await handlers.escalate(
207
- projectAuditEscalation(decision, deliveredOutput),
208
- );
209
- case "transport_failure":
210
- if (handlers.transportFailure !== undefined) {
211
- return await handlers.transportFailure(decision);
212
- }
213
- throw new GatekeeperDecisionError({
214
- status: "transport_failure",
215
- stage: "auditor",
216
- reason: decision.diagnostic,
217
- submission: decision.submissions ?? decision.terminal,
218
- });
219
- }
4
+ export function isAuditEscalationResult(value: unknown): value is { readonly kind: typeof AUDIT_ESCALATION_KIND } {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value)
6
+ && (value as Record<string, unknown>).kind === AUDIT_ESCALATION_KIND;
220
7
  }
@@ -1,3 +1,6 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+
1
4
  import {
2
5
  joinPackageMaterials,
3
6
  readPackageMaterial,
@@ -102,3 +105,33 @@ export async function loadAuditorSoulFromSubjectInput(raw?: string): Promise<str
102
105
  export function loadAuditorReferenceMaterialsFromSubjectInput(raw?: string): Promise<string> {
103
106
  return loadAuditorReferenceMaterials(resolveAuditorSubject(raw));
104
107
  }
108
+
109
+ function subjectFromSourceDirectory(sourceRunDirectory: string): AuditorSoulRole | undefined {
110
+ const role = basename(sourceRunDirectory).split("@")[1];
111
+ return isAuditorSoulRole(role) ? role : undefined;
112
+ }
113
+
114
+ /**
115
+ * Resume binding already on the admitted page.
116
+ * Explicit subject wins; otherwise the source-run role is the audited object.
117
+ */
118
+ export async function readAuditorResumeBinding(runDirectory: string): Promise<
119
+ | { readonly subject: AuditorSoulRole; readonly sourceRunDirectory: string }
120
+ | undefined
121
+ > {
122
+ let raw: unknown;
123
+ try {
124
+ raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8")) as unknown;
125
+ } catch (error) {
126
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
127
+ throw error;
128
+ }
129
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
130
+ const record = raw as { sourceRunPath?: unknown; auditorSubject?: unknown };
131
+ if (typeof record.sourceRunPath !== "string" || record.sourceRunPath.trim() === "") return undefined;
132
+ const subject = isAuditorSoulRole(record.auditorSubject)
133
+ ? record.auditorSubject
134
+ : subjectFromSourceDirectory(record.sourceRunPath);
135
+ if (subject === undefined) return undefined;
136
+ return { subject, sourceRunDirectory: record.sourceRunPath };
137
+ }
@@ -1,15 +1,13 @@
1
1
  import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
2
- import { disposeComplianceDecision } from "./audit-escalation.ts";
3
- import { ComplianceResponseRetentionError, type ComplianceDecision } from "./compliance-transport.ts";
2
+ import { ComplianceResponseRetentionError } from "./compliance-transport.ts";
4
3
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
5
- import { readableGateItem } from "./readable-gate-item.ts";
6
4
  import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
7
5
 
8
6
  import { sitianReport } from "./sitian-facade.ts";
9
7
 
10
8
  export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME };
11
9
  export const DOCTOR_CASE_FLAG = { name: "ak-doctor-case", definition: { description: "Retained .ak-roles/books/<book>/<n>/runs directory", type: "string" as const } } as const;
12
- export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase>; auditCompliance(options: { context: HostContext; signal?: AbortSignal; submission: unknown }): Promise<ComplianceDecision> };
10
+ export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase> };
13
11
  function appendCandidate(ctx: HostContext, data: unknown): void {
14
12
  try {
15
13
  sitianReport({
@@ -36,7 +34,15 @@ export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDe
36
34
  let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
37
35
  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) };
38
36
  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 }; } });
39
- 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 active = activation; const testimony = validateDoctorOutput(params, active.patient, active.store); try { appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx, submission: testimony } : { context: ctx, signal, submission: testimony }); } catch (error) { host.failInfrastructure(error, ctx, id); } const acceptedDetails = testimony; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { converged: (usage) => ({ content: audit.status === "converged" && audit.receipt !== undefined ? [{ type: "text" as const, text: readableGateItem(audit.receipt) }] : [], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => { try { appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, auditNoReceipt, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } return { content: [], details: acceptedDetails, terminate: true as const, ...usageProjection }; }, received: () => { throw new Error("审刑院非三态应交 runComplianceAudit 重问说话者"); }, continue: (violations) => ({ content: [{ type: "text" as const, text: readableGateItem(audit.status === "continue" ? audit.receipt ?? violations : violations) }], details: acceptedDetails, terminate: false as const }), escalate: (result) => result, transportFailure: (facts) => { host.failInfrastructure(Object.assign(new Error(facts.diagnostic), { name: "ComplianceTransportFailure", ...(facts.submissions === undefined && facts.terminal === undefined ? {} : { submission: facts.submissions ?? facts.terminal }) }), ctx, id); } }, acceptedDetails); } });
37
+ 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>> {
38
+ if (!activation) throw new Error("太医署未激活");
39
+ const active = activation;
40
+ const testimony = validateDoctorOutput(params, active.patient, active.store);
41
+ try {
42
+ appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity });
43
+ } catch (error) { host.failInfrastructure(error, ctx, id); }
44
+ return { content: [], details: testimony, terminate: true };
45
+ } });
40
46
  pi.on("before_agent_start", (event) => { if (!activation) throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { 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>` }; }); }
41
47
  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"); } };
42
48
  }
@@ -43,7 +43,7 @@ export type GatekeeperResult =
43
43
  readonly receipt: unknown;
44
44
  readonly runId?: string;
45
45
  }
46
- /** continue | escalate: both return the officer receipt to the parent (#753 / #756). */
46
+ /** continue returns the receipt for a subject resubmission; escalate resumes this officer directly. */
47
47
  | {
48
48
  readonly status: "continue";
49
49
  readonly officer: GateOfficer;
@@ -81,7 +81,7 @@ export type GatekeeperResult =
81
81
  /** Non-pass faces returned to the parent session (correctable; #836 never kill leg). */
82
82
  export type SubmissionGateNonPassResult = Extract<
83
83
  GatekeeperResult,
84
- { status: "continue" | "escalate" | "no_receipt" | "transport_failure" }
84
+ { status: "continue" | "no_receipt" | "transport_failure" }
85
85
  >;
86
86
 
87
87
  function gateSeatLabel(stage: GateOfficer): string {
@@ -340,14 +340,6 @@ type HostEventResultMap = {
340
340
  type HostEventHandler<K extends keyof HostEventMap> = (event: HostEventMap[K], ctx: HostContext) => HostEventResultMap[K] | void | Promise<HostEventResultMap[K] | void>;
341
341
  export type HostEventRegistration = { [K in keyof HostEventMap]: [event: K, handler: HostEventHandler<K>] }[keyof HostEventMap];
342
342
 
343
- type HostGatekeeperSubject = {
344
- readonly kind:
345
- | "worker_completion"
346
- | "judge_draft"
347
- | "judge_compliance"
348
- | "countersign_verdict"
349
- | "secretariat_verdict";
350
- };
351
343
  /** Gatekeeper non-pass faces returned to parent (#836 includes transport_failure; never kill leg). */
352
344
  type HostGatekeeperNonPass = { readonly status: "continue" | "escalate" | "no_receipt" | "transport_failure" } & Record<string, unknown>;
353
345
  export type HostSubmissionNonPass = HostGatekeeperNonPass;
@@ -378,23 +370,6 @@ export interface RoleHost {
378
370
  getAllTools(): Array<{ name: string; sourceInfo?: { path?: string } }>;
379
371
  setActiveTools(names: string[]): void;
380
372
  getActiveTools(): string[];
381
- /**
382
- * Shared submission gate. On converged/continue returns officer snapshot (receipt + nested
383
- * runId) for seat public-terminal projection (#969); callers may ignore it.
384
- */
385
- requireSubmissionGate?(options: {
386
- context: HostContext;
387
- subject: HostGatekeeperSubject;
388
- signal?: AbortSignal;
389
- hostActions: HostGatekeeperActions;
390
- toolCallId: string;
391
- submission?: unknown;
392
- }): Promise<void | {
393
- readonly status: "converged" | "continue";
394
- readonly officer: string;
395
- readonly receipt: unknown;
396
- readonly runId?: string;
397
- }>;
398
373
  on(event: "before_agent_start", handler: HostEventHandler<"before_agent_start">): void;
399
374
  on(event: "input", handler: HostEventHandler<"input">): void;
400
375
  on(event: "tool_call", handler: HostEventHandler<"tool_call">): void;
package/src/judge-role.ts CHANGED
@@ -1,19 +1,71 @@
1
- import type { RoleHost, HostContext, HostToolResult, HostGatekeeperActions } from "./host-contracts.ts";
1
+ import {
2
+ type RoleHost,
3
+ type HostContext,
4
+ type HostToolResult,
5
+ } from "./host-contracts.ts";
2
6
  import type { Static } from "typebox";
3
- import { REVIEW_QUEUE_STATUSES, reviewSubmissionSchema } from "./review-submission.ts";
4
- import { GatekeeperDecisionError, ParentQueueReaskError, unreadableDiscriminatorNotice } from "./submission-errors.ts";
5
- import { projectGatekeeperEscalation } from "./audit-escalation.ts";
6
-
7
- import { readableGateItem } from "./readable-gate-item.ts";
7
+ import { reviewSubmissionSchema } from "./review-submission.ts";
8
+ import { type GatekeeperSubject } from "./gatekeeper-role.ts";
8
9
  import {
9
10
  JUDGE_OUTPUT_TOOL_NAME,
10
11
  type JudgeVerdict,
11
12
  } from "./package-contracts/judge-output.ts";
12
13
 
13
-
14
14
  export { JUDGE_OUTPUT_TOOL_NAME };
15
15
  export type { JudgeVerdict };
16
16
 
17
+ /** The only judge audit order: 符宝郎, then 审刑院. */
18
+ const JUDGE_GATES: readonly GatekeeperSubject[] = [
19
+ { kind: "judge_draft" },
20
+ { kind: "judge_compliance" },
21
+ ];
22
+
23
+ export async function runJudgeGates(input: {
24
+ readonly gateAlreadyConverged: (subject: GatekeeperSubject) => Promise<boolean>;
25
+ readonly runGate: (
26
+ subject: GatekeeperSubject,
27
+ ) => Promise<{
28
+ readonly status?: unknown;
29
+ readonly receipt?: unknown;
30
+ readonly runId?: string;
31
+ readonly runDirectory?: string;
32
+ } | void>;
33
+ }): Promise<{
34
+ readonly status: "converged" | "continue" | "escalate";
35
+ readonly passes: readonly {
36
+ readonly subject: GatekeeperSubject;
37
+ readonly status: "converged" | "continue" | "escalate";
38
+ readonly receipt: unknown;
39
+ readonly runId?: string;
40
+ readonly runDirectory?: string;
41
+ }[];
42
+ }> {
43
+ const passes: {
44
+ subject: GatekeeperSubject;
45
+ status: "converged" | "continue" | "escalate";
46
+ receipt: unknown;
47
+ runId?: string;
48
+ runDirectory?: string;
49
+ }[] = [];
50
+ for (const subject of JUDGE_GATES) {
51
+ if (await input.gateAlreadyConverged(subject)) continue;
52
+ const pass = await input.runGate(subject);
53
+ const status = pass?.status;
54
+ if (pass === undefined || (status !== "converged" && status !== "continue" && status !== "escalate")) {
55
+ throw new Error("judge gate returned no conclusion");
56
+ }
57
+ passes.push({
58
+ subject,
59
+ status,
60
+ receipt: pass.receipt,
61
+ ...(pass.runId === undefined ? {} : { runId: pass.runId }),
62
+ ...(pass.runDirectory === undefined ? {} : { runDirectory: pass.runDirectory }),
63
+ });
64
+ if (status === "continue" || status === "escalate") return { status, passes };
65
+ }
66
+ return { status: "converged", passes };
67
+ }
68
+
17
69
  // #836 r16 class 1: fix/classes/note/decisionGate are LLM/human-read narrative
18
70
  // content — 符宝郎/审刑院 read the raw receipt, no code branches on their length
19
71
  // or nested presence. `status` alone is the machine discriminator the
@@ -25,16 +77,9 @@ export type JudgeRoleDependencies = {
25
77
  loadSoul(): Promise<string>;
26
78
  };
27
79
 
28
- export type JudgeRoleHostActions = HostGatekeeperActions & {
29
- /** Preserve the first officer's pass as a separate model-visible result item if the second gate fails. */
30
- bindPriorGatePass(toolCallId: string, receipt: unknown): void;
31
- };
32
-
33
-
34
80
  export function createJudgeRoleRuntime(
35
81
  pi: RoleHost,
36
82
  dependencies: JudgeRoleDependencies,
37
- hostActions: JudgeRoleHostActions,
38
83
  ): { activate(): Promise<void> } {
39
84
  let soul: string | undefined;
40
85
  let lifecycleRegistered = false;
@@ -48,86 +93,14 @@ export function createJudgeRoleRuntime(
48
93
  pi.registerTool({
49
94
  name: JUDGE_OUTPUT_TOOL_NAME,
50
95
  label: "大理寺输出",
51
- description: "提交大理寺终局判词。",
96
+ description: "提交大理寺终局判词;交卷调用结束后,由公开调用接缝按判词状态执行适用审核。",
52
97
  promptSnippet: "提交大理寺终局判词",
53
98
  parameters: judgeVerdictSchema,
54
99
  async execute(toolCallId: string, parameters: Static<typeof judgeVerdictSchema>, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> {
55
100
  if (soul === undefined) throw new Error("大理寺职分未装载");
56
- // #756 queue: read shared status only — escalate skips gates;
57
- // unreadable status returns to judge; else 符宝郎内闸 then 审刑院合规.
58
- const rawStatus =
59
- parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
60
- && typeof (parameters as Record<string, unknown>).status === "string"
61
- ? (parameters as Record<string, unknown>).status as string
62
- : undefined;
63
- if (rawStatus === undefined || !REVIEW_QUEUE_STATUSES.has(rawStatus)) {
64
- const received = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
65
- ? (parameters as Record<string, unknown>).status
66
- : undefined;
67
- throw new ParentQueueReaskError(unreadableDiscriminatorNotice("status", received));
68
- }
69
- if (rawStatus === "escalate") {
70
- // Parent escalate → throw to caller as-is; officers do not attend (#753 / #756).
71
- return {
72
- content: [],
73
- details: parameters,
74
- terminate: true as const,
75
- };
76
- }
77
- // Queue membership above is the only three-state check. The receipt is not re-judged.
78
- // Candidate verdict is already on the parent session books as this
79
- // tool-call leaf (first-record-then-audit; run 019fea05 L61/L62).
80
- // #753: 符宝郎内闸 — continue returns the raw receipt as a nonterminal tool result.
81
- let draftPass: { status: "converged" | "continue"; receipt: unknown } | undefined;
82
- try {
83
- const obtainedDraftPass = await pi.requireSubmissionGate!({
84
- context: ctx,
85
- subject: { kind: "judge_draft" },
86
- ...(signal === undefined ? {} : { signal }),
87
- hostActions,
88
- toolCallId,
89
- // #879: this-turn typed payload — identity-bound at submit site.
90
- submission: parameters,
91
- });
92
- draftPass = obtainedDraftPass || undefined;
93
- if (draftPass?.status === "continue") {
94
- return {
95
- content: [{ type: "text" as const, text: readableGateItem(draftPass.receipt) }],
96
- details: parameters,
97
- terminate: false,
98
- };
99
- }
100
- if (draftPass !== undefined) hostActions.bindPriorGatePass(toolCallId, draftPass.receipt);
101
- // #756: 审刑院合规路径 — same review-queue law as 符宝郎/台院.
102
- // converged → accept; continue → raw auditor receipt; escalate → pause;
103
- // not three-state → resume auditor; no round cap; no disposeCompliance mapping.
104
- const compliancePass = await pi.requireSubmissionGate!({
105
- context: ctx,
106
- subject: { kind: "judge_compliance" },
107
- ...(signal === undefined ? {} : { signal }),
108
- hostActions,
109
- toolCallId,
110
- // #879: same parent payload for 审刑院; not recovered from session latest.
111
- submission: parameters,
112
- });
113
- if (compliancePass?.status === "continue") {
114
- return {
115
- content: [draftPass, compliancePass].filter((pass) => pass !== undefined).map((pass) => ({ type: "text" as const, text: readableGateItem(pass.receipt) })),
116
- details: parameters,
117
- terminate: false,
118
- };
119
- }
120
- return {
121
- content: [draftPass, compliancePass].filter((pass) => pass !== undefined).map((pass) => ({ type: "text" as const, text: readableGateItem(pass.receipt) })),
122
- details: parameters,
123
- terminate: true as const,
124
- };
125
- } catch (error) {
126
- if (error instanceof GatekeeperDecisionError && error.result.status === "escalate") {
127
- return projectGatekeeperEscalation(error.result, parameters);
128
- }
129
- throw error;
130
- }
101
+ // ADR 0003: record the original receipt and end the tool call first.
102
+ // The public seam reads status and chooses the next route.
103
+ return { content: [], details: parameters, terminate: true };
131
104
  },
132
105
  });
133
106
  pi.on("before_agent_start", (event) => {
@@ -27,20 +27,3 @@ export type JudgeVerdict =
27
27
  note?: string;
28
28
  evidence?: unknown;
29
29
  };
30
-
31
- export function validateAcceptedJudgeDetails(verdict: unknown): JudgeVerdict {
32
- if (verdict === null || typeof verdict !== "object" || Array.isArray(verdict)) throw new Error("Judge verdict has no execution discriminator");
33
- let status: unknown;
34
- try {
35
- status = (verdict as Record<string, unknown>).status;
36
- } catch {
37
- throw new Error("Judge verdict has no execution discriminator");
38
- }
39
- if (typeof status !== "string") {
40
- throw new Error("Judge verdict has no execution discriminator");
41
- }
42
- if (REVIEW_QUEUE_STATUSES.has(status)) {
43
- return verdict as JudgeVerdict;
44
- }
45
- throw new Error("Judge verdict has no execution discriminator");
46
- }
@@ -10,7 +10,6 @@ import {
10
10
  } from "./collector-output.ts";
11
11
  import {
12
12
  JUDGE_OUTPUT_TOOL_NAME,
13
- validateAcceptedJudgeDetails,
14
13
  type JudgeVerdict,
15
14
  } from "./judge-output.ts";
16
15
  import {
@@ -46,7 +45,6 @@ export {
46
45
  REVIEWER_OUTPUT_TOOL_NAME,
47
46
  MERGER_OUTPUT_TOOL_NAME,
48
47
  validateAcceptedCollectorReceipt,
49
- validateAcceptedJudgeDetails,
50
48
  validateReviewerIntent,
51
49
  validateAcceptedWorkerDetails,
52
50
  validateDoctorSubmissionShape,
package/src/pi/adapter.ts CHANGED
@@ -6,12 +6,10 @@ import {
6
6
  type ToolDefinition,
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import type { Static, TSchema } from "typebox";
9
- import type { SubmissionGateNonPassResult, GatekeeperSubject } from "../gatekeeper-role.ts";
10
- import { requireSubmissionGate } from "../submission-gate.ts";
9
+ import type { SubmissionGateNonPassResult } from "../gatekeeper-role.ts";
11
10
  import type {
12
11
  HostContext,
13
12
  HostEventRegistration,
14
- HostGatekeeperActions,
15
13
  HostToolDefinition,
16
14
  HostToolResult,
17
15
  RoleEnvelopeHost,
@@ -83,35 +81,6 @@ export function toPiContext(context: HostContext): ExtensionContext {
83
81
  return piContext;
84
82
  }
85
83
 
86
- /** Gatekeeper receives HostContext directly (#518 §1③) without recovering raw ExtensionContext via WeakMap. */
87
- function requirePiSubmissionGate(options: {
88
- context: HostContext;
89
- subject: GatekeeperSubject;
90
- signal?: AbortSignal;
91
- hostActions: HostGatekeeperActions;
92
- toolCallId: string;
93
- submission?: unknown;
94
- }): Promise<void | {
95
- readonly status: "converged" | "continue";
96
- readonly officer: string;
97
- readonly receipt: unknown;
98
- readonly runId?: string;
99
- }> {
100
- return requireSubmissionGate({
101
- context: options.context,
102
- subject: options.subject,
103
- ...(options.signal === undefined ? {} : { signal: options.signal }),
104
- hostActions: {
105
- failInfrastructure(error, _context, toolCallId) {
106
- options.hostActions.failInfrastructure(error, options.context, toolCallId);
107
- },
108
- bindSubmissionNonPass: options.hostActions.bindSubmissionNonPass,
109
- },
110
- toolCallId: options.toolCallId,
111
- ...(options.submission === undefined ? {} : { submission: options.submission }),
112
- });
113
- }
114
-
115
84
  function toPiResult<D>(result: HostToolResult<D>): HostToolResult<D> {
116
85
  return result;
117
86
  }
@@ -177,7 +146,6 @@ export function createPiRoleHostAdapter(
177
146
  })),
178
147
  setActiveTools: (names) => pi.setActiveTools(names),
179
148
  getActiveTools: () => pi.getActiveTools(),
180
- requireSubmissionGate: requirePiSubmissionGate,
181
149
  on(...registration: HostEventRegistration) {
182
150
  const context = (value: ExtensionContext) => projectPiContext(value, options.transcriptFromContext);
183
151
  if (registration[0] === "before_agent_start") {
@@ -82,7 +82,7 @@ import {
82
82
  } from "./option-definitions.ts";
83
83
  import { TurnDispatchedFailure } from "./auto-resume.ts";
84
84
  import { continueParentAfterChild, runPublicInstructionSeat, runPublicInstructionSeatResume } from "./instruction-seat-run.ts";
85
- import { courtDiaristEscalated } from "./countersign-run.ts";
85
+ import { latestPayloadEscalated } from "./countersign-run.ts";
86
86
  import { runPublicAnalyst } from "./analyst-run.ts";
87
87
  import {
88
88
  AUTO_RESUME_LIMIT,
@@ -1251,7 +1251,7 @@ export async function runAkRole(
1251
1251
  while (
1252
1252
  current.exitCode === 0 && current.admitted?.correlationId !== undefined
1253
1253
  && current.terminal?.roleOutcome.kind === "accepted"
1254
- && !courtDiaristEscalated(current.terminal.roleOutcome)
1254
+ && !latestPayloadEscalated(current.terminal.roleOutcome)
1255
1255
  ) {
1256
1256
  const parentRunId = current.admitted.correlationId;
1257
1257
  resumeFailureContext = {