@akagilnc/pi-workflow-roles 0.1.4977 → 0.1.4989

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 (65) hide show
  1. package/dist/acp-host/production-host.js +460 -462
  2. package/dist/analyst-gate-cycles-read.js +16 -11
  3. package/dist/analyst-metric-families/gate-cycles.js +2 -2
  4. package/dist/audit-escalation.js +19 -4
  5. package/dist/compliance-transport.js +2 -2
  6. package/dist/countersign-contracts.js +6 -5
  7. package/dist/doctor-evidence.js +2 -1
  8. package/dist/gatekeeper-role.js +22 -43
  9. package/dist/headless-host/production-host.js +460 -462
  10. package/dist/inspector-contracts.js +3 -2
  11. package/dist/migrate-book-topology.js +59 -54
  12. package/dist/navigator-invocation-identity.js +6 -4
  13. package/dist/notary-contracts.js +3 -19
  14. package/dist/package-contracts/auditor-output.js +4 -19
  15. package/dist/package-contracts/judge-output.js +6 -5
  16. package/dist/package-contracts/terminating-tools.js +9 -3
  17. package/dist/packaged-role-registry.js +10 -12
  18. package/dist/public-cli/countersign-run.js +1 -1
  19. package/dist/public-cli/main.js +95 -87
  20. package/dist/public-cli/option-definitions.js +2 -2
  21. package/dist/public-cli/settlement.js +15 -12
  22. package/dist/review-submission.js +20 -0
  23. package/dist/submission-errors.js +2 -2
  24. package/dist/submission-ledger.js +16 -2
  25. package/package.json +1 -1
  26. package/souls/doctor-auditor.md +1 -1
  27. package/souls/fixer-auditor.md +4 -4
  28. package/souls/gate-output-guide.md +2 -2
  29. package/souls/inspector.md +1 -1
  30. package/souls/judge-auditor.md +2 -2
  31. package/souls/notary.md +1 -1
  32. package/souls/quality-law.md +1 -1
  33. package/src/analyst-gate-cycles-read.ts +17 -9
  34. package/src/analyst-metric-families/gate-cycles.ts +2 -2
  35. package/src/audit-escalation.ts +27 -7
  36. package/src/auditor-role.ts +1 -1
  37. package/src/compliance-transport.ts +7 -7
  38. package/src/countersign-contracts.ts +10 -8
  39. package/src/countersign-role.ts +6 -35
  40. package/src/doctor-evidence.ts +2 -1
  41. package/src/doctor-role.ts +1 -2
  42. package/src/gatekeeper-role.ts +32 -57
  43. package/src/host-contracts.ts +4 -3
  44. package/src/inspector-contracts.ts +7 -6
  45. package/src/inspector-role.ts +3 -20
  46. package/src/judge-role.ts +31 -51
  47. package/src/navigator-invocation-identity.ts +16 -4
  48. package/src/notary-contracts.ts +8 -26
  49. package/src/notary-role.ts +1 -1
  50. package/src/package-contracts/auditor-output.ts +6 -27
  51. package/src/package-contracts/judge-output.ts +10 -8
  52. package/src/package-contracts/terminating-tools.ts +9 -3
  53. package/src/packaged-role-registry.ts +12 -13
  54. package/src/pi/adapter.ts +6 -5
  55. package/src/public-cli/countersign-run.ts +1 -1
  56. package/src/public-cli/option-definitions.ts +2 -2
  57. package/src/public-cli/settlement.ts +18 -15
  58. package/src/review-submission.ts +29 -0
  59. package/src/role-envelope.ts +17 -9
  60. package/src/role-runtime.ts +32 -15
  61. package/src/submission-errors.ts +6 -6
  62. package/src/{gatekeeper-pass-envelope.ts → submission-gate.ts} +29 -16
  63. package/src/submission-ledger.ts +18 -3
  64. package/src/ticket-trajectory.ts +9 -4
  65. package/src/worker-role.ts +19 -5
@@ -2,8 +2,8 @@
2
2
 
3
3
  三官共读。产出指引,不因措辞拒收。
4
4
 
5
- - `pass`:正常放行
6
- - `bounce`:职掌内查出须重写。逐条给理由与证据。重写交卷,不是本局失败。
5
+ - `converged`:正常放行
6
+ - `continue`:职掌内查出须重写。逐条给理由与证据。重写交卷,不是本局失败。
7
7
  - 受理御前(受审者提交 `escalate`):能解决就给出答案放其续写;解决不了就放行上呈;没有打回重叫。
8
8
  - 第0条:reason 不评格式;中性记账;原卷随附;下一个 LLM 自己读。
9
9
 
@@ -14,4 +14,4 @@
14
14
 
15
15
  ## 结论
16
16
 
17
- 结论以三态明示:pass(署)/bounce(封驳)/escalate(上呈)。
17
+ 结论以三态明示:converged(署)/continue(封驳)/escalate(上呈)。
@@ -6,6 +6,6 @@
6
6
  二、错封——封驳或所加约束指不出法源,或超出票面与陛下的话。
