@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.
- package/dist/acp-host/production-host.js +519 -561
- package/dist/analyst-gate-cycles-read.js +11 -1
- package/dist/archivist-record-entry.js +1 -1
- package/dist/audit-escalation.js +4 -3
- package/dist/auditor-dossier-tool.js +12 -0
- package/dist/doctor-role.js +2 -1
- package/dist/dossier-resolution.js +0 -35
- package/dist/gatekeeper-pass-envelope.js +39 -34
- package/dist/gatekeeper-role.js +70 -76
- package/dist/judge-auditor.js +5 -24
- package/dist/judge-role.js +39 -41
- package/dist/notary-contracts.js +2 -17
- package/dist/notary-role.js +3 -6
- package/dist/package-contracts/judge-output.js +0 -2
- package/dist/package-contracts/terminating-tools.js +2 -2
- package/dist/public-cli/inspector-run.js +16 -2
- package/dist/public-cli/instruction-seat-run.js +14 -2
- package/dist/public-cli/main.js +198 -213
- package/dist/public-cli/notary-run.js +23 -20
- package/dist/public-cli/post-admission.js +30 -23
- package/dist/public-cli/run-lifecycle.js +6 -14
- package/dist/public-role-summons.js +38 -11
- package/dist/readable-gate-item.js +16 -0
- package/dist/role-runtime.js +28 -3
- package/dist/submission-correctable-error.js +2 -1
- package/dist/submission-errors.js +26 -8
- package/dist/submission-ledger.js +7 -23
- package/extensions/role-runtime.ts +0 -2
- package/package.json +1 -1
- package/souls/countersign.md +3 -1
- package/souls/notary.md +5 -0
- package/src/acp-host/production-host.ts +1 -4
- package/src/acp-host/role-envelope.ts +1 -1
- package/src/analyst-gate-cycles-read.ts +10 -1
- package/src/archivist-record-entry.ts +7 -3
- package/src/audit-escalation.ts +4 -3
- package/src/auditor-dossier-tool.ts +21 -0
- package/src/doctor-role.ts +2 -1
- package/src/dossier-resolution.ts +0 -39
- package/src/gatekeeper-pass-envelope.ts +59 -43
- package/src/gatekeeper-role.ts +114 -112
- package/src/host-contracts.ts +3 -3
- package/src/judge-auditor.ts +5 -43
- package/src/judge-role.ts +41 -53
- package/src/notary-contracts.ts +8 -27
- package/src/notary-role.ts +2 -7
- package/src/package-contracts/judge-output.ts +0 -1
- package/src/package-contracts/terminating-tools.ts +2 -2
- package/src/public-cli/inspector-run.ts +32 -2
- package/src/public-cli/instruction-seat-run.ts +30 -2
- package/src/public-cli/notary-run.ts +48 -16
- package/src/public-cli/post-admission.ts +31 -37
- package/src/public-cli/run-lifecycle.ts +6 -17
- package/src/public-role-summons.ts +76 -12
- package/src/readable-gate-item.ts +20 -0
- package/src/role-runtime.ts +34 -9
- package/src/submission-correctable-error.ts +2 -0
- package/src/submission-errors.ts +28 -8
- package/src/submission-ledger.ts +8 -32
- package/dist/shape-unreadable-failure.js +0 -34
package/src/audit-escalation.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Usage } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import type {
|
|
4
4
|
ComplianceDecision,
|
|
5
5
|
} from "./compliance-transport.ts";
|
|
6
|
+
import { readableGateItem } from "./readable-gate-item.ts";
|
|
6
7
|
|
|
7
8
|
export const AUDIT_ESCALATION_KIND = "audit_escalation" as const;
|
|
8
9
|
|
|
@@ -126,17 +127,17 @@ function humanDecisionText(
|
|
|
126
127
|
lines.push(`Reason: ${result.reason}`);
|
|
127
128
|
}
|
|
128
129
|
if (Array.isArray(result.conflicts)) {
|
|
129
|
-
lines.push("Conflicts:", ...result.conflicts.map((conflict) => `- ${conflict}`));
|
|
130
|
+
lines.push("Conflicts:", ...result.conflicts.map((conflict) => `- ${readableGateItem(conflict)}`));
|
|
130
131
|
}
|
|
131
132
|
if (officer !== undefined && Array.isArray(result.findings) && result.findings.length > 0) {
|
|
132
|
-
lines.push("Findings:", ...result.findings.map((finding) => `- ${finding}`));
|
|
133
|
+
lines.push("Findings:", ...result.findings.map((finding) => `- ${readableGateItem(finding)}`));
|
|
133
134
|
}
|
|
134
135
|
const gate = result.auditDecisionGate;
|
|
135
136
|
if (gate !== null && typeof gate === "object" && !Array.isArray(gate)) {
|
|
136
137
|
const record = gate as Record<string, unknown>;
|
|
137
138
|
if (typeof record.question === "string") lines.push(`Question: ${record.question}`);
|
|
138
139
|
if (Array.isArray(record.options)) {
|
|
139
|
-
lines.push("Options:", ...record.options.map((option) => `- ${option}`));
|
|
140
|
+
lines.push("Options:", ...record.options.map((option) => `- ${readableGateItem(option)}`));
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
return lines.join("\n");
|
|
@@ -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).
|
package/src/doctor-role.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 { joinReadableGateItems } from "./readable-gate-item.ts";
|
|
4
5
|
import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
|
|
5
6
|
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, type DoctorCase } from "./doctor-contracts.ts";
|
|
6
7
|
|
|
@@ -35,7 +36,7 @@ export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDe
|
|
|
35
36
|
let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
|
|
36
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) };
|
|
37
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 }; } });
|
|
38
|
-
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 testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendCandidate(ctx, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), received: (auditReceived, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: { ...acceptedDetails, audit: auditReceived.reply }, terminate: true as const, ...usageProjection }), bounce: (violations) => { throw new Error(`太医署回执违 soul:${violations
|
|
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 testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendCandidate(ctx, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), received: (auditReceived, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: { ...acceptedDetails, audit: auditReceived.reply }, terminate: true as const, ...usageProjection }), bounce: (violations) => { throw new Error(`太医署回执违 soul:${joinReadableGateItems(violations)}`); }, escalate: (result) => result }, acceptedDetails); } });
|
|
39
40
|
pi.on("before_agent_start", (event) => { if (!activation) 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>` }; }); }
|
|
40
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"); } };
|
|
41
42
|
}
|
|
@@ -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
|
-
|
|
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:
|
|
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 !== "
|
|
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
|
-
*
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
}
|
package/src/gatekeeper-role.ts
CHANGED
|
@@ -15,76 +15,80 @@ import {
|
|
|
15
15
|
} from "./package-contracts/gatekeeper-output.ts";
|
|
16
16
|
import type { PublicSummonResult } from "./public-role-summons.ts";
|
|
17
17
|
import type { TerminalResult } from "./public-cli/terminal.ts";
|
|
18
|
-
import { retainedShapeUnreadable } from "./shape-unreadable-failure.ts";
|
|
19
18
|
export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
|
|
20
19
|
export const NOTARY_OUTPUT_TOOL = "ak_notary_output";
|
|
21
20
|
|
|
21
|
+
/** Gate review officers — 察院 / 符宝郎 / 审刑院 (#753 / #756). */
|
|
22
|
+
export type GateOfficer = "inspector" | "notary" | "auditor";
|
|
23
|
+
|
|
22
24
|
/** Officer routing only — content is self-fetched via the shared run-dossier tool (#632). */
|
|
23
25
|
export type GatekeeperSubject =
|
|
24
26
|
| { readonly kind: "worker_completion" }
|
|
25
27
|
| { readonly kind: "judge_draft" }
|
|
28
|
+
| { readonly kind: "judge_compliance" }
|
|
26
29
|
| { readonly kind: "countersign_verdict" };
|
|
27
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
|
+
*/
|
|
28
37
|
export type GatekeeperResult =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
| { readonly status: "
|
|
33
|
-
| {
|
|
34
|
-
readonly status: "bounce";
|
|
35
|
-
readonly officer: "inspector" | "notary";
|
|
36
|
-
readonly disposition: "rewrite";
|
|
37
|
-
readonly findings: readonly string[];
|
|
38
|
-
readonly submission: unknown;
|
|
39
|
-
}
|
|
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 }
|
|
40
42
|
| {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/** Shape-unreadable officer output — typed fact, not a forged bounce (ADR 0055 / §0). */
|
|
50
|
-
readonly status: "unreadable";
|
|
51
|
-
readonly officer: "inspector" | "notary";
|
|
52
|
-
readonly reason: string;
|
|
53
|
-
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;
|
|
54
51
|
}
|
|
52
|
+
| { readonly status: "no_receipt"; readonly stage: GateOfficer; readonly reason: string; readonly facts: NoReceiptLifecycleFacts }
|
|
55
53
|
| {
|
|
56
54
|
readonly status: "transport_failure";
|
|
57
|
-
readonly stage:
|
|
55
|
+
readonly stage: GateOfficer;
|
|
58
56
|
readonly reason: string;
|
|
59
|
-
/** Original
|
|
57
|
+
/** Original transport/process failure payload retained for the failure channel. */
|
|
60
58
|
readonly submission?: unknown;
|
|
61
59
|
};
|
|
62
60
|
|
|
61
|
+
/** Non-pass faces that bounce the parent session (correctable). */
|
|
63
62
|
export type GatekeeperNonPassResult = Extract<
|
|
64
63
|
GatekeeperResult,
|
|
65
|
-
{ status: "bounce" | "
|
|
64
|
+
{ status: "bounce" | "escalate" | "no_receipt" }
|
|
66
65
|
>;
|
|
67
66
|
|
|
68
|
-
function gateSeatLabel(stage:
|
|
69
|
-
|
|
67
|
+
function gateSeatLabel(stage: GateOfficer): string {
|
|
68
|
+
if (stage === "inspector") return "察院";
|
|
69
|
+
if (stage === "auditor") return "审刑院";
|
|
70
|
+
return "符宝郎";
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
super(`门下省${gateSeatLabel(gatekeeper.officer)}上呈`);
|
|
78
|
-
this.name = "GatekeeperEscalationError";
|
|
79
|
-
this.gatekeeper = gatekeeper;
|
|
80
|
-
}
|
|
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";
|
|
81
78
|
}
|
|
82
79
|
|
|
80
|
+
export { GatekeeperDecisionError } from "./submission-errors.ts";
|
|
81
|
+
|
|
83
82
|
export type GateOfficerSummon = (
|
|
84
|
-
officer:
|
|
83
|
+
officer: GateOfficer,
|
|
85
84
|
sourceRunDirectory: string,
|
|
86
85
|
/** Parent cancellation forwarded to the nested activation (#675). */
|
|
87
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,
|
|
88
92
|
) => Promise<PublicSummonResult>;
|
|
89
93
|
|
|
90
94
|
export type RunGatekeeperOptions = {
|
|
@@ -93,6 +97,10 @@ export type RunGatekeeperOptions = {
|
|
|
93
97
|
readonly signal?: AbortSignal;
|
|
94
98
|
/** Run directory of the parent role (pointer-only summons, ADR 0079). */
|
|
95
99
|
readonly runDirectory?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Plain-language re-ask for this summon (resume speaker after non-three-state).
|
|
102
|
+
*/
|
|
103
|
+
readonly reask?: string;
|
|
96
104
|
/**
|
|
97
105
|
* Test seam for public-role summons. Production calls the shared public
|
|
98
106
|
* activation path (#675); inject only in offline tracers.
|
|
@@ -144,13 +152,6 @@ function failureReason(error: unknown): string {
|
|
|
144
152
|
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
function asStringArray(value: unknown): readonly string[] {
|
|
148
|
-
if (!Array.isArray(value)) return [];
|
|
149
|
-
// Officer findings may be structured objects (category/law/evidence); the
|
|
150
|
-
// parent role must see them verbatim, not an empty list (#750 evidence).
|
|
151
|
-
return value.map((item) => (typeof item === "string" ? item : JSON.stringify(item)));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
155
|
/** Serializable stand-in when the child tool call had no arguments object. */
|
|
155
156
|
export const MISSING_ARGUMENTS_SUBMISSION = Object.freeze({ missing: "arguments" as const });
|
|
156
157
|
|
|
@@ -159,7 +160,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
/** Keep original decision bytes for the next reader; undefined becomes a serializable missing-args fact. */
|
|
162
|
-
function
|
|
163
|
+
function retainedReceipt(decision: unknown): unknown {
|
|
163
164
|
// undefined must not be stored: JSON drops it and the missing-args fact vanishes.
|
|
164
165
|
// Through the real provider adapter an undefined root argument arrives as an
|
|
165
166
|
// empty object after serialization; that must also project a missing-args fact.
|
|
@@ -168,66 +169,44 @@ function retainedSubmission(decision: unknown): unknown {
|
|
|
168
169
|
: decision;
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
/**
|
|
172
|
-
* Shape-unreadable officer decision — retain original candidate + typed reason.
|
|
173
|
-
* Not a forged bounce, not transport abort (CLAUDE.md §0 / ADR 0055).
|
|
174
|
-
* Real provider/engine/disk failures stay transport_failure elsewhere.
|
|
175
|
-
*/
|
|
176
|
-
function shapeUnreadable(
|
|
177
|
-
officer: "inspector" | "notary",
|
|
178
|
-
decision: unknown,
|
|
179
|
-
reason = "decision 无显式 pass/bounce/escalate",
|
|
180
|
-
): Extract<GatekeeperResult, { status: "unreadable" }> {
|
|
181
|
-
return {
|
|
182
|
-
status: "unreadable",
|
|
183
|
-
officer,
|
|
184
|
-
reason,
|
|
185
|
-
submission: retainedSubmission(decision),
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
172
|
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
190
173
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
191
174
|
return value as Record<string, unknown>;
|
|
192
175
|
}
|
|
193
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
|
+
*/
|
|
194
184
|
function projectOfficerDecision(
|
|
195
|
-
officer:
|
|
185
|
+
officer: GateOfficer,
|
|
196
186
|
decision: unknown,
|
|
187
|
+
fallbackStatus?: string,
|
|
197
188
|
): GatekeeperResult {
|
|
189
|
+
const receipt = retainedReceipt(decision);
|
|
198
190
|
const record = readRecord(decision);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
disposition: "rewrite",
|
|
205
|
-
findings: asStringArray(record.findings),
|
|
206
|
-
submission: retainedSubmission(decision),
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
if (record.status === "pass") {
|
|
210
|
-
return {
|
|
211
|
-
status: "pass",
|
|
212
|
-
officer,
|
|
213
|
-
findings: asStringArray(record.findings),
|
|
214
|
-
};
|
|
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 };
|
|
215
196
|
}
|
|
216
|
-
if (
|
|
217
|
-
return {
|
|
218
|
-
status: "escalate",
|
|
219
|
-
officer,
|
|
220
|
-
...(Object.hasOwn(record, "reason") ? { reason: record.reason } : {}),
|
|
221
|
-
findings: record.findings,
|
|
222
|
-
submission: retainedSubmission(decision),
|
|
223
|
-
};
|
|
197
|
+
if (status === "bounce" || status === "escalate") {
|
|
198
|
+
return { status, officer, receipt };
|
|
224
199
|
}
|
|
225
|
-
return
|
|
200
|
+
return { status: "needs_reask", officer, receipt };
|
|
226
201
|
}
|
|
227
202
|
|
|
228
|
-
/**
|
|
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
|
+
*/
|
|
229
208
|
function projectOfficerTerminal(
|
|
230
|
-
officer:
|
|
209
|
+
officer: GateOfficer,
|
|
231
210
|
summoned: PublicSummonResult,
|
|
232
211
|
): GatekeeperResult {
|
|
233
212
|
const terminal: TerminalResult | undefined = summoned.terminal;
|
|
@@ -252,11 +231,7 @@ function projectOfficerTerminal(
|
|
|
252
231
|
};
|
|
253
232
|
}
|
|
254
233
|
if (outcome.kind === "failure") {
|
|
255
|
-
//
|
|
256
|
-
const shape = retainedShapeUnreadable(outcome.decisiveFacts);
|
|
257
|
-
if (shape !== undefined) {
|
|
258
|
-
return shapeUnreadable(officer, shape.candidate, outcome.diagnostic);
|
|
259
|
-
}
|
|
234
|
+
// Real provider/engine/disk failure — keep loud. Not a shape judgment.
|
|
260
235
|
return {
|
|
261
236
|
status: "transport_failure",
|
|
262
237
|
stage: officer,
|
|
@@ -264,28 +239,47 @@ function projectOfficerTerminal(
|
|
|
264
239
|
submission: outcome.decisiveFacts,
|
|
265
240
|
};
|
|
266
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
|
+
}
|
|
267
251
|
if (outcome.kind === "accepted") {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 });
|
|
272
259
|
}
|
|
260
|
+
// Unknown terminal kind: still not a shape judgment — ask the speaker again.
|
|
273
261
|
return {
|
|
274
|
-
status: "
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
submission: outcome,
|
|
262
|
+
status: "needs_reask",
|
|
263
|
+
officer,
|
|
264
|
+
receipt: retainedReceipt(outcome),
|
|
278
265
|
};
|
|
279
266
|
}
|
|
280
267
|
|
|
281
268
|
/** Projection carrier for the shared submit envelope (ADR 0018). No lifecycle book here. */
|
|
282
269
|
export type GatekeeperProjection = {
|
|
283
|
-
readonly officer:
|
|
270
|
+
readonly officer: GateOfficer;
|
|
284
271
|
readonly result: GatekeeperResult;
|
|
285
272
|
/** Present only when a public summon actually returned (not transport pre-summon failure). */
|
|
286
273
|
readonly summoned?: PublicSummonResult;
|
|
287
274
|
};
|
|
288
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
|
+
|
|
289
283
|
/**
|
|
290
284
|
* Summon + project only. Lifecycle book and host abort face live on the shared
|
|
291
285
|
* submit envelope (`gatekeeper-pass-envelope.ts`, ADR 0018 / #675).
|
|
@@ -293,7 +287,7 @@ export type GatekeeperProjection = {
|
|
|
293
287
|
export async function projectGatekeeperRun(
|
|
294
288
|
options: RunGatekeeperOptions,
|
|
295
289
|
): Promise<GatekeeperProjection> {
|
|
296
|
-
const officer = options.subject
|
|
290
|
+
const officer = gateOfficerForSubject(options.subject);
|
|
297
291
|
const runDirectory = options.runDirectory ?? auditorRunDirectory(options.context);
|
|
298
292
|
if (runDirectory === undefined) {
|
|
299
293
|
return {
|
|
@@ -307,21 +301,29 @@ export async function projectGatekeeperRun(
|
|
|
307
301
|
}
|
|
308
302
|
// Pointer-only summons need a resolvable leaf: Grok session.jsonl is header-only
|
|
309
303
|
// (#617 DK-4); write the in-memory tool-call candidate as a run artifact first (#632).
|
|
310
|
-
|
|
304
|
+
// Candidate path also rides same-parent officer resume as 人读材料 (#753 / #750).
|
|
305
|
+
const submissionCandidatePath = persistGateSubmissionCandidate(
|
|
306
|
+
runDirectory,
|
|
307
|
+
options.context,
|
|
308
|
+
);
|
|
311
309
|
let summoned: PublicSummonResult;
|
|
312
310
|
try {
|
|
313
311
|
const summon =
|
|
314
312
|
options.summonOfficer
|
|
315
|
-
?? (async (nextOfficer, sourceRunDirectory, officerSignal) => {
|
|
313
|
+
?? (async (nextOfficer, sourceRunDirectory, officerSignal, reask) => {
|
|
316
314
|
const { summonGateOfficer } = await import("./public-role-summons.ts");
|
|
317
315
|
return summonGateOfficer({
|
|
318
316
|
officer: nextOfficer,
|
|
319
317
|
sourceRunDirectory,
|
|
320
318
|
cwd: options.context.cwd ?? process.cwd(),
|
|
321
319
|
...(officerSignal === undefined ? {} : { signal: officerSignal }),
|
|
320
|
+
...(reask === undefined ? {} : { reask }),
|
|
321
|
+
...(submissionCandidatePath === undefined
|
|
322
|
+
? {}
|
|
323
|
+
: { submissionCandidatePath }),
|
|
322
324
|
});
|
|
323
325
|
});
|
|
324
|
-
summoned = await summon(officer, runDirectory, options.signal);
|
|
326
|
+
summoned = await summon(officer, runDirectory, options.signal, options.reask);
|
|
325
327
|
} catch (error) {
|
|
326
328
|
return {
|
|
327
329
|
officer,
|
package/src/host-contracts.ts
CHANGED
|
@@ -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" | "
|
|
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" };
|