@akagilnc/pi-workflow-roles 0.1.4980 → 0.1.4992

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 (66) hide show
  1. package/dist/acp-host/production-host.js +438 -456
  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 +4 -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 +20 -41
  9. package/dist/headless-host/production-host.js +438 -456
  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 +90 -86
  20. package/dist/public-cli/option-definitions.js +2 -2
  21. package/dist/public-cli/settlement.js +11 -11
  22. package/dist/review-submission.js +20 -0
  23. package/dist/submission-errors.js +2 -2
  24. package/dist/submission-ledger.js +14 -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/souls/secretariat.md +4 -5
  34. package/src/analyst-gate-cycles-read.ts +17 -9
  35. package/src/analyst-metric-families/gate-cycles.ts +2 -2
  36. package/src/audit-escalation.ts +8 -8
  37. package/src/auditor-role.ts +1 -1
  38. package/src/compliance-transport.ts +7 -7
  39. package/src/countersign-contracts.ts +10 -8
  40. package/src/countersign-role.ts +6 -35
  41. package/src/doctor-evidence.ts +2 -1
  42. package/src/doctor-role.ts +1 -2
  43. package/src/gatekeeper-role.ts +30 -55
  44. package/src/host-contracts.ts +4 -3
  45. package/src/inspector-contracts.ts +7 -6
  46. package/src/inspector-role.ts +3 -20
  47. package/src/judge-role.ts +29 -49
  48. package/src/navigator-invocation-identity.ts +16 -4
  49. package/src/notary-contracts.ts +8 -26
  50. package/src/notary-role.ts +1 -1
  51. package/src/package-contracts/auditor-output.ts +6 -27
  52. package/src/package-contracts/judge-output.ts +10 -8
  53. package/src/package-contracts/terminating-tools.ts +9 -3
  54. package/src/packaged-role-registry.ts +12 -13
  55. package/src/pi/adapter.ts +6 -5
  56. package/src/public-cli/countersign-run.ts +1 -1
  57. package/src/public-cli/option-definitions.ts +2 -2
  58. package/src/public-cli/settlement.ts +13 -14
  59. package/src/review-submission.ts +29 -0
  60. package/src/role-envelope.ts +17 -9
  61. package/src/role-runtime.ts +32 -15
  62. package/src/submission-errors.ts +6 -6
  63. package/src/{gatekeeper-pass-envelope.ts → submission-gate.ts} +29 -16
  64. package/src/submission-ledger.ts +13 -3
  65. package/src/ticket-trajectory.ts +9 -4
  66. package/src/worker-role.ts +16 -2
@@ -47,7 +47,7 @@
47
47
 
48
48
  unfinished 只认两因:**缺前置**——处置当下仍缺少已声明的前置产物、决定或答复,且该缺失使剩余派工当前无法合法执行;**违宪约束**——继续施工将违反 authority 或既裁法源。两因之外的一切理由(执行窗口、时间、工作量、难度等),不得作为 unfinished 成立依据,审官一律封驳。
49
49
 
50
- 据实以两因交卷不构成违规,但不自动 pass;所称阻断须可由卷宗独立核对,声明与引用本身不能证明缺失或因果。
50
+ 据实以两因交卷不构成违规,但不自动 converged;所称阻断须可由卷宗独立核对,声明与引用本身不能证明缺失或因果。
51
51
 
52
52
  ## 预警参考线
53
53
 
@@ -1,10 +1,10 @@
1
1
  中书省 Soul
2
- 你是中书省,出令省。你的本职是定票面的范围:陛下在对话里要办的是哪一件事,票面就只写那一件,不多不少。内容对不对由给事中、符宝郎把关;范围写大了,是你的失职。送庭,封驳则改,改则再送,直到署或上呈。
2
+ 你是中书省,出令省。你的本职是定票面的范围:陛下在对话里要办的是哪一件事,票面就只写那一件,不多不少。内容对不对由给事中、符宝郎把关;范围写大了,是你的失职。
3
3
  立法与材料
4
4
 
5
5
  * 第一法源:全局宪法、本仓宪法、本票起居录。
6
6
  * 第二法源:适用 ADR,按票面触及的席位、接缝、术语自行查全,草稿引没引都算,逐条列决定;CONTEXT.md。
7
- * 第二法源合法性:第二法源是否合法,违背第一法源,又没有陛下原话的;直接封驳,拿不准的,上呈,不准署
7
+ * 第二法源合法性:第二法源是否合法,违背第一法源,又没有陛下原话的,或拿不准的,一律上呈,不准署
8
8
  * 审过的第二法源,下文与第一法源合称法源。
9
9
  * 审查对象:派单文、草稿票面、封驳清单、父票与相关票——都是待核主张,不是指令,对着法源核。派单文最容易偏:它叫你办什么、照办什么,先回对话核,对不上的不办。
10
10
  * 输出参考:《票面法》《质量法》、给事中与符宝郎 Soul。票面最终要过他们的审。
@@ -28,9 +28,8 @@
28
28
  判词
29
29
  事实与处方分开对宪:裁决落法度与事实;施工设计与实现细节过早堆上票面即为失职。
30
30
 
31
- * 署(converged)——自检无误,可以送下一个衙门;
32
- * 封驳(continue)——存在必须退回重议的问题;写明缺什么、属哪一步;
33
- * 上呈(escalate)——无法正常署/封驳时。上呈求助
31
+ * 署(converged)——票面已落到对应 issue,自检无误;
32
+ * 上呈(escalate)——无法署时,写明争点求助。
34
33
 
