@akagilnc/pi-workflow-roles 0.1.3682 → 0.1.3700

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 (60) hide show
  1. package/dist/acp-host/production-host.js +519 -561
  2. package/dist/analyst-gate-cycles-read.js +11 -1
  3. package/dist/archivist-record-entry.js +1 -1
  4. package/dist/audit-escalation.js +4 -3
  5. package/dist/auditor-dossier-tool.js +12 -0
  6. package/dist/doctor-role.js +2 -1
  7. package/dist/dossier-resolution.js +0 -35
  8. package/dist/gatekeeper-pass-envelope.js +39 -34
  9. package/dist/gatekeeper-role.js +70 -76
  10. package/dist/judge-auditor.js +5 -24
  11. package/dist/judge-role.js +39 -41
  12. package/dist/notary-contracts.js +2 -17
  13. package/dist/notary-role.js +3 -6
  14. package/dist/package-contracts/judge-output.js +0 -2
  15. package/dist/package-contracts/terminating-tools.js +2 -2
  16. package/dist/public-cli/inspector-run.js +16 -2
  17. package/dist/public-cli/instruction-seat-run.js +14 -2
  18. package/dist/public-cli/main.js +198 -213
  19. package/dist/public-cli/notary-run.js +23 -20
  20. package/dist/public-cli/post-admission.js +30 -23
  21. package/dist/public-cli/run-lifecycle.js +6 -14
  22. package/dist/public-role-summons.js +38 -11
  23. package/dist/readable-gate-item.js +16 -0
  24. package/dist/role-runtime.js +28 -3
  25. package/dist/submission-correctable-error.js +2 -1
  26. package/dist/submission-errors.js +26 -8
  27. package/dist/submission-ledger.js +7 -23
  28. package/extensions/role-runtime.ts +0 -2
  29. package/package.json +1 -1
  30. package/souls/countersign.md +3 -1
  31. package/souls/notary.md +5 -0
  32. package/src/acp-host/production-host.ts +1 -4
  33. package/src/acp-host/role-envelope.ts +1 -1
  34. package/src/analyst-gate-cycles-read.ts +10 -1
  35. package/src/archivist-record-entry.ts +7 -3
  36. package/src/audit-escalation.ts +4 -3
  37. package/src/auditor-dossier-tool.ts +21 -0
  38. package/src/doctor-role.ts +2 -1
  39. package/src/dossier-resolution.ts +0 -39
  40. package/src/gatekeeper-pass-envelope.ts +59 -43
  41. package/src/gatekeeper-role.ts +114 -112
  42. package/src/host-contracts.ts +3 -3
  43. package/src/judge-auditor.ts +5 -43
  44. package/src/judge-role.ts +41 -53
  45. package/src/notary-contracts.ts +8 -27
  46. package/src/notary-role.ts +2 -7
  47. package/src/package-contracts/judge-output.ts +0 -1
  48. package/src/package-contracts/terminating-tools.ts +2 -2
  49. package/src/public-cli/inspector-run.ts +32 -2
  50. package/src/public-cli/instruction-seat-run.ts +30 -2
  51. package/src/public-cli/notary-run.ts +48 -16
  52. package/src/public-cli/post-admission.ts +31 -37
  53. package/src/public-cli/run-lifecycle.ts +6 -17
  54. package/src/public-role-summons.ts +76 -12
  55. package/src/readable-gate-item.ts +20 -0
  56. package/src/role-runtime.ts +34 -9
  57. package/src/submission-correctable-error.ts +2 -0
  58. package/src/submission-errors.ts +28 -8
  59. package/src/submission-ledger.ts +8 -32
  60. package/dist/shape-unreadable-failure.js +0 -34