7
7
  三、事实错——判词所引代码事实、法源引文与源头不符。
8
8
 
9
- 三者皆无即 pass,一轮了结。判词少写一句而事实立得住,不是错;施工没做完,是大理寺封驳的理由,不是你打回判词的理由。打回与上一轮相同、对方只换措辞重交,上呈。
9
+ 三者皆无即 converged,一轮了结。判词少写一句而事实立得住,不是错;施工没做完,是大理寺封驳的理由,不是你打回判词的理由。打回与上一轮相同、对方只换措辞重交,上呈。
10
10
 
11
- 调卷宗与取证、纪律依《审刑院法典》。合规则 `pass`;有错则 `bounce`,逐条指出是哪一种错和你查到的原因;无法判断时 `escalate`。
11
+ 调卷宗与取证、纪律依《审刑院法典》。合规则 `converged`;有错则 `continue`,逐条指出是哪一种错和你查到的原因;无法判断时 `escalate`。
package/souls/notary.md CHANGED
@@ -31,4 +31,4 @@
31
31
 
32
32
  ## 结论
33
33
 
34
- 结论以三态明示:'pass'(署) 'bounce'(封驳) 'escalate'(上呈)。
34
+ 结论以三态明示:'converged'(署) 'continue'(封驳) 'escalate'(上呈)。
@@ -47,7 +47,7 @@
47
47
 
48
48
  unfinished 只认两因:**缺前置**——处置当下仍缺少已声明的前置产物、决定或答复,且该缺失使剩余派工当前无法合法执行;**违宪约束**——继续施工将违反 authority 或既裁法源。两因之外的一切理由(执行窗口、时间、工作量、难度等),不得作为 unfinished 成立依据,审官一律封驳。
49
49
 
50
- 据实以两因交卷不构成违规,但不自动 pass;所称阻断须可由卷宗独立核对,声明与引用本身不能证明缺失或因果。
50
+ 据实以两因交卷不构成违规,但不自动 converged;所称阻断须可由卷宗独立核对,声明与引用本身不能证明缺失或因果。
51
51
 
52
52
  ## 预警参考线
53
53
 
@@ -140,7 +140,7 @@ type GateToolCall = {
140
140
  };
141
141
 
142
142
  function isGateTerminatingToolName(toolName: string): boolean {
143
- return DISPATCH_TOOLS.has(toolName) || OFFICER_TOOL_TO_FACE[toolName] !== undefined;
143
+ return toolName === "ak_submission_output" || DISPATCH_TOOLS.has(toolName) || OFFICER_TOOL_TO_FACE[toolName] !== undefined;
144
144
  }
145
145
 
146
146
  /**
@@ -278,6 +278,7 @@ function projectAcceptedGateCall(
278
278
  readonly attemptEntryId?: string;
279
279
  readonly parentSessionFile?: string;
280
280
  },
281
+ pointerOfficer?: "inspector" | "notary",
281
282
  ): ClassifiedVolume | undefined {
282
283
  // Continuous memory volumes carry many summons; span only this binding's interval.
283
284
  const span = requireAcceptedGateSpan(intervalRowsForGateCall(rows, call.rowIndex), filePath);
@@ -312,7 +313,9 @@ function projectAcceptedGateCall(
312
313
  };
313
314
  }
314
315
 
315
- const officer = OFFICER_TOOL_TO_FACE[call.toolName];
316
+ const officer = call.toolName === "ak_submission_output"
317
+ ? pointerOfficer
318
+ : OFFICER_TOOL_TO_FACE[call.toolName];
316
319
  if (officer === undefined) {
317
320
  // isGateTerminatingToolName already screened; keep loud if tables drift.
318
321
  throw new Error(
@@ -334,6 +337,7 @@ function projectAcceptedGateCall(
334
337
 
335
338
  async function classifyAuditorVolume(
336
339
  filePath: string,
340
+ pointerOfficer?: "inspector" | "notary",
337
341
  ): Promise<readonly ClassifiedVolume[]> {
338
342
  // Canonical JSONL errors propagate — failure honesty (never wash to fewer rounds).
339
343
  const rows = await readLedgerSessionJsonl(filePath);
@@ -342,7 +346,9 @@ async function classifyAuditorVolume(
342
346
  if (accepted.length === 0) return [];
343
347
  const volumes: ClassifiedVolume[] = [];
344
348
  for (const call of accepted) {
345
- const projected = projectAcceptedGateCall(filePath, rows, call);
349
+ const projected = call.toolName === "ak_submission_output" && pointerOfficer === undefined
350
+ ? undefined // Shared tool alone carries no officer identity; a typed pointer does.
351
+ : projectAcceptedGateCall(filePath, rows, call, pointerOfficer);
346
352
  if (projected !== undefined) volumes.push(projected);
347
353
  }
348
354
  return volumes;
@@ -425,7 +431,7 @@ function pairGateRounds(
425
431
  /** Resolve a direct-officer-run-pointer file to the officer session 正本 path. */
