@akagilnc/pi-workflow-roles 0.1.3685 → 0.1.3702

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 (54) hide show
  1. package/dist/acp-host/production-host.js +499 -559
  2. package/dist/analyst-gate-cycles-read.js +11 -1
  3. package/dist/archivist-record-entry.js +1 -1
  4. package/dist/auditor-dossier-tool.js +12 -0
  5. package/dist/dossier-resolution.js +0 -35
  6. package/dist/gatekeeper-pass-envelope.js +39 -34
  7. package/dist/gatekeeper-role.js +70 -77
  8. package/dist/judge-auditor.js +5 -24
  9. package/dist/judge-role.js +39 -42
  10. package/dist/notary-contracts.js +2 -17
  11. package/dist/notary-role.js +3 -6
  12. package/dist/package-contracts/judge-output.js +0 -2
  13. package/dist/package-contracts/terminating-tools.js +2 -2
  14. package/dist/public-cli/inspector-run.js +16 -2
  15. package/dist/public-cli/instruction-seat-run.js +14 -2
  16. package/dist/public-cli/main.js +189 -214
  17. package/dist/public-cli/notary-run.js +23 -20
  18. package/dist/public-cli/post-admission.js +30 -23
  19. package/dist/public-cli/run-lifecycle.js +6 -14
  20. package/dist/public-role-summons.js +38 -11
  21. package/dist/role-runtime.js +28 -3
  22. package/dist/submission-correctable-error.js +2 -1
  23. package/dist/submission-errors.js +26 -11
  24. package/dist/submission-ledger.js +7 -23
  25. package/extensions/role-runtime.ts +0 -2
  26. package/package.json +1 -1
  27. package/souls/countersign.md +12 -11
  28. package/souls/notary.md +5 -0
  29. package/src/acp-host/production-host.ts +1 -4
  30. package/src/acp-host/role-envelope.ts +1 -1
  31. package/src/analyst-gate-cycles-read.ts +10 -1
  32. package/src/archivist-record-entry.ts +7 -3
  33. package/src/auditor-dossier-tool.ts +21 -0
  34. package/src/dossier-resolution.ts +0 -39
  35. package/src/gatekeeper-pass-envelope.ts +59 -43
  36. package/src/gatekeeper-role.ts +114 -113
  37. package/src/host-contracts.ts +3 -3
  38. package/src/judge-auditor.ts +5 -43
  39. package/src/judge-role.ts +41 -54
  40. package/src/notary-contracts.ts +8 -27
  41. package/src/notary-role.ts +2 -7
  42. package/src/package-contracts/judge-output.ts +0 -1
  43. package/src/package-contracts/terminating-tools.ts +2 -2
  44. package/src/public-cli/inspector-run.ts +32 -2
  45. package/src/public-cli/instruction-seat-run.ts +30 -2
  46. package/src/public-cli/notary-run.ts +48 -16
  47. package/src/public-cli/post-admission.ts +31 -37
  48. package/src/public-cli/run-lifecycle.ts +6 -17
  49. package/src/public-role-summons.ts +76 -12
  50. package/src/role-runtime.ts +34 -9
  51. package/src/submission-correctable-error.ts +2 -0
  52. package/src/submission-errors.ts +28 -11
  53. package/src/submission-ledger.ts +8 -32
  54. package/dist/shape-unreadable-failure.js +0 -34
