@akagilnc/pi-workflow-roles 0.1.3280 → 0.1.3353

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 (41) hide show
  1. package/README.md +5 -5
  2. package/README.zh-CN.md +5 -5
  3. package/dist/audit-escalation.js +31 -4
  4. package/dist/gatekeeper-role.js +25 -4
  5. package/dist/grok/production-host.js +1167 -862
  6. package/dist/inspector-contracts.js +5 -2
  7. package/dist/notary-contracts.js +16 -5
  8. package/dist/packaged-role-registry.js +0 -7
  9. package/dist/public-cli/main.js +867 -757
  10. package/package.json +1 -1
  11. package/src/audit-escalation.ts +52 -8
  12. package/src/collector-ledger.ts +309 -74
  13. package/src/collector-receipt.ts +0 -1
  14. package/src/collector-role.ts +21 -14
  15. package/src/diarist-mechanical.ts +16 -5
  16. package/src/gatekeeper-role.ts +33 -4
  17. package/src/grok/role-envelope.ts +68 -52
  18. package/src/inspector-contracts.ts +6 -3
  19. package/src/inspector-role.ts +5 -2
  20. package/src/notary-contracts.ts +20 -5
  21. package/src/notary-role.ts +2 -2
  22. package/src/packaged-role-registry.ts +0 -12
  23. package/src/public-cli/cli.ts +50 -125
  24. package/src/public-cli/coder-run.ts +5 -19
  25. package/src/public-cli/collector-run.ts +65 -21
  26. package/src/public-cli/countersign-run.ts +14 -40
  27. package/src/public-cli/doctor-run.ts +50 -6
  28. package/src/public-cli/fixer-run.ts +5 -19
  29. package/src/public-cli/gleaner-left-run.ts +14 -40
  30. package/src/public-cli/inspector-run.ts +51 -7
  31. package/src/public-cli/instruction-seat-run.ts +27 -0
  32. package/src/public-cli/judge-run.ts +11 -36
  33. package/src/public-cli/merger-run.ts +5 -19
  34. package/src/public-cli/notary-run.ts +54 -5
  35. package/src/public-cli/option-definitions.ts +3 -3
  36. package/src/public-cli/post-admission.ts +76 -0
  37. package/src/public-cli/reviewer-run.ts +5 -19
  38. package/src/public-cli/run-lifecycle.ts +342 -107
  39. package/src/public-cli/settlement.ts +134 -71
  40. package/src/role-runtime.ts +18 -1
  41. package/src/submission-ledger.ts +55 -5
@@ -21,8 +21,8 @@ import {
21
21
  COLLECTOR_READ_TOOL,
22
22
  COLLECTOR_REQUEST_TOOL,
23
23
  COLLECTOR_WAIT_TOOL,
24
- createCollectorLedger,
25
24
  projectEvidenceEntryView,
25
+ type CollectorConfigState,
26
26
  type CollectorLedger,
27
27
  } from "./collector-ledger.ts";
28
28
  import {
@@ -88,6 +88,7 @@ export type CollectorRoleDependencies = {
88
88
  loadSoul(): Promise<string>;
89
89
  createTransport(): CollectorGitHubTransport;
90
90
  createClock?(): CollectorClock;
91
+ createLedger(config: CollectorConfigState, clock: CollectorClock, ctx: HostContext): CollectorLedger;
91
92
  packageExtensionPath?: string;
92
93
  };
93
94
 
@@ -249,15 +250,16 @@ export function createCollectorRoleRuntime(
249
250
  reason: `通进司禁用工具 ${event.toolName}`,
250
251
  };
251
252
  }
252
- // Concurrency is owned at execute (beginOperational), not announced-batch preflight.
253
- // Pi may emit tool_call for every part before any execute runs (ADR 0041).
253
+ if (event.toolName === COLLECTOR_OUTPUT_TOOL) {
254
+ activation.ledger.beginOperational(COLLECTOR_OUTPUT_TOOL, event.toolCallId);
255
+ }
254
256
  if (
255
- activation.ledger.outputAccepted &&
257
+ activation.ledger.outputCandidate &&
256
258
  event.toolName !== COLLECTOR_OUTPUT_TOOL
257
259
  ) {
258
260
  return {
259
261
  block: true,
260
- reason: "回执已受理,本局不再受理操作",
262
+ reason: "通进司已产出输出候选,本局不再受理操作",
261
263
  };
262
264
  }