426
432
  async function resolveOfficerSessionFromPointerFile(
427
433
  pointerPath: string,
428
- ): Promise<string | undefined> {
434
+ ): Promise<{ sessionFile: string; officer?: "inspector" | "notary" }> {
429
435
  const { readFile } = await import("node:fs/promises");
430
436
  let raw: unknown;
431
437
  try {
@@ -443,7 +449,9 @@ async function resolveOfficerSessionFromPointerFile(
443
449
  if (typeof sessionFile !== "string" || sessionFile.trim() === "") {
444
450
  throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
445
451
  }
446
- return sessionFile;
452
+ const officer = raw.officer === "inspector" || raw.officer === "notary"
453
+ ? raw.officer : undefined;
454
+ return { sessionFile, ...(officer === undefined ? {} : { officer }) };
447
455
  }
448
456
 
449
457
  /**
@@ -488,15 +496,15 @@ export async function readAnalystGateCyclesFromAuditorRoles(
488
496
  for (const name of names) {
489
497
  const path = join(directory, name);
490
498
  const fromPointer = name.endsWith(".pointer.json");
491
- const sessionPath = fromPointer
499
+ const pointer = fromPointer
492
500
  ? await resolveOfficerSessionFromPointerFile(path)
493
- : path;
494
- if (sessionPath === undefined) continue;
501
+ : undefined;
502
+ const sessionPath = pointer?.sessionFile ?? path;
495
503
  if (fromPointer) {
496
504
  if (classifiedOfficerSessions.has(sessionPath)) continue;
497
505
  classifiedOfficerSessions.add(sessionPath);
498
506
  }
499
- const classified = await classifyAuditorVolume(sessionPath);
507
+ const classified = await classifyAuditorVolume(sessionPath, pointer?.officer);
500
508
  for (const volume of classified) {
501
509
  if (
502
510
  options.parentSessionFile !== undefined &&
@@ -93,8 +93,8 @@ function emptyAccum(): OfficerAccum {
93
93
  function absorbRound(accum: OfficerAccum, round: AnalystGateCyclesRoundRow): void {
94
94
  accum.rounds += 1;
95
95
  accum.wallSum += round.officerWallMs;
96
- if (round.status === "bounce") accum.bounceCount += 1;
97
- if (round.status === "pass") accum.passCount += 1;
96
+ if (round.status === "bounce" || round.status === "continue") accum.bounceCount += 1;
97
+ if (round.status === "pass" || round.status === "converged") accum.passCount += 1;
98
98
  }
99
99
 
100
100
  function finishOfficer(
@@ -4,6 +4,7 @@ import type {
4
4
  ComplianceDecision,
5
5
  } from "./compliance-transport.ts";
6
6
  import { GatekeeperDecisionError } from "./submission-errors.ts";
7
+ import type { SubmissionGateNonPassResult } from "./gatekeeper-role.ts";
7
8
 
8
9
  export const AUDIT_ESCALATION_KIND = "audit_escalation" as const;
9
10
 
@@ -37,7 +38,7 @@ export type AuditEscalationToolResult = {
37
38
 
38
39
  export type OfficerAuditEscalationDecision = {
39
40
  readonly status: "escalate";
40
- readonly officer?: "inspector" | "notary" | "countersign";
41
+ readonly officer?: "inspector" | "notary" | "auditor" | "countersign";
41
42
  readonly reason?: unknown;
42
43
  readonly findings?: unknown;
43
44
  readonly conflicts?: unknown;
@@ -113,6 +114,25 @@ export function projectAuditEscalation(
113
114
  };
114
115
  }
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
+
116
136
  /**
117
137
  * Discriminator-only recognition (ADR 0040). Shape of conflicts/options/gate
118
138
  * is not a reject gate — element types and cardinality are delivery content.
@@ -127,7 +147,7 @@ export function isAuditEscalationResult(
127
147
  }
128
148
 
129
149
  export type ComplianceDecisionHandlers<T> = {
130
- pass: (usage: Usage | undefined) => T | PromiseLike<T>;
150
+ converged: (usage: Usage | undefined) => T | PromiseLike<T>;
131
151
  /** Project the accepted parent candidate beside the typed audit-leg facts. */
132
152
  noReceipt?: (
133
153
  facts: Extract<ComplianceDecision, { status: "no-receipt" }>,
@@ -141,7 +161,7 @@ export type ComplianceDecisionHandlers<T> = {
141
161
  facts: Extract<ComplianceDecision, { status: "received" }>,
142
162
  usageProjection: { usage?: Usage },
143
163
  ) => T | PromiseLike<T>;
144
- bounce: (violations: readonly unknown[]) => T | PromiseLike<T>;
164
+ continue: (violations: readonly unknown[]) => T | PromiseLike<T>;
145
165
  escalate: (result: AuditEscalationToolResult) => T | PromiseLike<T>;
146
166
  /** Host failure beside any recorded auditor payloads — parent is not accepted. */
147
167
  transportFailure?: (
@@ -159,8 +179,8 @@ export async function disposeComplianceDecision<T>(
159
179
  deliveredOutput?: unknown,
160
180
  ): Promise<Awaited<T>> {
161
181
  switch (decision.status) {
162
- case "pass":
163
- return await handlers.pass(decision.usage);
182
+ case "converged":
183
+ return await handlers.converged(decision.usage);
164
184
  case "no-receipt":
165
185
  // The parent candidate remains accepted, but its parallel audit leg is a
166
186
  // public typed fact and must not be collapsed into an ordinary pass.
@@ -180,8 +200,8 @@ export async function disposeComplianceDecision<T>(
180
200
  decision,
181
201
  decision.usage === undefined ? {} : { usage: decision.usage },
182
202
  );
183
- case "bounce":
184
- return await handlers.bounce(decision.violations);
203
+ case "continue":
204
+ return await handlers.continue(decision.violations);
185
205
  case "escalate":
186
206
  return await handlers.escalate(
187
207
  projectAuditEscalation(decision, deliveredOutput),
@@ -15,7 +15,7 @@ export type AuditorRuntimeDependencies = {
15
15
  export const AUDITOR_TOOL_SPEC = {
16
16
  name: AUDITOR_OUTPUT_TOOL_NAME,
17
17
  label: "审刑院输出",
18
- description: "审刑院终局回执,状态为 pass、bounce 或 escalate。",
18
+ description: "审刑院终局回执,status 为 converged、continue 或 escalate。",
19
19
  promptSnippet: "审刑院终局回执",
20
20
  parameters: auditorOutputSchema,
21
21
  } as const;
@@ -11,7 +11,7 @@ import { readableGateItem } from "./readable-gate-item.ts";
11
11
  export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt"; usage?: Usage };
12
12
  /**
13
13
  * #757 / #750: no unreadable/unusable judgment on auditor replies.
14
- * Known three-state (pass/bounce/escalate) is read for queueing only.
14
+ * Known three-state (converged/continue/escalate) is read for queueing only.
15
15
  * Unknown accepted shape rides as `received` with the raw reply — parent stands,
16
16
  * no forged pass, no shape-death label. Resume-speaker for three pairs is #753/#756.
17
17
  */
@@ -29,8 +29,8 @@ export type ComplianceTransportFailure = {
29
29
  readonly usage?: Usage;
30
30
  };
31
31
  export type ComplianceDecision =
32
- | { status: "pass"; receipt?: unknown; usage?: Usage }
33
- | { status: "bounce"; violations: readonly unknown[]; receipt?: unknown; usage?: Usage }
32
+ | { status: "converged"; receipt?: unknown; usage?: Usage }
33
+ | { status: "continue"; violations: readonly unknown[]; receipt?: unknown; usage?: Usage }
34
34
  | { status: "escalate"; conflicts?: unknown; decisionGate?: unknown; receipt?: unknown; usage?: Usage }
35
35
  | ComplianceNoReceipt
36
36
  | ComplianceReceived
@@ -60,15 +60,15 @@ export type AuditorParentAttemptBinding = {
60
60
 
61
61
  function readListField(value: unknown): readonly unknown[] { return Array.isArray(value) ? value : value === undefined ? [] : [value]; }
62
62
 
63
- /** Try to project a known three-state compliance decision; undefined when not pass/bounce/escalate. */
63
+ /** Try to project a known three-state compliance decision; undefined when not converged/continue/escalate. */
64
64
  export function tryReadComplianceCandidate(arguments_: unknown, usage?: Usage): ComplianceDecision | undefined {
65
65
  if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
66
66
  return undefined;
67
67
  }
68
68
  const args = arguments_ as Record<string, unknown>;
69
69
  const status = args.status;
70
- if (status === "pass") return { status, receipt: arguments_, ...(usage === undefined ? {} : { usage }) };
71
- if (status === "bounce") return { status, violations: readListField(args.violations), receipt: arguments_, ...(usage === undefined ? {} : { usage }) };
70
+ if (status === "converged") return { status, receipt: arguments_, ...(usage === undefined ? {} : { usage }) };
71
+ if (status === "continue") return { status, violations: readListField(args.violations), receipt: arguments_, ...(usage === undefined ? {} : { usage }) };
72
72
  if (status === "escalate") {
73
73
  return {
74
74
  status,
@@ -129,7 +129,7 @@ async function usageFromSummonedSession(summoned: PublicSummonResult): Promise<U
129
129
 
130
130
  /**
131
131
  * Project a public auditor terminal onto the parent compliance decision.
132
- * Known pass/bounce/escalate/no-receipt flow through for queueing.
132
+ * Known converged/continue/escalate/no-receipt flow through for queueing.
133
133
  * Accepted-but-not-three-state → `received` with raw reply (#757) — no unreadable label,
134
134
  * never forged pass, never parent abort. Real provider/engine/disk failures stay loud.
135
135
  * Accepted audits always carry real session usage when present (#675 metering).
@@ -5,18 +5,20 @@
5
5
  * is defaulted, rewritten, or dropped (ADR 0055).
6
6
  */
7
7
 
8
- export const COUNTERSIGN_OUTPUT_TOOL_NAME = "ak_countersign_output";
8
+ import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
9
+
10
+ export const COUNTERSIGN_OUTPUT_TOOL_NAME = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
9
11
 
10
12
  export type CountersignVerdict =
11
- | { countersignStatus: "converged"; note?: string; evidence?: unknown }
13
+ | { status: "converged"; note?: string; evidence?: unknown }
12
14
  | {
13
- countersignStatus: "continue";
15
+ status: "continue";
14
16
  fix?: { summary: string };
15
17
  note?: string;
16
18
  evidence?: unknown;
17
19
  }
18
20
  | {
19
- countersignStatus: "escalate";
21
+ status: "escalate";
20
22
  decisionGate?: { question: string; options: string[] };
21
23
  note?: string;
22
24
  evidence?: unknown;
@@ -26,16 +28,16 @@ export function validateRecordedCountersignOutput(verdict: unknown): Countersign
26
28
  if (verdict === null || typeof verdict !== "object" || Array.isArray(verdict)) {
27
29
  throw new Error("Countersign verdict has no execution discriminator");
28
30
  }
29
- let countersignStatus: unknown;
31
+ let status: unknown;
30
32
  try {
31
- countersignStatus = (verdict as Record<string, unknown>).countersignStatus;
33
+ status = (verdict as Record<string, unknown>).status;
32
34
  } catch {
33
35
  throw new Error("Countersign verdict has no execution discriminator");
34
36
  }
35
- if (typeof countersignStatus !== "string") {
37
+ if (typeof status !== "string") {
36
38
  throw new Error("Countersign verdict has no execution discriminator");
37
39
  }
38
- if (["converged", "continue", "escalate"].includes(countersignStatus)) {
40
+ if (["converged", "continue", "escalate"].includes(status)) {
39
41
  return verdict as CountersignVerdict;
40
42
  }
41
43
  throw new Error("Countersign verdict has no execution discriminator");
@@ -1,7 +1,4 @@
1
- import type { Static } from "typebox";
2
- import { Type } from "typebox";
3
-
4
- import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
1
+ import { reviewSubmissionSchema } from "./review-submission.ts";
5
2
  import {
6
3
  COUNTERSIGN_OUTPUT_TOOL_NAME,
7
4
  validateRecordedCountersignOutput,
@@ -12,44 +9,18 @@ export { COUNTERSIGN_OUTPUT_TOOL_NAME } from "./countersign-contracts.ts";
12
9
  export type { CountersignVerdict };
13
10
 
14
11
  // #836 r16 class 1: fix/note/decisionGate are LLM/human-read narrative content —
15
- // no code branches on their length or nested presence. `countersignStatus`
16
- // alone is the machine discriminator the queue reads (src/role-runtime.ts:979-1025).
12
+ // no code branches on their length or nested presence. `status` alone is the
13
+ // machine discriminator the queue reads.
17
14
  // #836 (2026-09-11 御批 / ADR 0003 Amendment): registered-to-provider closed
18
- // value domains reject an unknown countersignStatus before the submission
15
+ // value domains reject an unknown status before the submission
19
16
  // ledger ever records it, defeating 读不出三态→resume 说话者本人. Kept open
20
17
  // (Type.Unknown) like every other gate-queue status field in this package
21
18
  // (notary/auditor/gatekeeper/navigator); src/role-runtime.ts:997-1025 still
22
19
  // classifies the recorded value and reasks the countersign itself when it
23
20
  // isn't one of the three states — code, not the transport, does that work.
24
21
  /** 给事中票庭审读五问的交卷形状(ADR 0074);形状指引,非 schema 闸。 */
25
- export const countersignVerdictSchema = withTerminatingOutputDeclarations(
26
- Type.Object(
27
- {
28
- countersignStatus: Type.Unknown({ description: "converged | continue | escalate。非三态时请重读后重交,勿改标。" }),
29
- fix: Type.Optional(
30
- Type.Object(
31
- { summary: Type.Optional(Type.String({ description: "退回摘要" })) },
32
- { additionalProperties: true, description: "退回说明" },
33
- ),
34
- ),
35
- note: Type.Optional(Type.String({ description: "附注" })),
36
- evidence: Type.Optional(Type.Unknown({ description: "留存证据" })),
37
- decisionGate: Type.Optional(
38
- Type.Object(
39
- {
40
- question: Type.Optional(Type.String()),
41
- options: Type.Optional(Type.Array(Type.String())),
42
- },
43
- { additionalProperties: true, description: "需陛下处置的问题与选项" },
44
- ),
45
- ),
46
- },
47
- { additionalProperties: true },
48
- ),
49
- );
50
- (countersignVerdictSchema as unknown as { required: string[] }).required = [];
51
-
52
- export type CountersignVerdictParameters = Static<typeof countersignVerdictSchema>;
22
+ export const countersignVerdictSchema = reviewSubmissionSchema;
23
+ export type CountersignVerdictParameters = import("./review-submission.ts").ReviewSubmission;
53
24
 
54
25
  export type CountersignRuntimeDependencies = {
55
26
  loadSoul(): Promise<string>;
@@ -20,10 +20,11 @@ function deriveSession(content: string, id: string): SessionDerivation {
20
20
  let accepted: Record<string, unknown> | undefined, observedCommit: string | undefined, turns = 0, calls = 0, tokens = 0;
21
21
  const statuses: DoctorCaseCost["statuses"] = [], commits: DoctorCaseCost["commits"] = [];
22
22
  for (const row of rows) {
23
+ if (row.type === "custom" && row.customType === "ak-role-submission-closure" && record(row.data) && row.data.isError === false && typeof row.data.toolName === "string" && isTerminatingToolName(row.data.toolName)) accepted = row;
23
24
  const message = record(row.message) ? row.message : undefined;
24
25
  if (message?.role === "assistant") { for (const part of Array.isArray(message.content) ? message.content : []) if (record(part) && part.type === "toolCall") calls++; if (typeof message.responseId === "string") { turns++; const usage = record(message.usage) ? message.usage : undefined; if (usage && typeof usage.output === "number") tokens += usage.output; } }
25
26
  if (message?.role === "toolResult" && message.isError !== true && typeof message.toolName === "string" && isTerminatingToolName(message.toolName) && record(message.details)) { let details; try { details = validateAcceptedDetails(message.toolName, message.details); } catch (error) { // Contract: README.md#Doctor — non-accepted terminating receipts are expected-negative evidence and are skipped; all other validation failures propagate with their cause.
26
- if (error instanceof AcceptedDetailsContractError) continue; throw error; } accepted = row; const facts = acceptedFacts(message.toolName, details); if (facts.commit && facts.commit !== observedCommit) { commits.push({ source: id, commit: facts.commit }); observedCommit = facts.commit; } /* #836 B10.3: do not wipe earlier statuses or invent a code status sentence */ if (facts.status !== undefined) { statuses.push({ source: id, status: facts.status }); } }
27
+ if (error instanceof AcceptedDetailsContractError) continue; throw error; } const facts = acceptedFacts(message.toolName, details); if (facts.commit && facts.commit !== observedCommit) { commits.push({ source: id, commit: facts.commit }); observedCommit = facts.commit; } /* #836 B10.3: do not wipe earlier statuses or invent a code status sentence */ if (facts.status !== undefined) { statuses.push({ source: id, status: facts.status }); } }
27
28
  }
28
29
  const acceptedAt = accepted && timestamp(accepted); const final = acceptedAt ? accepted! : rows.at(-1); const endedAt = final && timestamp(final); const wall = startedAt && endedAt ? Date.parse(endedAt) - Date.parse(startedAt) : undefined;
29
30
  if (wall !== undefined && wall < 0) degradationReasons.push(`non-monotonic session timestamps in ${id}`);
@@ -1,7 +1,6 @@
1
1
  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
- import { GatekeeperDecisionError } from "./submission-errors.ts";
5
4
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
6
5
  import { readableGateItem } from "./readable-gate-item.ts";
7
6
  import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
@@ -37,7 +36,7 @@ export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDe
37
36
  let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
38
37
  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) };
39
38
  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 }; } });
40
- 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, { pass: (usage) => ({ content: audit.status === "pass" && 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 重问说话者"); }, bounce: (violations) => { throw new GatekeeperDecisionError({ status: "bounce", officer: "auditor", receipt: audit.status === "bounce" ? audit.receipt ?? violations : violations }); }, 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); } });
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); } });
41
40
  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>` }; }); }
42
41
  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"); } };
43
42
  }
@@ -2,10 +2,11 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { HostContext } from "./host-contracts.ts";
3
3
 
4
4
  import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
5
- import { packagedGateDecision, packagedGateStageLabel } from "./packaged-role-registry.ts";
5
+ import { packagedGateStageLabel } from "./packaged-role-registry.ts";
6
6
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
7
7
  import { GatekeeperDecisionError } from "./submission-errors.ts";
8
8
  import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.ts";
9
+ import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
9
10
  import {
10
11
  GATEKEEPER_OUTPUT_TOOL_NAME,
11
12
  gatekeeperDecisionSchema,
@@ -15,7 +16,7 @@ import type { PublicSummonResult } from "./public-role-summons.ts";
15
16
  import type { TerminalResult } from "./public-cli/terminal.ts";
16
17
  import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
17
18
  export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
18
- export const NOTARY_OUTPUT_TOOL = "ak_notary_output";
19
+ export const NOTARY_OUTPUT_TOOL = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
19
20
 
20
21
  /** Gate review officers — 台院 / 符宝郎 / 审刑院 / 给事中 (#753 / #756 / #969). */
21
22
  export type GateOfficer = "inspector" | "notary" | "auditor" | "countersign";
@@ -38,14 +39,14 @@ export type GatekeeperSubject =
38
39
  */
39
40
  export type GatekeeperResult =
40
41
  | {
41
- readonly status: "pass";
42
+ readonly status: "converged";
42
43
  readonly officer: GateOfficer;
43
44
  readonly receipt: unknown;
44
45
  readonly runId?: string;
45
46
  }
46
- /** bounce | escalate: both return the officer receipt to the parent (#753 / #756). */
47
+ /** continue | escalate: both return the officer receipt to the parent (#753 / #756). */
47
48
  | {
48
- readonly status: "bounce";
49
+ readonly status: "continue";
49
50
  readonly officer: GateOfficer;
50
51
  readonly receipt: unknown;
51
52
  readonly runId?: string;
@@ -58,9 +59,9 @@ export type GatekeeperResult =
58
59
  }
59
60
  | {
60
61
  /**
61
- * Accepted reply whose conclusion is not pass|bounce|escalate.
62
+ * Accepted reply whose conclusion is not converged|continue|escalate.
62
63
  * Envelope resumes the officer with plain-language re-ask — never parent-stands,
63
- * never forges bounce (#753 / unreadable-conclusion-resume-speaker).
64
+ * never forges continue (#753 / unreadable-conclusion-resume-speaker).
64
65
  */
65
66
  readonly status: "needs_reask";
66
67
  readonly officer: GateOfficer;
@@ -77,9 +78,9 @@ export type GatekeeperResult =
77
78
  };
78
79
 
79
80
  /** Non-pass faces returned to the parent session (correctable; #836 never kill leg). */
80
- export type GatekeeperNonPassResult = Extract<
81
+ export type SubmissionGateNonPassResult = Extract<
81
82
  GatekeeperResult,
82
- { status: "bounce" | "escalate" | "no_receipt" | "transport_failure" }
83
+ { status: "continue" | "escalate" | "no_receipt" | "transport_failure" }
83
84
  >;
84
85
 
85
86
  function gateSeatLabel(stage: GateOfficer): string {
@@ -138,16 +139,16 @@ export type RunGatekeeperOptions = {
138
139
  readonly submission?: unknown;
139
140
  /**
140
141
  * Officer summon seam. Production default lives on the shared envelope
141
- * (`createDefaultGateOfficerSummon` / requireGatekeeperPass — ADR 0018).
142
+ * (`createDefaultGateOfficerSummon` / requireSubmissionGate — ADR 0018).
142
143
  * Role module only projects; callers must supply the summon.
143
144
  */
144
145
  readonly summonOfficer: GateOfficerSummon;
145
146
  };
146
147
 
147
- export type GatekeeperPassHostActions = {
148
+ export type SubmissionGateHostActions = {
148
149
  failInfrastructure(error: unknown, ctx: ExtensionContext | HostContext, toolCallId?: string): never;
149
150
  /** Envelope-owned execute→tool_result bridge (role-runtime); role module only throws typed error. */
150
- bindSubmissionNonPass(toolCallId: string, result: GatekeeperNonPassResult): void;
151
+ bindSubmissionNonPass(toolCallId: string, result: SubmissionGateNonPassResult): void;
151
152
  };
152
153
 
153
154
  /**
@@ -204,15 +205,15 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
204
205
 
205
206
  /**
206
207
  * Read only the conclusion field for queueing (#753).
207
- * pass | bounce | escalate → queue signal + raw receipt.
208
+ * converged | continue | escalate → queue signal + raw receipt.
208
209
  * Anything else accepted → needs_reask (resume speaker), never unreadable/parent-stand.
209
210
  * `fallbackStatus` is the terminal outcome.status when the receipt body has no status key
210
211
  * (keeps missing-args sentinel intact as the receipt).
211
212
  */
212
- /** Map 给事中 countersignStatus onto the shared gate queue words (#969). */
213
- function gateStatusFromCountersign(status: string): string | undefined {
214
- if (status === "converged") return "pass";
215
- if (status === "continue") return "bounce";
213
+ /** Map the shared review verdict onto the gate queue words (#1028). */
214
+ function reviewQueueStatus(status: string): string | undefined {
215
+ if (status === "converged") return "converged";
216
+ if (status === "continue") return "continue";
216
217
  if (status === "escalate") return "escalate";
217
218
  return undefined;
218
219
  }
@@ -224,34 +225,15 @@ function projectOfficerDecision(
224
225
  ): GatekeeperResult {
225
226
  const receipt = retainedReceipt(decision);
226
227
  const record = readRecord(decision);
227
- // 给事中: only countersignStatus, strict three-word map; never generic status,
228
- // never unmapped raw word, never fallbackStatus (#969 / ADR 0040/0055).
229
- if (packagedGateDecision(officer) === "countersign-status") {
230
- const countersignStatus =
231
- record !== undefined && typeof record.countersignStatus === "string"
232
- ? record.countersignStatus
233
- : undefined;
234
- const status =
235
- typeof countersignStatus === "string"
236
- ? gateStatusFromCountersign(countersignStatus)
237
- : undefined;
238
- if (status === "pass") {
239
- return { status: "pass", officer, receipt };
240
- }
241
- if (status === "bounce" || status === "escalate") {
242
- return { status, officer, receipt };
243
- }
244
- return { status: "needs_reask", officer, receipt };
245
- }
246
- // inspector / notary / auditor: shared gate words on generic status.
247
228
  const status =
248
229
  (record !== undefined && typeof record.status === "string" ? record.status : undefined)
249
230
  ?? fallbackStatus;
250
- if (status === "pass") {
251
- return { status: "pass", officer, receipt };
231
+ const queueStatus = typeof status === "string" ? reviewQueueStatus(status) : undefined;
232
+ if (queueStatus === "converged") {
233
+ return { status: "converged", officer, receipt };
252
234
  }
253
- if (status === "bounce" || status === "escalate") {
254
- return { status, officer, receipt };
235
+ if (queueStatus === "continue" || queueStatus === "escalate") {
236
+ return { status: queueStatus, officer, receipt };
255
237
  }
256
238
  return { status: "needs_reask", officer, receipt };
257
239
  }
@@ -316,8 +298,8 @@ function withOfficerRunId(
316
298
  summoned: PublicSummonResult,
317
299
  ): GatekeeperResult {
318
300
  if (
319
- result.status !== "pass"
320
- && result.status !== "bounce"
301
+ result.status !== "converged"
302
+ && result.status !== "continue"
321
303
  && result.status !== "escalate"
322
304
  && result.status !== "needs_reask"
323
305
  ) {
@@ -382,8 +364,8 @@ function projectOfficerTerminal(
382
364
  }
383
365
  if (outcome.kind === "accepted") {
384
366
  // outcome.status is the fixture/compat leaf: production settlement leaves
385
- // it undefined once payloads are recorded, so this only matters when a
386
- // caller still supplies status without any recorded payload (#836 hang).
367
+ // it undefined once payloads are recorded. It may interpret a receipt
368
+ // lacking its own status, never substitute for a missing receipt.
387
369
  return withOfficerRunId(
388
370
  projectOfficerPayloads(officer, thisCourt, outcome.status),
389
371
  summoned,
@@ -409,28 +391,21 @@ export type GatekeeperProjection = {
409
391
  };
410
392
 
411
393
  /**
412
- * Plain-language re-ask when the officer conclusion is not pass|bounce|escalate.
394
+ * Plain-language re-ask when the officer conclusion is not converged|continue|escalate.
413
395
  * Not a packaged engine handbook line (#755 exception for 读不出三态).
414
- * inspector / notary / auditor keep shared gate words; 给事中 uses own three-state
415
- * field and words so reask does not invite non-contract values (#969 / ADR 0055).
396
+ * Every review officer uses the same `status` field and three queue words (#1028).
416
397
  */
417
398
  export const OFFICER_CONCLUSION_REASK =
418
- "上次交卷的结论不是 pass、bounce、escalate 三态之一。请重新输出,结论字段写明其一;打回或上呈的话就是给对方看的原文。" as const;
419
-
420
- /** 给事中 re-ask: countersignStatus converged|continue|escalate only (#969). */
421
- export const COUNTERSIGN_CONCLUSION_REASK =
422
- "上次交卷的 countersignStatus 不是 converged、continue、escalate 三态之一。请重新输出,countersignStatus 写明其一;封驳或上呈的话就是给对方看的原文。" as const;
399
+ "上次交卷的 status 不是 converged、continue、escalate 三态之一。请重新输出,status 写明其一;continue 或 escalate 的话就是给对方看的原文。" as const;
423
400
 
424
401
  /** Pick the re-ask line for the officer under review. */
425
402
  export function officerConclusionReask(officer: GateOfficer): string {
426
- return packagedGateDecision(officer) === "countersign-status"
427
- ? COUNTERSIGN_CONCLUSION_REASK
428
- : OFFICER_CONCLUSION_REASK;
403
+ return OFFICER_CONCLUSION_REASK;
429
404
  }
430
405
 
431
406
  /**
432
407
  * Summon (via injected seam) + project. Default summonGateOfficer drive lives
433
- * on the shared envelope (`gatekeeper-pass-envelope.ts`, ADR 0018 / #675) —
408
+ * on the shared envelope (`submission-gate.ts`, ADR 0018 / #675) —
434
409
  * role module keeps projection only.
435
410
  */
436
411
  export async function projectGatekeeperRun(