@@ -350,6 +350,10 @@ export async function readAnalystGateCyclesFromAuditorRoles(auditorRolesDirector
350
350
  ? [auditorRolesDirectory]
351
351
  : auditorRolesDirectory;
352
352
  const volumes = [];
353
+ // Same officer session pointed at N times (historical multi-mint pointers, or
354
+ // one parent booking many leaves) must classify once — else each accepted
355
+ // seal is counted ×N in terminal gate-round (#753 acceptance A).
356
+ const classifiedOfficerSessions = new Set();
353
357
  for (const directory of directories) {
354
358
  let names;
355
359
  try {
@@ -367,11 +371,17 @@ export async function readAnalystGateCyclesFromAuditorRoles(auditorRolesDirector
367
371
  }
368
372
  for (const name of names) {
369
373
  const path = join(directory, name);
370
- const sessionPath = name.endsWith(".pointer.json")
374
+ const fromPointer = name.endsWith(".pointer.json");
375
+ const sessionPath = fromPointer
371
376
  ? await resolveOfficerSessionFromPointerFile(path)
372
377
  : path;
373
378
  if (sessionPath === undefined)
374
379
  continue;
380
+ if (fromPointer) {
381
+ if (classifiedOfficerSessions.has(sessionPath))
382
+ continue;
383
+ classifiedOfficerSessions.add(sessionPath);
384
+ }
375
385
  const classified = await classifyAuditorVolume(sessionPath);
376
386
  for (const volume of classified) {
377
387
  if (options.parentSessionFile !== undefined &&
@@ -142,7 +142,7 @@ function bookDirectOfficerRunPointer(options) {
142
142
  ...options.runDirectory !== void 0 && options.runDirectory.trim() !== "" ? { runDirectory: options.runDirectory } : {}
143
143
  };
144
144
  writeFileSync(
145
- join(nest, `${options.officer}-${Date.now().toString(36)}.pointer.json`),
145
+ join(nest, `${options.officer}.pointer.json`),
146
146
  `${JSON.stringify(pointer)}
147
147
  `,
148
148
  "utf8"
@@ -1,3 +1,4 @@
1
+ import { readableGateItem } from "./readable-gate-item.js";
1
2
  export const AUDIT_ESCALATION_KIND = "audit_escalation";
2
3
  // Live Navigator settlement may consume only the projection produced by this
3
4
  // owner. Its private WeakSet cannot be authored by role output; persisted/
@@ -71,10 +72,10 @@ function humanDecisionText(decision, result) {
71
72
  lines.push(`Reason: ${result.reason}`);
72
73
  }
73
74
  if (Array.isArray(result.conflicts)) {
74
- lines.push("Conflicts:", ...result.conflicts.map((conflict) => `- ${conflict}`));
75
+ lines.push("Conflicts:", ...result.conflicts.map((conflict) => `- ${readableGateItem(conflict)}`));
75
76
  }
76
77
  if (officer !== undefined && Array.isArray(result.findings) && result.findings.length > 0) {
77
- lines.push("Findings:", ...result.findings.map((finding) => `- ${finding}`));
78
+ lines.push("Findings:", ...result.findings.map((finding) => `- ${readableGateItem(finding)}`));
78
79
  }
79
80
  const gate = result.auditDecisionGate;
80
81
  if (gate !== null && typeof gate === "object" && !Array.isArray(gate)) {
@@ -82,7 +83,7 @@ function humanDecisionText(decision, result) {
82
83
  if (typeof record.question === "string")
83
84
  lines.push(`Question: ${record.question}`);
84
85
  if (Array.isArray(record.options)) {
85
- lines.push("Options:", ...record.options.map((option) => `- ${option}`));
86
+ lines.push("Options:", ...record.options.map((option) => `- ${readableGateItem(option)}`));
86
87
  }
87
88
  }
88
89
  return lines.join("\n");
@@ -29,6 +29,17 @@ function readLatestToolCallLeaf(context) {
29
29
  function gateSubmissionCandidatePath(runDirectory) {
30
30
  return join(runDirectory, "artifacts", GATE_SUBMISSION_CANDIDATE_FILE);
31
31
  }
32
+ function buildGateOfficerReviewInstruction(input) {
33
+ const lines = [
34
+ "\u672C\u8F6E\u7236\u5E2D\u4EA4\u5377\u5F85\u5BA1\u3002",
35
+ `\u6765\u6E90 run\uFF1A${input.sourceRunDirectory}`
36
+ ];
37
+ const candidate = input.submissionCandidatePath?.trim();
38
+ if (candidate !== void 0 && candidate.length > 0) {
39
+ lines.push(`\u4EA4\u5377\u5019\u9009\uFF08\u51BB\u7ED3\u5FEB\u7167\uFF09\uFF1A${candidate}`);
40
+ }
41
+ return lines.join("\n");
42
+ }
32
43
  function persistGateSubmissionCandidate(runDirectory, context) {
33
44
  const leaf = readLatestToolCallLeaf(context);
34
45
  if (leaf === void 0) return void 0;
@@ -71,6 +82,7 @@ export {
71
82
  AUDITOR_DOSSIER_TOOL_NAME,
72
83
  GATE_SUBMISSION_CANDIDATE_FILE,
73
84
  auditorRunDirectory,
85
+ buildGateOfficerReviewInstruction,
74
86
  createAuditorDossierTool,
75
87
  gateSubmissionCandidatePath,
76
88
  persistGateSubmissionCandidate,
@@ -1,5 +1,6 @@
1
1
  import { disposeComplianceDecision } from "./audit-escalation.js";
2
2
  import { ComplianceResponseRetentionError } from "./compliance-transport.js";
3
+ import { joinReadableGateItems } from "./readable-gate-item.js";
3
4
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.js";
4
5
  import { DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, DOCTOR_ACCEPTED_TEXT, DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput } from "./doctor-contracts.js";
5
6
  import { sitianReport } from "./sitian-facade.js";
@@ -57,7 +58,7 @@ export function createDoctorRoleRuntime(pi, dependencies, host) {
57
58
  }
58
59
  catch (error) {
59
60
  host.failInfrastructure(error, ctx, id);
60
- } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision(audit, { pass: (usage) => ({ content: [{ type: "text", text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text", text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true, ...usageProjection }), received: (auditReceived, usageProjection) => ({ content: [{ type: "text", text: DOCTOR_ACCEPTED_TEXT }], details: { ...acceptedDetails, audit: auditReceived.reply }, terminate: true, ...usageProjection }), bounce: (violations) => { throw new Error(`太医署回执违 soul:${violations.join("; ")}`); }, escalate: (result) => result }, acceptedDetails); } });
61
+ } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision(audit, { pass: (usage) => ({ content: [{ type: "text", text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text", text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true, ...usageProjection }), received: (auditReceived, usageProjection) => ({ content: [{ type: "text", text: DOCTOR_ACCEPTED_TEXT }], details: { ...acceptedDetails, audit: auditReceived.reply }, terminate: true, ...usageProjection }), bounce: (violations) => { throw new Error(`太医署回执违 soul:${joinReadableGateItems(violations)}`); }, escalate: (result) => result }, acceptedDetails); } });
61
62
  pi.on("before_agent_start", (event) => { if (!activation)
62
63
  throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { provenance: "由留存 session 字节推导,封入受理回执。", 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>` }; });
63
64
  }
@@ -4,8 +4,6 @@
4
4
  */
5
5
  import { existsSync, statSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
- import { JUDGE_OUTPUT_TOOL_NAME as JUDGE_OUTPUT_TOOL } from "./package-contracts/judge-output.js";
8
- export const JUDGE_OUTPUT_TOOL_NAME = JUDGE_OUTPUT_TOOL;
9
7
  export const AUDIT_RUN_DIR_ENV = "AK_ROLE_RUN_DIR";
10
8
  export const DOCTOR_CANDIDATE_ENTRY_TYPE = "ak_doctor_audit_candidate";
11
9
  /**
@@ -36,39 +34,6 @@ export function resolveAuditDossier(env = process.env) {
36
34
  function isRecord(value) {
37
35
  return typeof value === "object" && value !== null && !Array.isArray(value);
38
36
  }
39
- export function readJudgeAuditSubjects(context) {
40
- const entries = context.sessionManager.getEntries?.() ?? [];
41
- let hasAssignment = false;
42
- let hasCandidate = false;
43
- for (const entry of entries) {
44
- if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message))
45
- continue;
46
- const message = entry.message;
47
- if (message.role === "user") {
48
- const text = typeof message.content === "string"
49
- ? message.content
50
- : Array.isArray(message.content)
51
- ? message.content.map((part) => (isRecord(part) && part.type === "text" && typeof part.text === "string" ? part.text : "")).join("")
52
- : "";
53
- if (text.trim().length > 0)
54
- hasAssignment = true;
55
- }
56
- if (message.role === "assistant" && Array.isArray(message.content)) {
57
- for (const part of message.content) {
58
- if (isRecord(part) && part.type === "toolCall" && part.name === JUDGE_OUTPUT_TOOL_NAME && isRecord(part.arguments)) {
59
- hasCandidate = true;
60
- }
61
- }
62
- }
63
- }
64
- if (!hasAssignment) {
65
- return { status: "incomplete", observation: { kind: "missing-subject", subject: "assignment" } };
66
- }
67
- if (!hasCandidate) {
68
- return { status: "incomplete", observation: { kind: "missing-subject", subject: "candidate-verdict" } };
69
- }
70
- return { status: "ok" };
71
- }
72
37
  /**
73
38
  * Doctor candidate testimony must be recorded before audit.
74
39
  */
@@ -1,5 +1,5 @@
1
1
  import { bookDirectOfficerRunPointer } from "./archivist-record-entry.js";
2
- import { GatekeeperDecisionError, GatekeeperEscalationError, projectGatekeeperRun, } from "./gatekeeper-role.js";
2
+ import { GatekeeperDecisionError, OFFICER_CONCLUSION_REASK, projectGatekeeperRun, } from "./gatekeeper-role.js";
3
3
  import { sessionFileFromPublicSummon } from "./session-assistant-usage.js";
4
4
  /**
5
5
  * Book a typed pointer under parent session/auditor-roles (archivist-owned write).
@@ -9,7 +9,7 @@ function bookDirectOfficerPointer(context, officer, result, summoned) {
9
9
  if (result.status !== "pass"
10
10
  && result.status !== "bounce"
11
11
  && result.status !== "escalate"
12
- && result.status !== "unreadable") {
12
+ && result.status !== "needs_reask") {
13
13
  return;
14
14
  }
15
15
  const parentFile = context.sessionManager?.getSessionFile?.();
@@ -31,42 +31,47 @@ function bookDirectOfficerPointer(context, officer, result, summoned) {
31
31
  }
32
32
  /**
33
33
  * Shared envelope: project gate, book officer pointer, map onto host actions.
34
- * unreadable = parent stands (ADR 0055); never mechanical NonPass reject.
34
+ * Review loop has no round cap (#753 no-round-cap).
35
35
  */
36
36
  export async function requireGatekeeperPass(options) {
37
- const projected = await projectGatekeeperRun({
38
- context: options.context,
39
- subject: options.subject,
40
- ...(options.signal === undefined ? {} : { signal: options.signal }),
41
- ...(options.summonOfficer === undefined ? {} : { summonOfficer: options.summonOfficer }),
42
- });
43
- const gatekeeper = projected.result;
44
- // Envelope-owned pointer book. Failure is host infrastructure — single face.
45
- if (projected.summoned !== undefined) {
46
- try {
47
- bookDirectOfficerPointer(options.context, projected.officer, gatekeeper, projected.summoned);
37
+ let reask;
38
+ // No round cap — end only on pass, bounce/escalate-to-parent, or real failure.
39
+ for (;;) {
40
+ const projected = await projectGatekeeperRun({
41
+ context: options.context,
42
+ subject: options.subject,
43
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
44
+ ...(options.summonOfficer === undefined ? {} : { summonOfficer: options.summonOfficer }),
45
+ ...(reask === undefined ? {} : { reask }),
46
+ });
47
+ const gatekeeper = projected.result;
48
+ // Envelope-owned pointer book. Failure is host infrastructure — single face.
49
+ if (projected.summoned !== undefined) {
50
+ try {
51
+ bookDirectOfficerPointer(options.context, projected.officer, gatekeeper, projected.summoned);
52
+ }
53
+ catch (error) {
54
+ options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
55
+ }
56
+ }
57
+ if (gatekeeper.status === "pass")
58
+ return;
59
+ if (gatekeeper.status === "needs_reask") {
60
+ // Resume the speaker with plain-language re-ask — never bounce the parent (#753).
61
+ reask = OFFICER_CONCLUSION_REASK;
62
+ continue;
48
63
  }
49
- catch (error) {
64
+ if (gatekeeper.status === "transport_failure") {
65
+ const error = new Error(`交卷闸 transport_failure(${gatekeeper.stage}):${gatekeeper.reason}`);
66
+ error.stage = gatekeeper.stage;
67
+ error.reason = gatekeeper.reason;
68
+ if (gatekeeper.submission !== undefined)
69
+ error.submission = gatekeeper.submission;
50
70
  options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
51
71
  }
72
+ // bounce | escalate | no_receipt: raw receipt (or lifecycle fact) back to parent.
73
+ // escalate is NOT thrown as parent next-step — parent reads the officer words (#753).
74
+ options.hostActions.bindSubmissionNonPass(options.toolCallId, gatekeeper);
75
+ throw new GatekeeperDecisionError(gatekeeper);
52
76
  }
53
- if (gatekeeper.status === "pass")
54
- return;
55
- // ADR 0055 / #675: shape-unreadable officer output must not mechanically reject parent.
56
- if (gatekeeper.status === "unreadable")
57
- return;
58
- if (gatekeeper.status === "transport_failure") {
59
- const error = new Error(`交卷闸 transport_failure(${gatekeeper.stage}):${gatekeeper.reason}`);
60
- error.stage = gatekeeper.stage;
61
- error.reason = gatekeeper.reason;
62
- if (gatekeeper.submission !== undefined)
63
- error.submission = gatekeeper.submission;
64
- options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
65
- }
66
- if (gatekeeper.status === "escalate") {
67
- throw new GatekeeperEscalationError(gatekeeper);
68
- }
69
- // bounce / no_receipt: typed non-pass — envelope owns execute→tool_result bridge.
70
- options.hostActions.bindSubmissionNonPass(options.toolCallId, gatekeeper);
71
- throw new GatekeeperDecisionError(gatekeeper);
72
77
  }
@@ -2,21 +2,24 @@ import { auditorRunDirectory, persistGateSubmissionCandidate, } from "./auditor-
2
2
  import { GatekeeperDecisionError } from "./submission-errors.js";
3
3
  import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.js";
4
4
  import { GATEKEEPER_OUTPUT_TOOL_NAME, gatekeeperDecisionSchema, gatekeeperOutputSchema, } from "./package-contracts/gatekeeper-output.js";
5
- import { retainedShapeUnreadable } from "./shape-unreadable-failure.js";
6
5
  export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
7
6
  export const NOTARY_OUTPUT_TOOL = "ak_notary_output";
8
7
  function gateSeatLabel(stage) {
9
- return stage === "inspector" ? "察院" : "符宝郎";
8
+ if (stage === "inspector")
9
+ return "察院";
10
+ if (stage === "auditor")
11
+ return "审刑院";
12
+ return "符宝郎";
10
13
  }
11
- export { GatekeeperDecisionError } from "./submission-errors.js";
12
- export class GatekeeperEscalationError extends Error {
13
- gatekeeper;
14
- constructor(gatekeeper) {
15
- super(`门下省${gateSeatLabel(gatekeeper.officer)}上呈`);
16
- this.name = "GatekeeperEscalationError";
17
- this.gatekeeper = gatekeeper;
18
- }
14
+ /** Subject kind → review officer (#753 countersign/notary, #756 judge/auditor + worker/inspector). */
15
+ export function gateOfficerForSubject(subject) {
16
+ if (subject.kind === "worker_completion")
17
+ return "inspector";
18
+ if (subject.kind === "judge_compliance")
19
+ return "auditor";
20
+ return "notary";
19
21
  }
22
+ export { GatekeeperDecisionError } from "./submission-errors.js";
20
23
  /**
21
24
  * Direct-seat decision tool spec (#639). Lifecycle assembly stays on the
22
25
  * registration envelope — src/role-runtime.ts (ADR 0018). Schema authority is
@@ -48,20 +51,13 @@ function failureReason(error) {
48
51
  return error.errors.map(failureReason).join("; ");
49
52
  return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
50
53
  }
51
- function asStringArray(value) {
52
- if (!Array.isArray(value))
53
- return [];
54
- // Officer findings may be structured objects (category/law/evidence); the
55
- // parent role must see them verbatim, not an empty list (#750 evidence).
56
- return value.map((item) => (typeof item === "string" ? item : JSON.stringify(item)));
57
- }
58
54
  /** Serializable stand-in when the child tool call had no arguments object. */
59
55
  export const MISSING_ARGUMENTS_SUBMISSION = Object.freeze({ missing: "arguments" });
60
56
  function isRecord(value) {
61
57
  return typeof value === "object" && value !== null && !Array.isArray(value);
62
58
  }
63
59
  /** Keep original decision bytes for the next reader; undefined becomes a serializable missing-args fact. */
64
- function retainedSubmission(decision) {
60
+ function retainedReceipt(decision) {
65
61
  // undefined must not be stored: JSON drops it and the missing-args fact vanishes.
66
62
  // Through the real provider adapter an undefined root argument arrives as an
67
63
  // empty object after serialization; that must also project a missing-args fact.
@@ -69,56 +65,36 @@ function retainedSubmission(decision) {
69
65
  ? MISSING_ARGUMENTS_SUBMISSION
70
66
  : decision;
71
67
  }
72
- /**
73
- * Shape-unreadable officer decision — retain original candidate + typed reason.
74
- * Not a forged bounce, not transport abort (CLAUDE.md §0 / ADR 0055).
75
- * Real provider/engine/disk failures stay transport_failure elsewhere.
76
- */
77
- function shapeUnreadable(officer, decision, reason = "decision 无显式 pass/bounce/escalate") {
78
- return {
79
- status: "unreadable",
80
- officer,
81
- reason,
82
- submission: retainedSubmission(decision),
83
- };
84
- }
85
68
  function readRecord(value) {
86
69
  if (typeof value !== "object" || value === null || Array.isArray(value))
87
70
  return undefined;
88
71
  return value;
89
72
  }
90
- function projectOfficerDecision(officer, decision) {
73
+ /**
74
+ * Read only the conclusion field for queueing (#753).
75
+ * pass | bounce | escalate → queue signal + raw receipt.
76
+ * Anything else accepted → needs_reask (resume speaker), never unreadable/parent-stand.
77
+ * `fallbackStatus` is the terminal outcome.status when the receipt body has no status key
78
+ * (keeps missing-args sentinel intact as the receipt).
79
+ */
80
+ function projectOfficerDecision(officer, decision, fallbackStatus) {
81
+ const receipt = retainedReceipt(decision);
91
82
  const record = readRecord(decision);
92
- if (record === undefined)
93
- return shapeUnreadable(officer, decision);
94
- if (record.status === "bounce") {
95
- return {
96
- status: "bounce",
97
- officer,
98
- disposition: "rewrite",
99
- findings: asStringArray(record.findings),
100
- submission: retainedSubmission(decision),
101
- };
83
+ const status = (record !== undefined && typeof record.status === "string" ? record.status : undefined)
84
+ ?? fallbackStatus;
85
+ if (status === "pass") {
86
+ return { status: "pass", officer, receipt };
102
87
  }
103
- if (record.status === "pass") {
104
- return {
105
- status: "pass",
106
- officer,
107
- findings: asStringArray(record.findings),
108
- };
88
+ if (status === "bounce" || status === "escalate") {
89
+ return { status, officer, receipt };
109
90
  }
110
- if (record.status === "escalate") {
111
- return {
112
- status: "escalate",
113
- officer,
114
- ...(Object.hasOwn(record, "reason") ? { reason: record.reason } : {}),
115
- findings: record.findings,
116
- submission: retainedSubmission(decision),
117
- };
118
- }
119
- return shapeUnreadable(officer, decision);
91
+ return { status: "needs_reask", officer, receipt };
120
92
  }
121
- /** Map a public-role terminal onto the gate officer result surface. */
93
+ /**
94
+ * Project a public-role terminal onto the gate queue surface.
95
+ * Lifecycle facts (no_receipt / transport) stay loud; conclusion reads only
96
+ * the status field. No unusable/unreadable judgment (#753).
97
+ */
122
98
  function projectOfficerTerminal(officer, summoned) {
123
99
  const terminal = summoned.terminal;
124
100
  const outcome = terminal?.roleOutcome;
@@ -142,11 +118,7 @@ function projectOfficerTerminal(officer, summoned) {
142
118
  };
143
119
  }
144
120
  if (outcome.kind === "failure") {
145
- // Single settlement marker only (ADR 0055 / #675) — no cause=output re-derivation.
146
- const shape = retainedShapeUnreadable(outcome.decisiveFacts);
147
- if (shape !== undefined) {
148
- return shapeUnreadable(officer, shape.candidate, outcome.diagnostic);
149
- }
121
+ // Real provider/engine/disk failure — keep loud. Not a shape judgment.
150
122
  return {
151
123
  status: "transport_failure",
152
124
  stage: officer,
@@ -154,25 +126,42 @@ function projectOfficerTerminal(officer, summoned) {
154
126
  submission: outcome.decisiveFacts,
155
127
  };
156
128
  }
129
+ if (outcome.kind === "audit_escalation") {
130
+ // Residual/compliance escalate face: queue as escalate, receipt as written.
131
+ // Nested officer escalate itself seals accepted+status escalate (no rewrite).
132
+ return {
133
+ status: "escalate",
134
+ officer,
135
+ receipt: retainedReceipt(outcome.decisiveFacts),
136
+ };
137
+ }
157
138
  if (outcome.kind === "accepted") {
158
- return projectOfficerDecision(officer, {
159
- status: outcome.status,
160
- ...outcome.decisiveFacts,
161
- });
139
+ // Prefer the officer's own decisiveFacts as the receipt body. outcome.status is
140
+ // only a fallback when facts have no status key (missing-args sentinel stays intact).
141
+ const facts = outcome.decisiveFacts;
142
+ if (isRecord(facts) && Object.keys(facts).length > 0) {
143
+ return projectOfficerDecision(officer, facts, outcome.status);
144
+ }
145
+ return projectOfficerDecision(officer, { status: outcome.status });
162
146
  }
147
+ // Unknown terminal kind: still not a shape judgment — ask the speaker again.
163
148
  return {
164
- status: "transport_failure",
165
- stage: officer,
166
- reason: `${gateSeatLabel(officer)} public summon returned unusable terminal kind`,
167
- submission: outcome,
149
+ status: "needs_reask",
150
+ officer,
151
+ receipt: retainedReceipt(outcome),
168
152
  };
169
153
  }
154
+ /**
155
+ * Plain-language re-ask when the officer conclusion is not pass|bounce|escalate.
156
+ * Not a packaged engine handbook line (#755 exception for 读不出三态).
157
+ */
158
+ export const OFFICER_CONCLUSION_REASK = "上次交卷的结论不是 pass、bounce、escalate 三态之一。请重新输出,结论字段写明其一;打回或上呈的话就是给对方看的原文。";
170
159
  /**
171
160
  * Summon + project only. Lifecycle book and host abort face live on the shared
172
161
  * submit envelope (`gatekeeper-pass-envelope.ts`, ADR 0018 / #675).
173
162
  */
174
163
  export async function projectGatekeeperRun(options) {
175
- const officer = options.subject.kind === "worker_completion" ? "inspector" : "notary";
164
+ const officer = gateOfficerForSubject(options.subject);
176
165
  const runDirectory = options.runDirectory ?? auditorRunDirectory(options.context);
177
166
  if (runDirectory === undefined) {
178
167
  return {
@@ -186,20 +175,25 @@ export async function projectGatekeeperRun(options) {
186
175
  }
187
176
  // Pointer-only summons need a resolvable leaf: Grok session.jsonl is header-only
188
177
  // (#617 DK-4); write the in-memory tool-call candidate as a run artifact first (#632).
189
- persistGateSubmissionCandidate(runDirectory, options.context);
178
+ // Candidate path also rides same-parent officer resume as 人读材料 (#753 / #750).
179
+ const submissionCandidatePath = persistGateSubmissionCandidate(runDirectory, options.context);
190
180
  let summoned;
191
181
  try {
192
182
  const summon = options.summonOfficer
193
- ?? (async (nextOfficer, sourceRunDirectory, officerSignal) => {
183
+ ?? (async (nextOfficer, sourceRunDirectory, officerSignal, reask) => {
194
184
  const { summonGateOfficer } = await import("./public-role-summons.js");
195
185
  return summonGateOfficer({
196
186
  officer: nextOfficer,
197
187
  sourceRunDirectory,
198
188
  cwd: options.context.cwd ?? process.cwd(),
199
189
  ...(officerSignal === undefined ? {} : { signal: officerSignal }),
190
+ ...(reask === undefined ? {} : { reask }),
191
+ ...(submissionCandidatePath === undefined
192
+ ? {}
193
+ : { submissionCandidatePath }),
200
194
  });
201
195
  });
202
- summoned = await summon(officer, runDirectory, options.signal);
196
+ summoned = await summon(officer, runDirectory, options.signal, options.reask);
203
197
  }
204
198
  catch (error) {
205
199
  return {
@@ -1,26 +1,7 @@
1
- import { runComplianceAudit, } from "./compliance-transport.js";
2
- import { auditorRunDirectory } from "./auditor-dossier-tool.js";
3
- import { readJudgeAuditSubjects, requireAuditMaterials, resolveAuditDossier, } from "./dossier-resolution.js";
4
- export const JUDGE_AUDIT_TOOL_NAME = "ak_soul_audit_decision";
5
- export const SOUL_AUDIT_TOOL_NAME = JUDGE_AUDIT_TOOL_NAME;
6
1
  /**
7
- * Judge compliance via public auditor activation (#675 / ADR 0062 / owner r11).
8
- * 审刑院 is an independent role; subject=judge selects souls/judge-auditor.md.
2
+ * Judge nested-audit tool name retained for settlement / historical session faces.
3
+ * Live judge↔auditor review queue is the shared gate envelope (#756) — no
4
+ * createPiJudgeAuditor compliance wrapper remains.
9
5
  */
10
- export function createPiJudgeAuditor() {
11
- return async (options) => {
12
- const dossier = resolveAuditDossier();
13
- requireAuditMaterials(dossier);
14
- const subjects = readJudgeAuditSubjects(options.context);
15
- requireAuditMaterials(subjects);
16
- return runComplianceAudit({
17
- subject: "judge",
18
- context: options.context,
19
- ...(auditorRunDirectory(options.context) === undefined
20
- ? {}
21
- : { runDirectory: auditorRunDirectory(options.context) }),
22
- ...(options.signal === undefined ? {} : { signal: options.signal }),
23
- ...(options.summonAuditor === undefined ? {} : { summonAuditor: options.summonAuditor }),
24
- });
25
- };
26
- }
6
+ export const JUDGE_AUDIT_TOOL_NAME = "ak_soul_audit_decision";
7
+ export const SOUL_AUDIT_TOOL_NAME = JUDGE_AUDIT_TOOL_NAME;
@@ -1,8 +1,10 @@
1
1
  import { stringEnum } from "./host-contracts.js";
2
2
  import { Type } from "typebox";
3
- import { disposeComplianceDecision } from "./audit-escalation.js";
4
3
  import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.js";
5
- import { JUDGE_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, JUDGE_ACCEPTED_TEXT, JUDGE_OUTPUT_TOOL_NAME, validateAcceptedJudgeDetails, } from "./package-contracts/judge-output.js";
4
+ import { ParentQueueReaskError } from "./submission-errors.js";
5
+ import { JUDGE_ACCEPTED_TEXT, JUDGE_OUTPUT_TOOL_NAME, validateAcceptedJudgeDetails, } from "./package-contracts/judge-output.js";
6
+ const JUDGE_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]);
7
+ const JUDGE_STATUS_REASK = "judgeStatus 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。";
6
8
  export { JUDGE_OUTPUT_TOOL_NAME };
7
9
  export const judgeVerdictSchema = withInfrastructureFailureDeclaration(Type.Object({
8
10
  judgeStatus: stringEnum(["converged", "continue", "escalate"], { description: "converged | continue | escalate — 形状指引,非 schema 闸" }),
@@ -37,16 +39,34 @@ export function createJudgeRoleRuntime(pi, dependencies, hostActions) {
37
39
  pi.registerTool({
38
40
  name: JUDGE_OUTPUT_TOOL_NAME,
39
41
  label: "大理寺输出",
40
- description: "提交大理寺终局判词;受理前经审刑院审计。",
42
+ description: "提交大理寺终局判词;受理前经符宝郎内闸与审刑院合规审核。",
41
43
  promptSnippet: "提交大理寺终局判词",
42
44
  parameters: judgeVerdictSchema,
43
45
  async execute(toolCallId, parameters, signal, _onUpdate, ctx) {
44
46
  if (soul === undefined)
45
47
  throw new Error("大理寺职分未装载");
48
+ // #756 queue: read judgeStatus only — escalate skips gates (thrown to caller);
49
+ // unreadable status returns to judge; else 符宝郎内闸 then 审刑院合规.
50
+ const rawStatus = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
51
+ && typeof parameters.judgeStatus === "string"
52
+ ? parameters.judgeStatus
53
+ : undefined;
54
+ if (rawStatus === undefined || !JUDGE_QUEUE_STATUSES.has(rawStatus)) {
55
+ throw new ParentQueueReaskError(JUDGE_STATUS_REASK);
56
+ }
57
+ if (rawStatus === "escalate") {
58
+ // Parent escalate → throw to caller as-is; officers do not attend (#753 / #756).
59
+ const verdict = validateVerdict(parameters);
60
+ return {
61
+ content: [{ type: "text", text: JUDGE_ACCEPTED_TEXT }],
62
+ details: verdict,
63
+ terminate: true,
64
+ };
65
+ }
46
66
  const verdict = validateVerdict(parameters);
47
67
  // Candidate verdict is already on the parent session books as this
48
68
  // tool-call leaf (first-record-then-audit; run 019fea05 L61/L62).
49
- // Gatekeeper runs after the draft is booked and before existing auditor.
69
+ // #753: 符宝郎内闸 — queue only, raw receipt on bounce/escalate.
50
70
  await pi.requireGatekeeperPass({
51
71
  context: ctx,
52
72
  subject: { kind: "judge_draft" },
@@ -54,43 +74,21 @@ export function createJudgeRoleRuntime(pi, dependencies, hostActions) {
54
74
  hostActions,
55
75
  toolCallId,
56
76
  });
57
- let audit;
58
- try {
59
- audit = await dependencies.auditSoulCompliance(signal === undefined
60
- ? { context: ctx }
61
- : { context: ctx, signal });
62
- }
63
- catch (error) {
64
- hostActions.failInfrastructure(error, ctx, toolCallId);
65
- }
66
- const acceptedDetails = verdict;
67
- return disposeComplianceDecision(audit, {
68
- pass: (usage) => ({
69
- content: [{ type: "text", text: JUDGE_ACCEPTED_TEXT }],
70
- details: acceptedDetails,
71
- terminate: true,
72
- ...(usage === undefined ? {} : { usage }),
73
- }),
74
- noReceipt: (auditNoReceipt, usageProjection) => ({
75
- content: [{ type: "text", text: JUDGE_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }],
76
- details: { ...acceptedDetails, auditNoReceipt },
77
- terminate: true,
78
- ...usageProjection,
79
- }),
80
- // #757: parent stands with auditor raw reply — no unreadable judgment.
81
- received: (auditReceived, usageProjection) => ({
82
- content: [{ type: "text", text: JUDGE_ACCEPTED_TEXT }],
83
- details: { ...acceptedDetails, audit: auditReceived.reply },
84
- terminate: true,
85
- ...usageProjection,
86
- }),
87
- bounce: (violations) => {
88
- throw new Error(`大理寺回执违 soul:${violations.join("; ")}`);
89
- },
90
- escalate: (result) => result,
91
- },
92
- // #380: escalate deliveredOutput must carry the same mechanical projection.
93
- acceptedDetails);
77
+ // #756: 审刑院合规路径 — same review-queue law as 符宝郎/察院.
78
+ // pass → accept; bounce|escalate → raw auditor receipt back to judge;
79
+ // not three-state → resume auditor; no round cap; no disposeCompliance mapping.
80
+ await pi.requireGatekeeperPass({
81
+ context: ctx,
82
+ subject: { kind: "judge_compliance" },
83
+ ...(signal === undefined ? {} : { signal }),
84
+ hostActions,
85
+ toolCallId,
86
+ });
87
+ return {
88
+ content: [{ type: "text", text: JUDGE_ACCEPTED_TEXT }],
89
+ details: verdict,
90
+ terminate: true,
91
+ };
94
92
  },
95
93
  });
96
94
  pi.on("before_agent_start", (event) => {