263
265
  return undefined;
@@ -270,7 +272,7 @@ export function createCollectorRoleRuntime(
270
272
 
271
273
  pi.on("session_shutdown", () => {
272
274
  if (activation === undefined) return;
273
- if (!activation.ledger.outputAccepted || activation.ledger.fatal) {
275
+ if (activation.ledger.fatal) {
274
276
  if (process.exitCode === undefined || process.exitCode === 0) {
275
277
  process.exitCode = 1;
276
278
  }
@@ -452,8 +454,7 @@ export function createCollectorRoleRuntime(
452
454
  params,
453
455
  activation.clock,
454
456
  );
455
- activation.ledger.markOutputAccepted();
456
- activation.ledger.completeOperational(toolCallId);
457
+ activation.ledger.recordOutputCandidate();
457
458
  const acceptedDetails = receipt;
458
459
  return {
459
460
  content: [{
@@ -472,6 +473,8 @@ export function createCollectorRoleRuntime(
472
473
  hostActions.failInfrastructure(error, ctx, toolCallId);
473
474
  }
474
475
  throw error;
476
+ } finally {
477
+ activation.ledger.completeOperational(toolCallId);
475
478
  }
476
479
  },
477
480
  });
@@ -488,7 +491,6 @@ export function createCollectorRoleRuntime(
488
491
  );
489
492
  }
490
493
  if (
491
- event.reason === "resume" ||
492
494
  event.reason === "fork" ||
493
495
  event.reason === "reload"
494
496
  ) {
@@ -581,11 +583,16 @@ export function createCollectorRoleRuntime(
581
583
 
582
584
  const clock = dependencies.createClock?.() ?? createSystemCollectorClock();
583
585
  const transport = dependencies.createTransport();
584
- const ledger = createCollectorLedger({
585
- repository,
586
- prNumber,
587
- manifest,
588
- });
586
+
587
+ const ledger = dependencies.createLedger(
588
+ { repository, prNumber, manifest },
589
+ clock,
590
+ ctx,
591
+ );
592
+
593
+ if (ledger.activationRecorded) {
594
+ firstDispatchDone = true;
595
+ }
589
596
 
590
597
  activation = {
591
598
  soul,
@@ -223,7 +223,7 @@ function isNotificationRow(type: string, text: string): boolean {
223
223
  }
224
224
 
225
225
  /**
226
- * Enumerate cc session user/assistant turns under Claude projects roots.
226
+ * Enumerate cc session user/assistant turns and human queued-command attachments under Claude projects roots.
227
227
  * cc-sessions-first source family (ADR 0075).
228
228
  * Directory/file/non-empty JSONL line failures throw DiaristSourceReadError
229
229
  * (失败诚实:不得把未读懂洗成没有来源块).
@@ -301,11 +301,22 @@ export function readCcSessionBlocks(
301
301
  { cause: error },
302
302
  );
303
303
  }
304
- // Unknown row shape (not user/assistant) is ignored — not a read failure.
304
+ // Unknown row shape (not user/assistant or a human queued-command attachment) is ignored — not a read failure.
305
305
  if (!isRecord(parsed)) continue;
306
306
  const type = typeof parsed.type === "string" ? parsed.type : "";
307
- if (type !== "user" && type !== "assistant") continue;
308
- const transcript = messageText(parsed.message ?? parsed);
307
+ const queuedCommand =
308
+ type === "attachment" &&
309
+ isRecord(parsed.attachment) &&
310
+ parsed.attachment.type === "queued_command" &&
311
+ isRecord(parsed.attachment.origin) &&
312
+ parsed.attachment.origin.kind === "human" &&
313
+ typeof parsed.attachment.prompt === "string"
314
+ ? parsed.attachment.prompt
315
+ : undefined;
316
+ if (type !== "user" && type !== "assistant" && queuedCommand === undefined) {
317
+ continue;
318
+ }
319
+ const transcript = queuedCommand ?? messageText(parsed.message ?? parsed);
309
320
  if (transcript.trim() === "") continue;
310
321
  const entryId =
311
322
  typeof parsed.uuid === "string"
@@ -323,7 +334,7 @@ export function readCcSessionBlocks(
323
334
  sourceRef: { sessionFile, entryId },
324
335
  transcript,
325
336
  timestamp,
326
- isUserTurn: type === "user",
337
+ isUserTurn: type === "user" || queuedCommand !== undefined,
327
338
  isNotification: notification,
328
339
  });
329
340
  }
@@ -36,6 +36,13 @@ export type GatekeeperResult =
36
36
  readonly findings: readonly string[];
37
37
  readonly submission: unknown;
38
38
  }
39
+ | {
40
+ readonly status: "escalate";
41
+ readonly officer: "inspector" | "notary";
42
+ readonly reason?: unknown;
43
+ readonly findings: unknown;
44
+ readonly submission: unknown;
45
+ }
39
46
  | { readonly status: "no_receipt"; readonly stage: "gatekeeper" | "inspector" | "notary"; readonly reason: string; readonly facts: NoReceiptLifecycleFacts }
40
47
  | {
41
48
  readonly status: "transport_failure";
@@ -63,6 +70,15 @@ function gateSeatLabel(stage: "gatekeeper" | "inspector" | "notary"): string {
63
70
 
64
71
  export { GatekeeperDecisionError } from "./submission-errors.ts";
65
72
 
73
+ export class GatekeeperEscalationError extends Error {
74
+ readonly gatekeeper: Extract<GatekeeperResult, { status: "escalate" }>;
75
+ constructor(gatekeeper: Extract<GatekeeperResult, { status: "escalate" }>) {
76
+ super(`门下省${gateSeatLabel(gatekeeper.officer)}上呈`);
77
+ this.name = "GatekeeperEscalationError";
78
+ this.gatekeeper = gatekeeper;
79
+ }
80
+ }
81
+
66
82
  export type RunGatekeeperOptions = {
67
83
  readonly context: ExtensionContext | HostContext;
68
84
  readonly subject: GatekeeperSubject;
@@ -82,8 +98,9 @@ export type GatekeeperPassHostActions = {
82
98
  // Unknown fields so wrong types/spellings still reach projection (ADR 0055/0057; 仓第 0 条).
83
99
  // Opening goes through the sole openToolObject owner — no parallel transport helper.
84
100
  const officerDecisionSchema = openToolObject(Type.Object({
85
- status: Type.Unknown({ description: "pass | bounce — 形状指引,非 schema 闸" }),
86
- findings: Type.Unknown({ description: "string[] findings,随 pass 或 bounce 留存" }),
101
+ status: Type.Unknown({ description: "pass | bounce | escalate — 形状指引,非 schema 闸" }),
102
+ reason: Type.Optional(Type.Unknown({ description: "status 为 escalate 时的理由" })),
103
+ findings: Type.Unknown({ description: "string[] findings,随 pass、bounce 或 escalate 留存" }),
87
104
  }));
88
105
 
89
106
 
@@ -121,7 +138,7 @@ function subjectTool(subject: GatekeeperSubject): AuditorDecisionTool {
121
138
  export function createOfficerDecisionTool(name: string): AuditorDecisionTool {
122
139
  return {
123
140
  name,
124
- description: "提交一份 typed pass/bounce 决议。",
141
+ description: "提交一份 typed pass/bounce/escalate 决议。",
125
142
  parameters: officerDecisionSchema,
126
143
  async execute(_id, args) { return result(`已收 ${String((args as { status?: unknown })?.status)}`, args); },
127
144
  };
@@ -179,7 +196,7 @@ function noUsableReleaseFailure(
179
196
  return {
180
197
  status: "transport_failure",
181
198
  stage,
182
- reason: stage === "gatekeeper" ? "decision 无显式 dispatch" : "decision 无显式 pass/bounce",
199
+ reason: stage === "gatekeeper" ? "decision 无显式 dispatch" : "decision 无显式 pass/bounce/escalate",
183
200
  submission: retainedSubmission(decision),
184
201
  };
185
202
  }
@@ -224,6 +241,15 @@ function projectOfficerDecision(
224
241
  findings: asStringArray(record.findings),
225
242
  };
226
243
  }
244
+ if (record.status === "escalate") {
245
+ return {
246
+ status: "escalate",
247
+ officer,
248
+ ...(Object.hasOwn(record, "reason") ? { reason: record.reason } : {}),
249
+ findings: record.findings,
250
+ submission: retainedSubmission(decision),
251
+ };
252
+ }
227
253
  return noUsableReleaseFailure(officer, decision);
228
254
  }
229
255
 
@@ -301,6 +327,9 @@ export async function requireGatekeeperPass(options: {
301
327
  if (gatekeeper.submission !== undefined) error.submission = gatekeeper.submission;
302
328
  options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
303
329
  }
330
+ if (gatekeeper.status === "escalate") {
331
+ throw new GatekeeperEscalationError(gatekeeper);
332
+ }
304
333
  // Envelope owns the execute→tool_result bridge; this module only projects + throws.
305
334
  options.hostActions.bindSubmissionNonPass(options.toolCallId, gatekeeper);
306
335
  throw new GatekeeperDecisionError(gatekeeper);
@@ -623,59 +623,75 @@ export async function prepareGrokRoleEnvelope(options: {
623
623
 
624
624
  // Shared envelope activation. systemPrompt must be ready before session/new
625
625
  // (ACP delivers it there), so activation runs during prepare.
626
- await emit("session_start", { reason: request.continuation.kind });
627
- const inputResults = await emit("input", { text: request.continuation.prompt, source: "interactive" });
628
- let prompt = request.continuation.prompt;
629
- for (const value of inputResults) {
630
- if (typeof value !== "object" || value === null) continue;
631
- const record = value as Record<string, unknown>;
632
- if (record.action === "transform" && typeof record.text === "string") prompt = record.text;
633
- }
634
- // Book the user assignment so judge audit subjects recover it from parent books.
635
- if (typeof prompt === "string" && prompt.trim() !== "") {
636
- sessionEntries.push({
637
- type: "message",
638
- message: { role: "user", content: prompt },
626
+ try {
627
+ await emit("session_start", { reason: request.continuation.kind });
628
+ const inputResults = await emit("input", { text: request.continuation.prompt, source: "interactive" });
629
+ let prompt = request.continuation.prompt;
630
+ for (const value of inputResults) {
631
+ if (typeof value !== "object" || value === null) continue;
632
+ const record = value as Record<string, unknown>;
633
+ if (record.action === "transform" && typeof record.text === "string") prompt = record.text;
634
+ }
635
+ // Book the user assignment so judge audit subjects recover it from parent books.
636
+ if (typeof prompt === "string" && prompt.trim() !== "") {
637
+ sessionEntries.push({
638
+ type: "message",
639
+ message: { role: "user", content: prompt },
640
+ });
641
+ }
642
+ const basePrompt = await loadMainRoleSessionMaterials(request.activation.role);
643
+ const methodPrompt = (await Promise.all(request.methods.map(({ path }) => readFile(path, "utf8")))).join("\n\n");
644
+ const promptResults = await emit("before_agent_start", {
645
+ prompt,
646
+ systemPrompt: [basePrompt, methodPrompt].filter(Boolean).join("\n\n"),
647
+ systemPromptOptions: {},
639
648
  });
640
- }
641
- const basePrompt = await loadMainRoleSessionMaterials(request.activation.role);
642
- const methodPrompt = (await Promise.all(request.methods.map(({ path }) => readFile(path, "utf8")))).join("\n\n");
643
- const promptResults = await emit("before_agent_start", {
644
- prompt,
645
- systemPrompt: [basePrompt, methodPrompt].filter(Boolean).join("\n\n"),
646
- systemPromptOptions: {},
647
- });
648
- const systemPromptBody = [...promptResults].reverse().find((value): value is { systemPrompt: string } =>
649
- typeof value === "object" && value !== null && "systemPrompt" in value && typeof value.systemPrompt === "string")?.systemPrompt
650
- ?? [basePrompt, methodPrompt].filter(Boolean).join("\n\n");
651
- // Typed reading materials from agent-start handlers (machine face; independent of prompt bytes).
652
- // Folded into the provider-visible systemPrompt by the adapter at the send boundary.
653
- const readingMaterials: unknown[] = [];
654
- for (const value of promptResults) {
655
- if (typeof value !== "object" || value === null) continue;
656
- if (!("readingMaterial" in value)) continue;
657
- const material = (value as { readingMaterial?: unknown }).readingMaterial;
658
- if (material !== undefined) readingMaterials.push(material);
659
- }
649
+ const systemPromptBody = [...promptResults].reverse().find((value): value is { systemPrompt: string } =>
650
+ typeof value === "object" && value !== null && "systemPrompt" in value && typeof value.systemPrompt === "string")?.systemPrompt
651
+ ?? [basePrompt, methodPrompt].filter(Boolean).join("\n\n");
652
+ // Typed reading materials from agent-start handlers (machine face; independent of prompt bytes).
653
+ // Folded into the provider-visible systemPrompt by the adapter at the send boundary.
654
+ const readingMaterials: unknown[] = [];
655
+ for (const value of promptResults) {
656
+ if (typeof value !== "object" || value === null) continue;
657
+ if (!("readingMaterial" in value)) continue;
658
+ const material = (value as { readingMaterial?: unknown }).readingMaterial;
659
+ if (material !== undefined) readingMaterials.push(material);
660
+ }
660
661
 
661
- priorAkRoleRunDir = process.env.AK_ROLE_RUN_DIR;
662
- process.env.AK_ROLE_RUN_DIR = request.runDirectory;
663
- runDirInjected = true;
662
+ priorAkRoleRunDir = process.env.AK_ROLE_RUN_DIR;
663
+ process.env.AK_ROLE_RUN_DIR = request.runDirectory;
664
+ runDirInjected = true;
664
665
 
665
- return {
666
- mcpServers: [{
667
- name: `ak-${request.activation.role}`,
668
- command: process.execPath,
669
- args: [relay],
670
- env: [
671
- { name: "AK_GROK_MCP_SOCKET", value: options.socketPath },
672
- { name: "AK_GROK_MCP_TOKEN", value: token },
673
- ],
674
- }],
675
- systemPrompt: { body: systemPromptBody, materials: readingMaterials },
676
- prompt,
677
- abortSignal: hostAbort.signal,
678
- closeRound,
679
- dispose,
680
- };
666
+ return {
667
+ mcpServers: [{
668
+ name: `ak-${request.activation.role}`,
669
+ command: process.execPath,
670
+ args: [relay],
671
+ env: [
672
+ { name: "AK_GROK_MCP_SOCKET", value: options.socketPath },
673
+ { name: "AK_GROK_MCP_TOKEN", value: token },
674
+ ],
675
+ }],
676
+ systemPrompt: { body: systemPromptBody, materials: readingMaterials },
677
+ prompt,
678
+ abortSignal: hostAbort.signal,
679
+ closeRound,
680
+ dispose,
681
+ };
682
+ } catch (error) {
683
+ // listen already succeeded; dispose is not yet caller-owned. Release the
684
+ // unix listener before surfacing the activation/prepare failure. Keep both
685
+ // causes when dispose also fails (failure-honesty; do not erase cleanup).
686
+ try {
687
+ await dispose();
688
+ } catch (cleanupFailure) {
689
+ throw new AggregateError(
690
+ [error, cleanupFailure],
691
+ "prepareGrokRoleEnvelope activation failed and its dispose cleanup also failed",
692
+ { cause: error },
693
+ );
694
+ }
695
+ throw error;
696
+ }
681
697
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public Inspector (察院) terminating receipt contracts.
3
- * Lawful explicit releases: pass | bounce.
3
+ * Lawful explicit releases: pass | bounce | escalate.
4
4
  * Dual path: gate-dispatched and independently callable (#568 / ADR 0074).
5
5
  */
6
6
 
@@ -9,7 +9,8 @@ export const INSPECTOR_ACCEPTED_TEXT = "察院回执已接受";
9
9
 
10
10
  export type InspectorOutput =
11
11
  | { readonly status: "pass"; readonly findings?: unknown }
12
- | { readonly status: "bounce"; readonly findings?: unknown };
12
+ | { readonly status: "bounce"; readonly findings?: unknown }
13
+ | { readonly status: "escalate"; readonly reason?: unknown; readonly findings?: unknown };
13
14
 
14
15
  export function validateRecordedInspectorOutput(value: unknown): InspectorOutput {
15
16
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -21,7 +22,7 @@ export function validateRecordedInspectorOutput(value: unknown): InspectorOutput
21
22
  } catch {
22
23
  throw new Error("Inspector output has no execution discriminator");
23
24
  }
24
- if (status === "pass" || status === "bounce") {
25
+ if (status === "pass" || status === "bounce" || status === "escalate") {
25
26
  return value as InspectorOutput;
26
27
  }
27
28
  throw new Error("Inspector output has no execution discriminator");
@@ -32,5 +33,7 @@ export function inspectorDecisiveFacts(output: InspectorOutput): Record<string,
32
33
  const facts: Record<string, unknown> = { status: output.status };
33
34
  const findings = (output as { findings?: unknown }).findings;
34
35
  if (findings !== undefined) facts.findings = findings;
36
+ const reason = (output as { reason?: unknown }).reason;
37
+ if (reason !== undefined) facts.reason = reason;
35
38
  return facts;
36
39
  }
@@ -23,11 +23,14 @@ export const inspectorOutputSchema = withInfrastructureFailureDeclaration(
23
23
  openToolObject(
24
24
  Type.Object({
25
25
  status: Type.Unknown({
26
- description: "pass | bounce — 形状指引,非 schema 闸",
26
+ description: "pass | bounce | escalate — 形状指引,非 schema 闸",
27
27
  }),
28
28
  findings: Type.Unknown({
29
29
  description: "随交卷留存的问题记录",
30
30
  }),
31
+ reason: Type.Optional(Type.Unknown({
32
+ description: "status 为 escalate 时的上呈理由",
33
+ })),
31
34
  }),
32
35
  ),
33
36
  );
@@ -44,7 +47,7 @@ export type InspectorRuntimeDependencies = {
44
47
  export const INSPECTOR_TOOL_SPEC = {
45
48
  name: INSPECTOR_OUTPUT_TOOL_NAME,
46
49
  label: "察院输出",
47
- description: "察院终局回执,状态为 pass 或 bounce。",
50
+ description: "察院终局回执,状态为 pass、bounce 或 escalate。",
48
51
  promptSnippet: "察院终局回执",
49
52
  parameters: inspectorOutputSchema,
50
53
  } as const;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public Notary (符宝郎) terminating receipt contracts.
3
- * Lawful explicit releases: pass | bounce.
3
+ * Lawful explicit releases: pass | bounce | escalate.
4
4
  * No usable result is infrastructure failure via public settlement, not a judgment status (#475).
5
5
  */
6
6
  import { Type } from "typebox";
@@ -35,11 +35,14 @@ export const notaryOutputSchema = withInfrastructureFailureDeclaration(
35
35
  openToolObject(
36
36
  Type.Object({
37
37
  status: Type.Unknown({
38
- description: "pass | bounce — 形状指引,非 schema 闸",
38
+ description: "pass | bounce | escalate — 形状指引,非 schema 闸",
39
39
  }),
40
40
  findings: Type.Unknown({
41
- description: "string[] findings,随 pass 或 bounce 留存",
41
+ description: "string[] findings,随 pass、bounce 或 escalate 留存",
42
42
  }),
43
+ reason: Type.Optional(Type.Unknown({
44
+ description: "status 为 escalate 时的上呈理由",
45
+ })),
43
46
  }),
44
47
  ),
45
48
  );
@@ -56,6 +59,11 @@ export type NotaryOutput =
56
59
  readonly status: "bounce";
57
60
  readonly disposition: "rewrite";
58
61
  readonly findings: readonly string[];
62
+ }
63
+ | {
64
+ readonly status: "escalate";
65
+ readonly reason?: unknown;
66
+ readonly findings?: unknown;
59
67
  };
60
68
 
61
69
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -68,7 +76,7 @@ function asStringArray(value: unknown): readonly string[] {
68
76
  }
69
77
 
70
78
  /**
71
- * Project one lawful explicit Notary release (pass | bounce).
79
+ * Project one lawful explicit Notary release (pass | bounce | escalate).
72
80
  * No throw on shape — ADR 0055 / 第 0 条: already-submitted params are retained as-is;
73
81
  * public-terminal projects non-usable releases via typed failure cause.
74
82
  */
@@ -86,6 +94,9 @@ export function projectLawfulNotaryOutput(value: unknown): NotaryOutput | undefi
86
94
  if (!Array.isArray(clone.findings)) clone.findings = asStringArray(clone.findings);
87
95
  return clone as NotaryOutput;
88
96
  }
97
+ if (status === "escalate") {
98
+ return structuredClone(value) as NotaryOutput;
99
+ }
89
100
  return undefined;
90
101
  }
91
102
 
@@ -100,7 +111,7 @@ export function retainNotarySubmission(value: unknown): unknown {
100
111
  }
101
112
 
102
113
  /**
103
- * Settlement/recording path: only lawful recorded pass/bounce.
114
+ * Settlement/recording path: only lawful recorded pass/bounce/escalate.
104
115
  * Does not gate role admission — callers must not use this to reject a submission.
105
116
  */
106
117
  export function validateRecordedNotaryOutput(value: unknown): NotaryOutput {
@@ -121,5 +132,9 @@ export function notaryDecisiveFacts(output: NotaryOutput): Record<string, unknow
121
132
  if (status === "bounce") {
122
133
  facts.disposition = "rewrite";
123
134
  }
135
+ if (status === "escalate") {
136
+ const reason = (output as { reason?: unknown }).reason;
137
+ if (reason !== undefined) facts.reason = reason;
138
+ }
124
139
  return facts;
125
140
  }
@@ -163,7 +163,7 @@ export function createNotaryRoleRuntime(
163
163
  pi.registerTool({
164
164
  name: NOTARY_OUTPUT_TOOL_NAME,
165
165
  label: "符宝郎输出",
166
- description: "提交引文保真与票面对齐的 typed pass/bounce 决议。",
166
+ description: "提交引文保真与票面对齐的 typed pass/bounce/escalate 决议。",
167
167
  promptSnippet: "提交符宝郎决议",
168
168
  parameters: notaryOutputSchema,
169
169
  async execute(toolCallId: string, parameters: unknown, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext, ): Promise<HostToolResult<unknown>> {
@@ -171,7 +171,7 @@ export function createNotaryRoleRuntime(
171
171
  throw new Error("符宝郎未激活");
172
172
  }
173
173
  // Unique submission + terminate only. Shape is not an admission gate
174
- // (第 0 条 / ADR 0055): lawful pass/bounce projected; else params as-is.
174
+ // (第 0 条 / ADR 0055): lawful pass/bounce/escalate projected; else params as-is.
175
175
  // #541 infra declaration + sole-final barrier are ledger-owned (#575).
176
176
  const lawful = projectLawfulNotaryOutput(parameters);
177
177
  const details = lawful ?? retainNotarySubmission(parameters);
@@ -190,18 +190,6 @@ export const PUBLIC_ROLE_RECORDS = [
190
190
  export type PublicRoleRecord = (typeof PUBLIC_ROLE_RECORDS)[number];
191
191
  export type PackagedRole = PublicRoleRecord["role"];
192
192
 
193
- /**
194
- * Seats that refuse resume. Collector/doctor/notary remain pending independent
195
- * authority review; countersign/gleaner-left revoked by #599 (DK-1/2/3).
196
- * Inspector is one-shot on the filed-officer envelope (#568 / ADR 0074).
197
- */
198
- export const ONE_SHOT_ROLES: readonly PackagedRole[] = [
199
- "collector",
200
- "doctor",
201
- "notary",
202
- "inspector",
203
- ];
204
-
205
193
  /**
206
194
  * Read-only metadata projection (no sessionMaterials).
207
195
  * Distributed per PublicRoleRecord member so role↔field associations stay intact.