@@ -232,7 +232,7 @@ export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as c
232
232
  export type DirectOfficerRunPointer = {
233
233
  readonly version: 1;
234
234
  readonly kind: typeof DIRECT_OFFICER_RUN_POINTER_KIND;
235
- readonly officer: "inspector" | "notary";
235
+ readonly officer: "inspector" | "notary" | "auditor";
236
236
  /** Absolute path to the officer session.jsonl 正本. */
237
237
  readonly sessionFile: string;
238
238
  /** Officer run directory when known. */
@@ -243,10 +243,14 @@ export type DirectOfficerRunPointer = {
243
243
  * Book a typed pointer under parent session/auditor-roles (same nest owner as
244
244
  * createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
245
245
  * Directory placement stays with the archivist record entry (ADR 0018 / 0065).
246
+ *
247
+ * Stable leaf per officer under one parent (#753 gate-round accounting):
248
+ * same-parent re-summons upsert the same pointer instead of minting N files that
249
+ * each re-scan the full officer session and multiply terminal gate-round counts.
246
250
  */
247
251
  export function bookDirectOfficerRunPointer(options: {
248
252
  readonly parentSessionFile: string;
249
- readonly officer: "inspector" | "notary";
253
+ readonly officer: "inspector" | "notary" | "auditor";
250
254
  readonly sessionFile: string;
251
255
  readonly runDirectory?: string;
252
256
  }): DirectOfficerRunPointer {
@@ -262,7 +266,7 @@ export function bookDirectOfficerRunPointer(options: {
262
266
  : {}),
263
267
  };
264
268
  writeFileSync(
265
- join(nest, `${options.officer}-${Date.now().toString(36)}.pointer.json`),
269
+ join(nest, `${options.officer}.pointer.json`),
266
270
  `${JSON.stringify(pointer)}\n`,
267
271
  "utf8",
268
272
  );
@@ -62,6 +62,27 @@ export function gateSubmissionCandidatePath(runDirectory: string): string {
62
62
  return join(runDirectory, "artifacts", GATE_SUBMISSION_CANDIDATE_FILE);
63
63
  }
64
64
 
65
+ /**
66
+ * Human-readable materials for gate-officer same-parent resume (#753 / #750).
67
+ * Pointers only — no body embed (ADR 0079/0081); no duty/handbook lines (#755).
68
+ * Three pairs share this face: countersign↔notary, judge↔auditor, worker↔inspector.
69
+ * Code delivers the pointers; it does not judge whether the officer read them.
70
+ */
71
+ export function buildGateOfficerReviewInstruction(input: {
72
+ readonly sourceRunDirectory: string;
73
+ readonly submissionCandidatePath?: string;
74
+ }): string {
75
+ const lines = [
76
+ "本轮父席交卷待审。",
77
+ `来源 run:${input.sourceRunDirectory}`,
78
+ ];
79
+ const candidate = input.submissionCandidatePath?.trim();
80
+ if (candidate !== undefined && candidate.length > 0) {
81
+ lines.push(`交卷候选(冻结快照):${candidate}`);
82
+ }
83
+ return lines.join("\n");
84
+ }
85
+
65
86
  /**
66
87
  * Persist the in-flight tool-call leaf as a run-directory artifact so pointer-only
67
88
  * officer summons resolve on hosts whose session.jsonl is header-only (#632 / DK-4).
@@ -5,9 +5,6 @@
5
5
  import { existsSync, statSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
 
8
- import { JUDGE_OUTPUT_TOOL_NAME as JUDGE_OUTPUT_TOOL } from "./package-contracts/judge-output.ts";
9
-
10
- export const JUDGE_OUTPUT_TOOL_NAME = JUDGE_OUTPUT_TOOL;
11
8
  export const AUDIT_RUN_DIR_ENV = "AK_ROLE_RUN_DIR" as const;
12
9
  export const DOCTOR_CANDIDATE_ENTRY_TYPE = "ak_doctor_audit_candidate" as const;
13
10
 
@@ -65,44 +62,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
65
62
  return typeof value === "object" && value !== null && !Array.isArray(value);
66
63
  }
67
64
 
68
- /**
69
- * Judge subjects must already be on the parent session books before audit starts:
70
- * assignment (user message) + candidate verdict (sole judge output tool call).
71
- */
72
65
  type AuditSubjectContext = { sessionManager: { getEntries?(): Iterable<unknown> } };
73
66
 
74
- export function readJudgeAuditSubjects(context: AuditSubjectContext): SubjectResolution {
75
- const entries = context.sessionManager.getEntries?.() ?? [];
76
- let hasAssignment = false;
77
- let hasCandidate = false;
78
- for (const entry of entries) {
79
- if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) continue;
80
- const message = entry.message;
81
- if (message.role === "user") {
82
- const text = typeof message.content === "string"
83
- ? message.content
84
- : Array.isArray(message.content)
85
- ? message.content.map((part) => (isRecord(part) && part.type === "text" && typeof part.text === "string" ? part.text : "")).join("")
86
- : "";
87
- if (text.trim().length > 0) hasAssignment = true;
88
- }
89
- if (message.role === "assistant" && Array.isArray(message.content)) {
90
- for (const part of message.content) {
91
- if (isRecord(part) && part.type === "toolCall" && part.name === JUDGE_OUTPUT_TOOL_NAME && isRecord(part.arguments)) {
92
- hasCandidate = true;
93
- }
94
- }
95
- }
96
- }
97
- if (!hasAssignment) {
98
- return { status: "incomplete", observation: { kind: "missing-subject", subject: "assignment" } };
99
- }
100
- if (!hasCandidate) {
101
- return { status: "incomplete", observation: { kind: "missing-subject", subject: "candidate-verdict" } };
102
- }
103
- return { status: "ok" };
104
- }
105
-
106
67
  /**
107
68
  * Doctor candidate testimony must be recorded before audit.
108
69
  */
@@ -1,18 +1,28 @@
1
1
  /**
2
- * Shared submit-path envelope for 门下省 gates (ADR 0018 / #675).
3
- * Owns officer-pointer book + host abort/non-pass faces.
2
+ * Shared submit-path envelope for 门下省 gates (ADR 0018 / #675 / #753).
3
+ * Owns officer-pointer book + host abort/non-pass faces + review queue loop.
4
4
  * Role modules only project via projectGatekeeperRun / runGatekeeper — no book, no catch.
5
+ *
6
+ * Queue guarantee only (#753 / #756 / #750):
7
+ * parent submit → summon officer → read conclusion field
8
+ * pass → accept end
9
+ * bounce | escalate → raw officer receipt as tool result back to parent
10
+ * not three-state → resume officer with plain-language re-ask (no round cap)
11
+ * transport / no_receipt → present honestly
12
+ * Three pairs: countersign↔notary, judge↔auditor, worker↔inspector.
13
+ * Code does not judge content, map next-step for parent, or label unreadable/unusable.
5
14
  */
6
15
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
16
  import { bookDirectOfficerRunPointer } from "./archivist-record-entry.ts";
8
17
  import type { HostContext } from "./host-contracts.ts";
9
18
  import {
10
19
  GatekeeperDecisionError,
11
- GatekeeperEscalationError,
20
+ OFFICER_CONCLUSION_REASK,
12
21
  projectGatekeeperRun,
13
22
  type GatekeeperPassHostActions,
14
23
  type GatekeeperResult,
15
24
  type GatekeeperSubject,
25
+ type GateOfficer,
16
26
  type GateOfficerSummon,
17
27
  } from "./gatekeeper-role.ts";
18
28
  import type { PublicSummonResult } from "./public-role-summons.ts";
@@ -24,7 +34,7 @@ import { sessionFileFromPublicSummon } from "./session-assistant-usage.ts";
24
34
  */
25
35
  function bookDirectOfficerPointer(
26
36
  context: ExtensionContext | HostContext,
27
- officer: "inspector" | "notary",
37
+ officer: GateOfficer,
28
38
  result: GatekeeperResult,
29
39
  summoned: PublicSummonResult,
30
40
  ): void {
@@ -32,7 +42,7 @@ function bookDirectOfficerPointer(
32
42
  result.status !== "pass"
33
43
  && result.status !== "bounce"
34
44
  && result.status !== "escalate"
35
- && result.status !== "unreadable"
45
+ && result.status !== "needs_reask"
36
46
  ) {
37
47
  return;
38
48
  }
@@ -55,7 +65,7 @@ function bookDirectOfficerPointer(
55
65
 
56
66
  /**
57
67
  * Shared envelope: project gate, book officer pointer, map onto host actions.
58
- * unreadable = parent stands (ADR 0055); never mechanical NonPass reject.
68
+ * Review loop has no round cap (#753 no-round-cap).
59
69
  */
60
70
  export async function requireGatekeeperPass(options: {
61
71
  readonly context: ExtensionContext | HostContext;
@@ -66,44 +76,50 @@ export async function requireGatekeeperPass(options: {
66
76
  /** Lowest seam: same as runGatekeeper options.summonOfficer — offline tracers only. */
67
77
  readonly summonOfficer?: GateOfficerSummon;
68
78
  }): Promise<void> {
69
- const projected = await projectGatekeeperRun({
70
- context: options.context,
71
- subject: options.subject,
72
- ...(options.signal === undefined ? {} : { signal: options.signal }),
73
- ...(options.summonOfficer === undefined ? {} : { summonOfficer: options.summonOfficer }),
74
- });
75
- const gatekeeper = projected.result;
76
- // Envelope-owned pointer book. Failure is host infrastructure — single face.
77
- if (projected.summoned !== undefined) {
78
- try {
79
- bookDirectOfficerPointer(
80
- options.context,
81
- projected.officer,
82
- gatekeeper,
83
- projected.summoned,
84
- );
85
- } catch (error) {
79
+ let reask: string | undefined;
80
+ // No round cap — end only on pass, bounce/escalate-to-parent, or real failure.
81
+ for (;;) {
82
+ const projected = await projectGatekeeperRun({
83
+ context: options.context,
84
+ subject: options.subject,
85
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
86
+ ...(options.summonOfficer === undefined ? {} : { summonOfficer: options.summonOfficer }),
87
+ ...(reask === undefined ? {} : { reask }),
88
+ });
89
+ const gatekeeper = projected.result;
90
+ // Envelope-owned pointer book. Failure is host infrastructure — single face.
91
+ if (projected.summoned !== undefined) {
92
+ try {
93
+ bookDirectOfficerPointer(
94
+ options.context,
95
+ projected.officer,
96
+ gatekeeper,
97
+ projected.summoned,
98
+ );
99
+ } catch (error) {
100
+ options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
101
+ }
102
+ }
103
+ if (gatekeeper.status === "pass") return;
104
+ if (gatekeeper.status === "needs_reask") {
105
+ // Resume the speaker with plain-language re-ask — never bounce the parent (#753).
106
+ reask = OFFICER_CONCLUSION_REASK;
107
+ continue;
108
+ }
109
+ if (gatekeeper.status === "transport_failure") {
110
+ const error = new Error(`交卷闸 transport_failure(${gatekeeper.stage}):${gatekeeper.reason}`) as Error & {
111
+ stage: typeof gatekeeper.stage;
112
+ reason: string;
113
+ submission?: unknown;
114
+ };
115
+ error.stage = gatekeeper.stage;
116
+ error.reason = gatekeeper.reason;
117
+ if (gatekeeper.submission !== undefined) error.submission = gatekeeper.submission;
86
118
  options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
87
119
  }
120
+ // bounce | escalate | no_receipt: raw receipt (or lifecycle fact) back to parent.
121
+ // escalate is NOT thrown as parent next-step — parent reads the officer words (#753).
122
+ options.hostActions.bindSubmissionNonPass(options.toolCallId, gatekeeper);
123
+ throw new GatekeeperDecisionError(gatekeeper);
88
124
  }
89
- if (gatekeeper.status === "pass") return;
90
- // ADR 0055 / #675: shape-unreadable officer output must not mechanically reject parent.
91
- if (gatekeeper.status === "unreadable") return;
92
- if (gatekeeper.status === "transport_failure") {
93
- const error = new Error(`交卷闸 transport_failure(${gatekeeper.stage}):${gatekeeper.reason}`) as Error & {
94
- stage: typeof gatekeeper.stage;
95
- reason: string;
96
- submission?: unknown;
97
- };
98
- error.stage = gatekeeper.stage;
99
- error.reason = gatekeeper.reason;
100
- if (gatekeeper.submission !== undefined) error.submission = gatekeeper.submission;
101
- options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
102
- }
103
- if (gatekeeper.status === "escalate") {
104
- throw new GatekeeperEscalationError(gatekeeper);
105
- }
106
- // bounce / no_receipt: typed non-pass — envelope owns execute→tool_result bridge.
107
- options.hostActions.bindSubmissionNonPass(options.toolCallId, gatekeeper);
108
- throw new GatekeeperDecisionError(gatekeeper);
109
125
  }
@@ -7,7 +7,6 @@ import {
7
7
  } from "./auditor-dossier-tool.ts";
8
8
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
9
9
  import { GatekeeperDecisionError } from "./submission-errors.ts";
10
- import { readableGateItem } from "./readable-gate-item.ts";
11
10
  import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.ts";
12
11
  import {
13
12
  GATEKEEPER_OUTPUT_TOOL_NAME,
@@ -16,76 +15,80 @@ import {
16
15
  } from "./package-contracts/gatekeeper-output.ts";
17
16
  import type { PublicSummonResult } from "./public-role-summons.ts";
18
17
  import type { TerminalResult } from "./public-cli/terminal.ts";
19
- import { retainedShapeUnreadable } from "./shape-unreadable-failure.ts";
20
18
  export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
21
19
  export const NOTARY_OUTPUT_TOOL = "ak_notary_output";
22
20
 
21
+ /** Gate review officers — 察院 / 符宝郎 / 审刑院 (#753 / #756). */
22
+ export type GateOfficer = "inspector" | "notary" | "auditor";
23
+
23
24
  /** Officer routing only — content is self-fetched via the shared run-dossier tool (#632). */
24
25
  export type GatekeeperSubject =
25
26
  | { readonly kind: "worker_completion" }
26
27
  | { readonly kind: "judge_draft" }
28
+ | { readonly kind: "judge_compliance" }
27
29
  | { readonly kind: "countersign_verdict" };
28
30
 
31
+ /**
32
+ * Gate projection for the review queue (#753 / #750).
33
+ * Code only reads the conclusion field for queueing. Officer words ride as
34
+ * `receipt` unchanged — no findings rewrite, no unreadable/unusable label,
35
+ * no next-step selection for the parent.
36
+ */
29
37
  export type GatekeeperResult =
30
- /**
31
- * Lawful direct-officer release.
32
- */
33
- | { readonly status: "pass"; readonly officer: "inspector" | "notary"; readonly findings: readonly string[] }
34
- | {
35
- readonly status: "bounce";
36
- readonly officer: "inspector" | "notary";
37
- readonly disposition: "rewrite";
38
- readonly findings: readonly string[];
39
- readonly submission: unknown;
40
- }
38
+ | { readonly status: "pass"; readonly officer: GateOfficer; readonly receipt: unknown }
39
+ /** bounce | escalate: both return the officer receipt to the parent (#753 / #756). */
40
+ | { readonly status: "bounce"; readonly officer: GateOfficer; readonly receipt: unknown }
41
+ | { readonly status: "escalate"; readonly officer: GateOfficer; readonly receipt: unknown }
41
42
  | {
42
- readonly status: "escalate";
43
- readonly officer: "inspector" | "notary";
44
- readonly reason?: unknown;
45
- readonly findings: unknown;
46
- readonly submission: unknown;
47
- }
48
- | { readonly status: "no_receipt"; readonly stage: "inspector" | "notary"; readonly reason: string; readonly facts: NoReceiptLifecycleFacts }
49
- | {
50
- /** Shape-unreadable officer output — typed fact, not a forged bounce (ADR 0055 / §0). */
51
- readonly status: "unreadable";
52
- readonly officer: "inspector" | "notary";
53
- readonly reason: string;
54
- readonly submission: unknown;
43
+ /**
44
+ * Accepted reply whose conclusion is not pass|bounce|escalate.
45
+ * Envelope resumes the officer with plain-language re-ask — never parent-stands,
46
+ * never forges bounce (#753 / unreadable-conclusion-resume-speaker).
47
+ */
48
+ readonly status: "needs_reask";
49
+ readonly officer: GateOfficer;
50
+ readonly receipt: unknown;
55
51
  }
52
+ | { readonly status: "no_receipt"; readonly stage: GateOfficer; readonly reason: string; readonly facts: NoReceiptLifecycleFacts }
56
53
  | {
57
54
  readonly status: "transport_failure";
58
- readonly stage: "inspector" | "notary";
55
+ readonly stage: GateOfficer;
59
56
  readonly reason: string;
60
- /** Original unusable submission retained for the failure channel. */
57
+ /** Original transport/process failure payload retained for the failure channel. */
61
58
  readonly submission?: unknown;
62
59
  };
63
60
 
61
+ /** Non-pass faces that bounce the parent session (correctable). */
64
62
  export type GatekeeperNonPassResult = Extract<
65
63
  GatekeeperResult,
66
- { status: "bounce" | "no_receipt" | "unreadable" }
64
+ { status: "bounce" | "escalate" | "no_receipt" }
67
65
  >;
68
66
 
69
- function gateSeatLabel(stage: "inspector" | "notary"): string {
70
- return stage === "inspector" ? "察院" : "符宝郎";
67
+ function gateSeatLabel(stage: GateOfficer): string {
68
+ if (stage === "inspector") return "察院";
69
+ if (stage === "auditor") return "审刑院";
70
+ return "符宝郎";
71
71
  }
72
72
 
73
- export { GatekeeperDecisionError } from "./submission-errors.ts";
74
-
75
- export class GatekeeperEscalationError extends Error {
76
- readonly gatekeeper: Extract<GatekeeperResult, { status: "escalate" }>;
77
- constructor(gatekeeper: Extract<GatekeeperResult, { status: "escalate" }>) {
78
- super(`门下省${gateSeatLabel(gatekeeper.officer)}上呈`);
79
- this.name = "GatekeeperEscalationError";
80
- this.gatekeeper = gatekeeper;
81
- }
73
+ /** Subject kind → review officer (#753 countersign/notary, #756 judge/auditor + worker/inspector). */
74
+ export function gateOfficerForSubject(subject: GatekeeperSubject): GateOfficer {
75
+ if (subject.kind === "worker_completion") return "inspector";
76
+ if (subject.kind === "judge_compliance") return "auditor";
77
+ return "notary";
82
78
  }
83
79
 
80
+ export { GatekeeperDecisionError } from "./submission-errors.ts";
81
+
84
82
  export type GateOfficerSummon = (
85
- officer: "inspector" | "notary",
83
+ officer: GateOfficer,
86
84
  sourceRunDirectory: string,
87
85
  /** Parent cancellation forwarded to the nested activation (#675). */
88
86
  signal?: AbortSignal,
87
+ /**
88
+ * Plain-language re-ask when the prior officer reply was not a three-state
89
+ * conclusion (#753 / #756). Hosted as same-ticket resume instruction.
90
+ */
91
+ reask?: string,
89
92
  ) => Promise<PublicSummonResult>;
90
93
 
91
94
  export type RunGatekeeperOptions = {
@@ -94,6 +97,10 @@ export type RunGatekeeperOptions = {
94
97
  readonly signal?: AbortSignal;
95
98
  /** Run directory of the parent role (pointer-only summons, ADR 0079). */
96
99
  readonly runDirectory?: string;
100
+ /**
101
+ * Plain-language re-ask for this summon (resume speaker after non-three-state).
102
+ */
103
+ readonly reask?: string;
97
104
  /**
98
105
  * Test seam for public-role summons. Production calls the shared public
99
106
  * activation path (#675); inject only in offline tracers.
@@ -145,13 +152,6 @@ function failureReason(error: unknown): string {
145
152
  return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
146
153
  }
147
154
 
148
- function asStringArray(value: unknown): readonly string[] {
149
- if (!Array.isArray(value)) return [];
150
- // Officer findings may be structured objects (category/law/evidence); the
151
- // parent role must see them verbatim, not an empty list (#750 / #775).
152
- return value.map(readableGateItem);
153
- }
154
-
155
155
  /** Serializable stand-in when the child tool call had no arguments object. */
156
156
  export const MISSING_ARGUMENTS_SUBMISSION = Object.freeze({ missing: "arguments" as const });
157
157
 
@@ -160,7 +160,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
160
160
  }
161
161
 
162
162
  /** Keep original decision bytes for the next reader; undefined becomes a serializable missing-args fact. */
163
- function retainedSubmission(decision: unknown): unknown {
163
+ function retainedReceipt(decision: unknown): unknown {
164
164
  // undefined must not be stored: JSON drops it and the missing-args fact vanishes.
165
165
  // Through the real provider adapter an undefined root argument arrives as an
166
166
  // empty object after serialization; that must also project a missing-args fact.
@@ -169,66 +169,44 @@ function retainedSubmission(decision: unknown): unknown {
169
169
  : decision;
170
170
  }
171
171
 
172
- /**
173
- * Shape-unreadable officer decision — retain original candidate + typed reason.
174
- * Not a forged bounce, not transport abort (CLAUDE.md §0 / ADR 0055).
175
- * Real provider/engine/disk failures stay transport_failure elsewhere.
176
- */
177
- function shapeUnreadable(
178
- officer: "inspector" | "notary",
179
- decision: unknown,
180
- reason = "decision 无显式 pass/bounce/escalate",
181
- ): Extract<GatekeeperResult, { status: "unreadable" }> {
182
- return {
183
- status: "unreadable",
184
- officer,
185
- reason,
186
- submission: retainedSubmission(decision),
187
- };
188
- }
189
-
190
172
  function readRecord(value: unknown): Record<string, unknown> | undefined {
191
173
  if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
192
174
  return value as Record<string, unknown>;
193
175
  }
194
176
 
177
+ /**
178
+ * Read only the conclusion field for queueing (#753).
179
+ * pass | bounce | escalate → queue signal + raw receipt.
180
+ * Anything else accepted → needs_reask (resume speaker), never unreadable/parent-stand.
181
+ * `fallbackStatus` is the terminal outcome.status when the receipt body has no status key
182
+ * (keeps missing-args sentinel intact as the receipt).
183
+ */
195
184
  function projectOfficerDecision(
196
- officer: "inspector" | "notary",
185
+ officer: GateOfficer,
197
186
  decision: unknown,
187
+ fallbackStatus?: string,
198
188
  ): GatekeeperResult {
189
+ const receipt = retainedReceipt(decision);
199
190
  const record = readRecord(decision);
200
- if (record === undefined) return shapeUnreadable(officer, decision);
201
- if (record.status === "bounce") {
202
- return {
203
- status: "bounce",
204
- officer,
205
- disposition: "rewrite",
206
- findings: asStringArray(record.findings),
207
- submission: retainedSubmission(decision),
208
- };
209
- }
210
- if (record.status === "pass") {
211
- return {
212
- status: "pass",
213
- officer,
214
- findings: asStringArray(record.findings),
215
- };
191
+ const status =
192
+ (record !== undefined && typeof record.status === "string" ? record.status : undefined)
193
+ ?? fallbackStatus;
194
+ if (status === "pass") {
195
+ return { status: "pass", officer, receipt };
216
196
  }
217
- if (record.status === "escalate") {
218
- return {
219
- status: "escalate",
220
- officer,
221
- ...(Object.hasOwn(record, "reason") ? { reason: record.reason } : {}),
222
- findings: record.findings,
223
- submission: retainedSubmission(decision),
224
- };
197
+ if (status === "bounce" || status === "escalate") {
198
+ return { status, officer, receipt };
225
199
  }
226
- return shapeUnreadable(officer, decision);
200
+ return { status: "needs_reask", officer, receipt };
227
201
  }
228
202
 
229
- /** Map a public-role terminal onto the gate officer result surface. */
203
+ /**
204
+ * Project a public-role terminal onto the gate queue surface.
205
+ * Lifecycle facts (no_receipt / transport) stay loud; conclusion reads only
206
+ * the status field. No unusable/unreadable judgment (#753).
207
+ */
230
208
  function projectOfficerTerminal(
231
- officer: "inspector" | "notary",
209
+ officer: GateOfficer,
232
210
  summoned: PublicSummonResult,
233
211
  ): GatekeeperResult {
234
212
  const terminal: TerminalResult | undefined = summoned.terminal;
@@ -253,11 +231,7 @@ function projectOfficerTerminal(
253
231
  };
254
232
  }
255
233
  if (outcome.kind === "failure") {
256
- // Single settlement marker only (ADR 0055 / #675) — no cause=output re-derivation.
257
- const shape = retainedShapeUnreadable(outcome.decisiveFacts);
258
- if (shape !== undefined) {
259
- return shapeUnreadable(officer, shape.candidate, outcome.diagnostic);
260
- }
234
+ // Real provider/engine/disk failure — keep loud. Not a shape judgment.
261
235
  return {
262
236
  status: "transport_failure",
263
237
  stage: officer,
@@ -265,28 +239,47 @@ function projectOfficerTerminal(
265
239
  submission: outcome.decisiveFacts,
266
240
  };
267
241
  }
242
+ if (outcome.kind === "audit_escalation") {
243
+ // Residual/compliance escalate face: queue as escalate, receipt as written.
244
+ // Nested officer escalate itself seals accepted+status escalate (no rewrite).
245
+ return {
246
+ status: "escalate",
247
+ officer,
248
+ receipt: retainedReceipt(outcome.decisiveFacts),
249
+ };
250
+ }
268
251
  if (outcome.kind === "accepted") {
269
- return projectOfficerDecision(officer, {
270
- status: outcome.status,
271
- ...outcome.decisiveFacts,
272
- });
252
+ // Prefer the officer's own decisiveFacts as the receipt body. outcome.status is
253
+ // only a fallback when facts have no status key (missing-args sentinel stays intact).
254
+ const facts = outcome.decisiveFacts;
255
+ if (isRecord(facts) && Object.keys(facts).length > 0) {
256
+ return projectOfficerDecision(officer, facts, outcome.status);
257
+ }
258
+ return projectOfficerDecision(officer, { status: outcome.status });
273
259
  }
260
+ // Unknown terminal kind: still not a shape judgment — ask the speaker again.
274
261
  return {
275
- status: "transport_failure",
276
- stage: officer,
277
- reason: `${gateSeatLabel(officer)} public summon returned unusable terminal kind`,
278
- submission: outcome,
262
+ status: "needs_reask",
263
+ officer,
264
+ receipt: retainedReceipt(outcome),
279
265
  };
280
266
  }
281
267
 
282
268
  /** Projection carrier for the shared submit envelope (ADR 0018). No lifecycle book here. */
283
269
  export type GatekeeperProjection = {
284
- readonly officer: "inspector" | "notary";
270
+ readonly officer: GateOfficer;
285
271
  readonly result: GatekeeperResult;
286
272
  /** Present only when a public summon actually returned (not transport pre-summon failure). */
287
273
  readonly summoned?: PublicSummonResult;
288
274
  };
289
275
 
276
+ /**
277
+ * Plain-language re-ask when the officer conclusion is not pass|bounce|escalate.
278
+ * Not a packaged engine handbook line (#755 exception for 读不出三态).
279
+ */
280
+ export const OFFICER_CONCLUSION_REASK =
281
+ "上次交卷的结论不是 pass、bounce、escalate 三态之一。请重新输出,结论字段写明其一;打回或上呈的话就是给对方看的原文。" as const;
282
+
290
283
  /**
291
284
  * Summon + project only. Lifecycle book and host abort face live on the shared
292
285
  * submit envelope (`gatekeeper-pass-envelope.ts`, ADR 0018 / #675).
@@ -294,7 +287,7 @@ export type GatekeeperProjection = {
294
287
  export async function projectGatekeeperRun(
295
288
  options: RunGatekeeperOptions,
296
289
  ): Promise<GatekeeperProjection> {
297
- const officer = options.subject.kind === "worker_completion" ? "inspector" : "notary";
290
+ const officer = gateOfficerForSubject(options.subject);
298
291
  const runDirectory = options.runDirectory ?? auditorRunDirectory(options.context);
299
292
  if (runDirectory === undefined) {
300
293
  return {
@@ -308,21 +301,29 @@ export async function projectGatekeeperRun(
308
301
  }
309
302
  // Pointer-only summons need a resolvable leaf: Grok session.jsonl is header-only
310
303
  // (#617 DK-4); write the in-memory tool-call candidate as a run artifact first (#632).
311
- persistGateSubmissionCandidate(runDirectory, options.context);
304
+ // Candidate path also rides same-parent officer resume as 人读材料 (#753 / #750).
305
+ const submissionCandidatePath = persistGateSubmissionCandidate(
306
+ runDirectory,
307
+ options.context,
308
+ );
312
309
  let summoned: PublicSummonResult;
313
310
  try {
314
311
  const summon =
315
312
  options.summonOfficer
316
- ?? (async (nextOfficer, sourceRunDirectory, officerSignal) => {
313
+ ?? (async (nextOfficer, sourceRunDirectory, officerSignal, reask) => {
317
314
  const { summonGateOfficer } = await import("./public-role-summons.ts");
318
315
  return summonGateOfficer({
319
316
  officer: nextOfficer,
320
317
  sourceRunDirectory,
321
318
  cwd: options.context.cwd ?? process.cwd(),
322
319
  ...(officerSignal === undefined ? {} : { signal: officerSignal }),
320
+ ...(reask === undefined ? {} : { reask }),
321
+ ...(submissionCandidatePath === undefined
322
+ ? {}
323
+ : { submissionCandidatePath }),
323
324
  });
324
325
  });
325
- summoned = await summon(officer, runDirectory, options.signal);
326
+ summoned = await summon(officer, runDirectory, options.signal, options.reask);
326
327
  } catch (error) {
327
328
  return {
328
329
  officer,
@@ -299,10 +299,10 @@ type HostEventHandler<K extends keyof HostEventMap> = (event: HostEventMap[K], c
299
299
  export type HostEventRegistration = { [K in keyof HostEventMap]: [event: K, handler: HostEventHandler<K>] }[keyof HostEventMap];
300
300
 
301
301
  type HostGatekeeperSubject = {
302
- readonly kind: "worker_completion" | "judge_draft" | "countersign_verdict";
302
+ readonly kind: "worker_completion" | "judge_draft" | "judge_compliance" | "countersign_verdict";
303
303
  };
304
- /** Gatekeeper bounce/no_receipt plus other correct submission rejects share one projection map. */
305
- type HostGatekeeperNonPass = { readonly status: "bounce" | "no_receipt" | "unreadable" } & Record<string, unknown>;
304
+ /** Gatekeeper bounce/escalate/no_receipt plus other correct submission rejects share one projection map. */
305
+ type HostGatekeeperNonPass = { readonly status: "bounce" | "escalate" | "no_receipt" } & Record<string, unknown>;
306
306
  export type HostSubmissionNonPass =
307
307
  | HostGatekeeperNonPass
308
308
  | { readonly code: "coder_skill_expansion_evidence_missing" };