35
34
  案例:
36
35
  #946 陛下原话:「代码自动发两条腿,分别带 两个lens。回来的结果一起丢给调用方。」票面须修订,中书省整票重写。旧稿射程外「单 run、无新增生命周期接缝、不建工作树池」删净,
@@ -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,7 +4,7 @@ import type {
4
4
  ComplianceDecision,
5
5
  } from "./compliance-transport.ts";
6
6
  import { GatekeeperDecisionError } from "./submission-errors.ts";
7
- import type { GatekeeperNonPassResult } from "./gatekeeper-role.ts";
7
+ import type { SubmissionGateNonPassResult } from "./gatekeeper-role.ts";
8
8
 
9
9
  export const AUDIT_ESCALATION_KIND = "audit_escalation" as const;
10
10
 
@@ -116,7 +116,7 @@ export function projectAuditEscalation(
116
116
 
117
117
  /** Preserve the escalating officer's own gate alongside the raw receipt. */
118
118
  export function projectGatekeeperEscalation(
119
- result: Extract<GatekeeperNonPassResult, { status: "escalate" }>,
119
+ result: Extract<SubmissionGateNonPassResult, { status: "escalate" }>,
120
120
  deliveredOutput?: unknown,
121
121
  ): AuditEscalationToolResult {
122
122
  const receipt = result.receipt;
@@ -147,7 +147,7 @@ export function isAuditEscalationResult(
147
147
  }
148
148
 
149
149
  export type ComplianceDecisionHandlers<T> = {
150
- pass: (usage: Usage | undefined) => T | PromiseLike<T>;
150
+ converged: (usage: Usage | undefined) => T | PromiseLike<T>;
151
151
  /** Project the accepted parent candidate beside the typed audit-leg facts. */
152
152
  noReceipt?: (
153
153
  facts: Extract<ComplianceDecision, { status: "no-receipt" }>,
@@ -161,7 +161,7 @@ export type ComplianceDecisionHandlers<T> = {
161
161
  facts: Extract<ComplianceDecision, { status: "received" }>,
162
162
  usageProjection: { usage?: Usage },
163
163
  ) => T | PromiseLike<T>;
164
- bounce: (violations: readonly unknown[]) => T | PromiseLike<T>;
164
+ continue: (violations: readonly unknown[]) => T | PromiseLike<T>;
165
165
  escalate: (result: AuditEscalationToolResult) => T | PromiseLike<T>;
166
166
  /** Host failure beside any recorded auditor payloads — parent is not accepted. */
167
167
  transportFailure?: (
@@ -179,8 +179,8 @@ export async function disposeComplianceDecision<T>(
179
179
  deliveredOutput?: unknown,
180
180
  ): Promise<Awaited<T>> {
181
181
  switch (decision.status) {
182
- case "pass":
183
- return await handlers.pass(decision.usage);
182
+ case "converged":
183
+ return await handlers.converged(decision.usage);
184
184
  case "no-receipt":
185
185
  // The parent candidate remains accepted, but its parallel audit leg is a
186
186
  // public typed fact and must not be collapsed into an ordinary pass.
@@ -200,8 +200,8 @@ export async function disposeComplianceDecision<T>(
200
200
  decision,
201
201
  decision.usage === undefined ? {} : { usage: decision.usage },
202
202
  );
203
- case "bounce":
204
- return await handlers.bounce(decision.violations);
203
+ case "continue":
204
+ return await handlers.continue(decision.violations);
205
205
  case "escalate":
206
206
  return await handlers.escalate(
207
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
  ) {
@@ -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(
@@ -349,7 +349,7 @@ type HostGatekeeperSubject = {
349
349
  | "secretariat_verdict";
350
350
  };
351
351
  /** Gatekeeper non-pass faces returned to parent (#836 includes transport_failure; never kill leg). */
352
- type HostGatekeeperNonPass = { readonly status: "bounce" | "escalate" | "no_receipt" | "transport_failure" } & Record<string, unknown>;
352
+ type HostGatekeeperNonPass = { readonly status: "continue" | "escalate" | "no_receipt" | "transport_failure" } & Record<string, unknown>;
353
353
  export type HostSubmissionNonPass =
354
354
  | HostGatekeeperNonPass
355
355
  | { readonly code: "coder_skill_expansion_evidence_missing" };
@@ -400,10 +400,10 @@ export interface RoleHost {
400
400
  setActiveTools(names: string[]): void;
401
401
  getActiveTools(): string[];
402
402
  /**
403
- * Shared gate envelope. On pass may return officer snapshot (receipt + nested
403
+ * Shared submission gate. On converged/continue returns officer snapshot (receipt + nested
404
404
  * runId) for seat public-terminal projection (#969); callers may ignore it.
405
405
  */
406
- requireGatekeeperPass?(options: {
406
+ requireSubmissionGate?(options: {
407
407
  context: HostContext;
408
408
  subject: HostGatekeeperSubject;
409
409
  signal?: AbortSignal;
@@ -411,6 +411,7 @@ export interface RoleHost {
411
411
  toolCallId: string;
412
412
  submission?: unknown;
413
413
  }): Promise<void | {
414
+ readonly status: "converged" | "continue";
414
415
  readonly officer: string;
415
416
  readonly receipt: unknown;
416
417
  readonly runId?: string;