@akagilnc/pi-workflow-roles 0.1.2383 → 0.1.2397
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/audit-escalation.js +0 -13
- package/dist/compliance-transport.js +28 -5
- package/dist/dossier-resolution.js +21 -2
- package/dist/evidence-child-executor.js +1 -1
- package/dist/navigator-invocation-identity.js +16 -4
- package/dist/notary-contracts.js +4 -17
- package/dist/package-contracts/terminating-tools.js +1 -1
- package/dist/public-cli/main.js +101 -374
- package/package.json +1 -1
- package/src/analyst-gate-cycles-read.ts +5 -8
- package/src/audit-escalation.ts +0 -25
- package/src/compliance-transport.ts +38 -6
- package/src/doctor-auditor.ts +3 -3
- package/src/doctor-role.ts +1 -1
- package/src/dossier-resolution.ts +25 -4
- package/src/evidence-child-executor.ts +1 -1
- package/src/gatekeeper-role.ts +33 -43
- package/src/judge-auditor.ts +3 -3
- package/src/judge-role.ts +0 -1
- package/src/navigator-invocation-identity.ts +34 -13
- package/src/notary-contracts.ts +5 -19
- package/src/notary-role.ts +2 -2
- package/src/package-contracts/terminating-tools.ts +1 -1
- package/src/public-cli/doctor-run.ts +0 -2
- package/src/public-cli/fixer-run.ts +0 -15
- package/src/public-cli/judge-run.ts +0 -16
- package/src/public-cli/notary-run.ts +1 -1
- package/src/public-cli/one-shot-dispatch.ts +1 -1
- package/src/public-cli/reviewer-run.ts +0 -15
- package/src/public-cli/settlement.ts +102 -348
- package/src/public-cli/terminal.ts +3 -71
- package/src/reviewer-auditor.ts +3 -3
- package/src/reviewer-role.ts +0 -1
- package/src/role-runtime.ts +42 -9
package/package.json
CHANGED
|
@@ -16,10 +16,9 @@
|
|
|
16
16
|
* An accepted gate terminating receipt (isError:false pair on dispatch/officer
|
|
17
17
|
* tool) whose required typed facts are unusable — status, dispatch officer, or
|
|
18
18
|
* first/last span missing/unknown/unparseable/inverted — also fails loudly via
|
|
19
|
-
* the same throw→ledger `auditor-roles` unreadable seam.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* non-gate volumes (soul-audit noise, etc.) stay omitted from pairing.
|
|
19
|
+
* the same throw→ledger `auditor-roles` unreadable seam. Unknown/non-contract
|
|
20
|
+
* dispatch status stays loud (#475 abolished Gatekeeper incomplete special-case).
|
|
21
|
+
* True non-gate volumes (soul-audit noise, etc.) stay omitted from pairing.
|
|
23
22
|
*/
|
|
24
23
|
import { readdir } from "node:fs/promises";
|
|
25
24
|
import { join } from "node:path";
|
|
@@ -36,7 +35,7 @@ export type AnalystGateCycleRound = {
|
|
|
36
35
|
readonly roundIndex: number;
|
|
37
36
|
/** Current English officer face after historical alias fold. */
|
|
38
37
|
readonly officer: "inspector" | "notary";
|
|
39
|
-
/** Typed officer terminal status (pass / bounce /
|
|
38
|
+
/** Typed officer terminal status (pass / bounce / …). */
|
|
40
39
|
readonly status: string;
|
|
41
40
|
/** Officer subsession first→last usable timestamp delta (ms). */
|
|
42
41
|
readonly officerWallMs: number;
|
|
@@ -237,9 +236,7 @@ async function classifyAuditorVolume(
|
|
|
237
236
|
const findingsCount = findings.length;
|
|
238
237
|
|
|
239
238
|
if (DISPATCH_TOOLS.has(call.toolName)) {
|
|
240
|
-
// Gatekeeper contract
|
|
241
|
-
// lawful zero-pair terminal (#434-436 / #458) — never wash the parent leg.
|
|
242
|
-
if (status === "incomplete") return undefined;
|
|
239
|
+
// Gatekeeper contract terminal is dispatch only (#475).
|
|
243
240
|
if (status !== "dispatch") {
|
|
244
241
|
throw new Error(
|
|
245
242
|
`accepted dispatch receipt has non-dispatch status ${JSON.stringify(status)} in ${filePath}`,
|
package/src/audit-escalation.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
ComplianceAuditIncomplete,
|
|
5
4
|
ComplianceDecision,
|
|
6
5
|
} from "./compliance-transport.ts";
|
|
7
6
|
|
|
@@ -35,13 +34,6 @@ export type AuditEscalationToolResult = {
|
|
|
35
34
|
usage?: Usage;
|
|
36
35
|
};
|
|
37
36
|
|
|
38
|
-
export type AuditIncompleteToolResult = {
|
|
39
|
-
content: [{ type: "text"; text: string }];
|
|
40
|
-
details: ComplianceAuditIncomplete;
|
|
41
|
-
terminate: true;
|
|
42
|
-
usage?: Usage;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
37
|
/**
|
|
46
38
|
* Build the escalation delivery face.
|
|
47
39
|
* Role-delivered fields ride under the escalation discriminator (ADR 0055).
|
|
@@ -119,17 +111,6 @@ export function projectAuditEscalation(
|
|
|
119
111
|
};
|
|
120
112
|
}
|
|
121
113
|
|
|
122
|
-
export function projectAuditIncomplete(
|
|
123
|
-
decision: ComplianceAuditIncomplete,
|
|
124
|
-
): AuditIncompleteToolResult {
|
|
125
|
-
return {
|
|
126
|
-
content: [{ type: "text", text: "Compliance audit incomplete; no role receipt was formed." }],
|
|
127
|
-
details: decision,
|
|
128
|
-
terminate: true,
|
|
129
|
-
...(decision.usage === undefined ? {} : { usage: decision.usage }),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
114
|
/**
|
|
134
115
|
* Discriminator-only recognition (ADR 0040). Shape of conflicts/options/gate
|
|
135
116
|
* is not a reject gate — element types and cardinality are delivery content.
|
|
@@ -152,7 +133,6 @@ export type ComplianceDecisionHandlers<T> = {
|
|
|
152
133
|
) => T | PromiseLike<T>;
|
|
153
134
|
revise: (violations: readonly unknown[]) => T | PromiseLike<T>;
|
|
154
135
|
escalate: (result: AuditEscalationToolResult) => T | PromiseLike<T>;
|
|
155
|
-
auditIncomplete?: (result: AuditIncompleteToolResult) => T | PromiseLike<T>;
|
|
156
136
|
};
|
|
157
137
|
|
|
158
138
|
/**
|
|
@@ -183,10 +163,5 @@ export async function disposeComplianceDecision<T>(
|
|
|
183
163
|
return await handlers.escalate(
|
|
184
164
|
projectAuditEscalation(decision, deliveredOutput),
|
|
185
165
|
);
|
|
186
|
-
case "audit-incomplete":
|
|
187
|
-
if (handlers.auditIncomplete === undefined) {
|
|
188
|
-
throw new Error("Compliance audit-incomplete handler is unavailable");
|
|
189
|
-
}
|
|
190
|
-
return await handlers.auditIncomplete(projectAuditIncomplete(decision));
|
|
191
166
|
}
|
|
192
167
|
}
|
|
@@ -15,9 +15,30 @@ export type ComplianceAuditObservation =
|
|
|
15
15
|
| { kind: "non-object-arguments"; type: ComplianceArgumentRootType }
|
|
16
16
|
| { kind: "object-status-unreadable"; status: "missing" | "unknown" }
|
|
17
17
|
| DossierObservation;
|
|
18
|
-
export type ComplianceAuditIncomplete = { status: "audit-incomplete"; observation: ComplianceAuditObservation; candidate: unknown; usage?: Usage };
|
|
19
18
|
export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt"; usage?: Usage };
|
|
20
|
-
export type ComplianceDecision = { status: "pass"; usage?: Usage } | { status: "revise"; violations: readonly unknown[]; usage?: Usage } | { status: "escalate"; conflicts?: unknown; decisionGate?: unknown; usage?: Usage } | ComplianceNoReceipt
|
|
19
|
+
export type ComplianceDecision = { status: "pass"; usage?: Usage } | { status: "revise"; violations: readonly unknown[]; usage?: Usage } | { status: "escalate"; conflicts?: unknown; decisionGate?: unknown; usage?: Usage } | ComplianceNoReceipt;
|
|
20
|
+
|
|
21
|
+
/** Unreadable compliance candidate — infrastructure failure, not a judgment status (#475). */
|
|
22
|
+
export class ComplianceCandidateUnreadableError extends Error {
|
|
23
|
+
readonly observation: ComplianceAuditObservation;
|
|
24
|
+
readonly candidate: unknown;
|
|
25
|
+
readonly usage?: Usage;
|
|
26
|
+
constructor(observation: ComplianceAuditObservation, candidate: unknown, usage?: Usage) {
|
|
27
|
+
const detail =
|
|
28
|
+
observation.kind === "non-object-arguments"
|
|
29
|
+
? `${observation.kind}:${observation.type}`
|
|
30
|
+
: observation.kind === "object-status-unreadable"
|
|
31
|
+
? `${observation.kind}:${observation.status}`
|
|
32
|
+
: observation.kind === "missing-subject"
|
|
33
|
+
? `${observation.kind}:${observation.subject}`
|
|
34
|
+
: observation.kind;
|
|
35
|
+
super(`Compliance candidate unreadable: ${detail}`);
|
|
36
|
+
this.name = "ComplianceCandidateUnreadableError";
|
|
37
|
+
this.observation = observation;
|
|
38
|
+
this.candidate = candidate;
|
|
39
|
+
if (usage !== undefined) this.usage = usage;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
21
42
|
export type ComplianceDispatch = { model: Model<Api>; auth: { apiKey?: string; headers?: Record<string, string | null>; env?: Record<string, string> } };
|
|
22
43
|
|
|
23
44
|
/** Zero-projection kickoff — soul already carries dossier-fetch duty; no hand-delivered materials. */
|
|
@@ -25,8 +46,9 @@ export const AUDITOR_DOSSIER_PROMPT = "Audit the current run dossier." as const;
|
|
|
25
46
|
|
|
26
47
|
const nonblank = Type.String({ minLength: 1, pattern: "\\S" });
|
|
27
48
|
const decisionGateSchema = Type.Object({ question: nonblank, options: Type.Array(nonblank, { minItems: 1 }) }, { additionalProperties: false });
|
|
28
|
-
// Transport
|
|
29
|
-
//
|
|
49
|
+
// Transport retains malformed candidates on ComplianceCandidateUnreadableError so
|
|
50
|
+
// the existing failure channel can publish observation + candidate (#475).
|
|
51
|
+
// Status values are guidance, not a schema gate.
|
|
30
52
|
export const complianceDecisionSchema = Type.Object({ status: Type.Unknown({ description: "Auditor decision status." }), violations: Type.Array(nonblank, { description: "Observed compliance violations." }), conflicts: Type.Array(nonblank, { description: "Unresolved authority or execution conflicts." }), decisionGate: Type.Union([decisionGateSchema, Type.Null()], { description: "Escalation question and available options." }) }, { additionalProperties: true, required: [] });
|
|
31
53
|
|
|
32
54
|
export function createComplianceDecisionTool(name: string, description: string) {
|
|
@@ -93,12 +115,22 @@ function retainComplianceResponse(context: ExtensionContext, response: Assistant
|
|
|
93
115
|
}
|
|
94
116
|
function readListField(value: unknown): readonly unknown[] { return Array.isArray(value) ? value : value === undefined ? [] : [value]; }
|
|
95
117
|
export function readComplianceCandidate(arguments_: unknown, usage?: Usage): ComplianceDecision {
|
|
96
|
-
if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_))
|
|
118
|
+
if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
|
|
119
|
+
throw new ComplianceCandidateUnreadableError(
|
|
120
|
+
{ kind: "non-object-arguments", type: arguments_ === null ? "null" : Array.isArray(arguments_) ? "array" : typeof arguments_ as ComplianceArgumentRootType },
|
|
121
|
+
arguments_,
|
|
122
|
+
usage,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
97
125
|
const args = arguments_ as Record<string, unknown>; const status = args.status;
|
|
98
126
|
if (status === "pass") return { status, ...(usage === undefined ? {} : { usage }) };
|
|
99
127
|
if (status === "revise") return { status, violations: readListField(args.violations), ...(usage === undefined ? {} : { usage }) };
|
|
100
128
|
if (status === "escalate") return { status, ...(Object.hasOwn(args, "conflicts") ? { conflicts: args.conflicts } : {}), ...(Object.hasOwn(args, "decisionGate") ? { decisionGate: args.decisionGate } : {}), ...(usage === undefined ? {} : { usage }) };
|
|
101
|
-
|
|
129
|
+
throw new ComplianceCandidateUnreadableError(
|
|
130
|
+
{ kind: "object-status-unreadable", status: status === undefined ? "missing" : "unknown" },
|
|
131
|
+
arguments_,
|
|
132
|
+
usage,
|
|
133
|
+
);
|
|
102
134
|
}
|
|
103
135
|
|
|
104
136
|
export type RunComplianceAuditOptions = {
|
package/src/doctor-auditor.ts
CHANGED
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
} from "./compliance-transport.ts";
|
|
11
11
|
import {
|
|
12
12
|
readDoctorAuditSubjects,
|
|
13
|
+
requireAuditMaterials,
|
|
13
14
|
resolveAuditDossier,
|
|
14
|
-
toAuditIncomplete,
|
|
15
15
|
} from "./dossier-resolution.ts";
|
|
16
16
|
|
|
17
17
|
export const DOCTOR_AUDIT_TOOL_NAME = "ak_doctor_audit_decision";
|
|
@@ -35,9 +35,9 @@ export function createPiDoctorAuditor(
|
|
|
35
35
|
): (options: DoctorAuditOptions) => Promise<ComplianceDecision> {
|
|
36
36
|
return async (options) => {
|
|
37
37
|
const dossier = resolveAuditDossier();
|
|
38
|
-
|
|
38
|
+
requireAuditMaterials(dossier);
|
|
39
39
|
const subjects = readDoctorAuditSubjects(options.context);
|
|
40
|
-
|
|
40
|
+
requireAuditMaterials(subjects);
|
|
41
41
|
|
|
42
42
|
return runComplianceAudit({
|
|
43
43
|
tool,
|
package/src/doctor-role.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function createDoctorRoleRuntime(pi: ExtensionAPI, dependencies: DoctorRo
|
|
|
16
16
|
let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
|
|
17
17
|
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) };
|
|
18
18
|
if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "Doctor Evidence", description: "Read retained Pi session bytes with bounded pagination.", parameters: doctorEvidenceReadSchema, async execute(_id, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("Doctor is not activated"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
|
|
19
|
-
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "Doctor Output", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id, params, signal, _update, ctx): Promise<AgentToolResult<unknown>> { if (!activation) throw new Error("Doctor is not activated"); singleton(id, ctx); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendActiveSessionCustomEntry(ctx, DOCTOR_CANDIDATE_ENTRY_TYPE, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }, { unavailable: "doctor candidate retention is unavailable", failed: "doctor candidate retention failed" }); } 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 = withEngineLaborFallbackField(details, readActivationEngineLaborFallbackField()); return disposeComplianceDecision<AgentToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: "Doctor output accepted" }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: "Doctor output accepted; compliance audit produced no receipt" }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`Doctor output violates its soul: ${violations.join("; ")}`); }, escalate: (result) => result
|
|
19
|
+
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "Doctor Output", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id, params, signal, _update, ctx): Promise<AgentToolResult<unknown>> { if (!activation) throw new Error("Doctor is not activated"); singleton(id, ctx); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendActiveSessionCustomEntry(ctx, DOCTOR_CANDIDATE_ENTRY_TYPE, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }, { unavailable: "doctor candidate retention is unavailable", failed: "doctor candidate retention failed" }); } 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 = withEngineLaborFallbackField(details, readActivationEngineLaborFallbackField()); return disposeComplianceDecision<AgentToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: "Doctor output accepted" }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: "Doctor output accepted; compliance audit produced no receipt" }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`Doctor output violates its soul: ${violations.join("; ")}`); }, escalate: (result) => result }, acceptedDetails); } });
|
|
20
20
|
pi.on("before_agent_start", (event) => { if (!activation) throw new Error("Doctor is not activated"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { provenance: "runtime-derived from retained session bytes and sealed into the accepted receipt", 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>` }; }); }
|
|
21
21
|
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"); } };
|
|
22
22
|
}
|
|
@@ -130,8 +130,29 @@ export function readDoctorAuditSubjects(context: ExtensionContext): SubjectResol
|
|
|
130
130
|
return { status: "incomplete", observation: { kind: "missing-subject", subject: "candidate-testimony" } };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Missing dossier/subject is infrastructure failure, not a judgment status (#475).
|
|
135
|
+
* Observation + empty candidate ride the existing failInfrastructure → error artifact path.
|
|
136
|
+
*/
|
|
137
|
+
export class AuditMaterialsUnavailableError extends Error {
|
|
138
|
+
readonly observation: DossierObservation;
|
|
139
|
+
readonly candidate: undefined;
|
|
140
|
+
constructor(observation: DossierObservation) {
|
|
141
|
+
const detail =
|
|
142
|
+
observation.kind === "missing-subject"
|
|
143
|
+
? `${observation.kind}:${observation.subject}`
|
|
144
|
+
: observation.kind;
|
|
145
|
+
super(`Audit materials unavailable: ${detail}`);
|
|
146
|
+
this.name = "AuditMaterialsUnavailableError";
|
|
147
|
+
this.observation = observation;
|
|
148
|
+
this.candidate = undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function requireAuditMaterials(
|
|
153
|
+
resolution: DossierResolution | SubjectResolution,
|
|
154
|
+
): asserts resolution is DossierOk | SubjectOk {
|
|
155
|
+
if (resolution.status === "incomplete") {
|
|
156
|
+
throw new AuditMaterialsUnavailableError(resolution.observation);
|
|
157
|
+
}
|
|
137
158
|
}
|
|
@@ -927,7 +927,7 @@ export async function executeAuditorChild(
|
|
|
927
927
|
decision = part.arguments;
|
|
928
928
|
decisionCallId = part.id;
|
|
929
929
|
// Pi can reject malformed root arguments before invoking execute;
|
|
930
|
-
// that remains the existing
|
|
930
|
+
// that remains the existing unreadable-candidate failure path.
|
|
931
931
|
if (part.arguments === undefined) decisionSubmitted = true;
|
|
932
932
|
}
|
|
933
933
|
}
|
package/src/gatekeeper-role.ts
CHANGED
|
@@ -24,18 +24,18 @@ export type GatekeeperResult =
|
|
|
24
24
|
readonly findings: readonly string[];
|
|
25
25
|
readonly submission: unknown;
|
|
26
26
|
}
|
|
27
|
+
| { readonly status: "no_receipt"; readonly stage: "gatekeeper" | "inspector" | "notary"; readonly reason: string; readonly facts: NoReceiptLifecycleFacts }
|
|
27
28
|
| {
|
|
28
|
-
readonly status: "
|
|
29
|
+
readonly status: "transport_failure";
|
|
29
30
|
readonly stage: "gatekeeper" | "inspector" | "notary";
|
|
30
31
|
readonly reason: string;
|
|
32
|
+
/** Original unusable submission retained for the failure channel. */
|
|
31
33
|
readonly submission?: unknown;
|
|
32
|
-
}
|
|
33
|
-
| { readonly status: "no_receipt"; readonly stage: "gatekeeper" | "inspector" | "notary"; readonly reason: string; readonly facts: NoReceiptLifecycleFacts }
|
|
34
|
-
| { readonly status: "transport_failure"; readonly stage: "gatekeeper" | "inspector" | "notary"; readonly reason: string };
|
|
34
|
+
};
|
|
35
35
|
|
|
36
36
|
export type GatekeeperNonPassResult = Extract<
|
|
37
37
|
GatekeeperResult,
|
|
38
|
-
{ status: "bounce" | "
|
|
38
|
+
{ status: "bounce" | "no_receipt" }
|
|
39
39
|
>;
|
|
40
40
|
|
|
41
41
|
function nonPassMessage(result: GatekeeperNonPassResult): string {
|
|
@@ -74,15 +74,13 @@ export type GatekeeperPassHostActions = {
|
|
|
74
74
|
// Unknown fields so wrong types/spellings still reach projection (ADR 0055/0057; 仓第 0 条).
|
|
75
75
|
// Opening goes through the sole openToolObject owner — no parallel transport helper.
|
|
76
76
|
const officerDecisionSchema = openToolObject(Type.Object({
|
|
77
|
-
status: Type.Unknown({ description: "pass | bounce
|
|
77
|
+
status: Type.Unknown({ description: "pass | bounce — guidance, not a schema gate." }),
|
|
78
78
|
findings: Type.Unknown({ description: "string[] findings retained with pass or bounce." }),
|
|
79
|
-
reason: Type.Unknown({ description: "Why the officer decision is incomplete." }),
|
|
80
79
|
}));
|
|
81
80
|
|
|
82
81
|
const gatekeeperDecisionSchema = openToolObject(Type.Object({
|
|
83
|
-
status: Type.Unknown({ description: "dispatch
|
|
82
|
+
status: Type.Unknown({ description: "dispatch — guidance, not a schema gate." }),
|
|
84
83
|
officer: Type.Unknown({ description: "inspector | notary when status is dispatch." }),
|
|
85
|
-
reason: Type.Unknown({ description: "Why Gatekeeper dispatch is incomplete." }),
|
|
86
84
|
}));
|
|
87
85
|
|
|
88
86
|
const INVOCATION_OVERLAY = "取证工具不受白名单限制;若取证产生临时副作用,取证结束后须自行恢复。";
|
|
@@ -104,7 +102,7 @@ function subjectTool(subject: GatekeeperSubject): AuditorDecisionTool {
|
|
|
104
102
|
export function createOfficerDecisionTool(name: string): AuditorDecisionTool {
|
|
105
103
|
return {
|
|
106
104
|
name,
|
|
107
|
-
description: "Submit one typed pass
|
|
105
|
+
description: "Submit one typed pass or bounce decision.",
|
|
108
106
|
parameters: officerDecisionSchema,
|
|
109
107
|
async execute(_id, args) { return result(`accepted ${String((args as { status?: unknown })?.status)}`, args); },
|
|
110
108
|
};
|
|
@@ -114,7 +112,7 @@ export function createOfficerDecisionTool(name: string): AuditorDecisionTool {
|
|
|
114
112
|
export function createGatekeeperOutputTool(): AuditorDecisionTool {
|
|
115
113
|
return {
|
|
116
114
|
name: GATEKEEPER_OUTPUT_TOOL,
|
|
117
|
-
description: "Dispatch the admitted subject to one officer
|
|
115
|
+
description: "Dispatch the admitted subject to one officer.",
|
|
118
116
|
parameters: gatekeeperDecisionSchema,
|
|
119
117
|
async execute(_id, args) { return result(`accepted ${String((args as { status?: unknown })?.status)}`, args); },
|
|
120
118
|
};
|
|
@@ -143,15 +141,18 @@ function retainedSubmission(decision: unknown): unknown {
|
|
|
143
141
|
return decision === undefined ? MISSING_ARGUMENTS_SUBMISSION : decision;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
/**
|
|
147
|
-
|
|
144
|
+
/**
|
|
145
|
+
* No usable explicit release is infrastructure failure, not a judgment status.
|
|
146
|
+
* Original submission is retained for the failure channel (#475).
|
|
147
|
+
*/
|
|
148
|
+
function noUsableReleaseFailure(
|
|
148
149
|
stage: "gatekeeper" | "inspector" | "notary",
|
|
149
150
|
decision: unknown,
|
|
150
|
-
): Extract<GatekeeperResult, { status: "
|
|
151
|
+
): Extract<GatekeeperResult, { status: "transport_failure" }> {
|
|
151
152
|
return {
|
|
152
|
-
status: "
|
|
153
|
+
status: "transport_failure",
|
|
153
154
|
stage,
|
|
154
|
-
reason: stage === "gatekeeper" ? "decision 无显式 dispatch" : "decision 无显式 pass",
|
|
155
|
+
reason: stage === "gatekeeper" ? "decision 无显式 dispatch" : "decision 无显式 pass/bounce",
|
|
155
156
|
submission: retainedSubmission(decision),
|
|
156
157
|
};
|
|
157
158
|
}
|
|
@@ -163,19 +164,11 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
163
164
|
|
|
164
165
|
function projectProvinceDecision(decision: unknown): GatekeeperResult | { status: "dispatch"; officer: "inspector" | "notary" } {
|
|
165
166
|
const record = readRecord(decision);
|
|
166
|
-
if (record === undefined) return
|
|
167
|
-
if (record.status === "incomplete") {
|
|
168
|
-
const reason = record.reason;
|
|
169
|
-
if (typeof reason === "string" && reason.trim() !== "") {
|
|
170
|
-
// Role's own incomplete reason is kept as-is; machine does not rewrite it.
|
|
171
|
-
return { status: "incomplete", stage: "gatekeeper", reason, submission: retainedSubmission(decision) };
|
|
172
|
-
}
|
|
173
|
-
return noExplicitReleaseIncomplete("gatekeeper", decision);
|
|
174
|
-
}
|
|
167
|
+
if (record === undefined) return noUsableReleaseFailure("gatekeeper", decision);
|
|
175
168
|
if (record.status === "dispatch" && (record.officer === "inspector" || record.officer === "notary")) {
|
|
176
169
|
return { status: "dispatch", officer: record.officer };
|
|
177
170
|
}
|
|
178
|
-
return
|
|
171
|
+
return noUsableReleaseFailure("gatekeeper", decision);
|
|
179
172
|
}
|
|
180
173
|
|
|
181
174
|
function projectOfficerDecision(
|
|
@@ -183,15 +176,7 @@ function projectOfficerDecision(
|
|
|
183
176
|
decision: unknown,
|
|
184
177
|
): GatekeeperResult {
|
|
185
178
|
const record = readRecord(decision);
|
|
186
|
-
if (record === undefined) return
|
|
187
|
-
if (record.status === "incomplete") {
|
|
188
|
-
const reason = record.reason;
|
|
189
|
-
if (typeof reason === "string" && reason.trim() !== "") {
|
|
190
|
-
// Role's own incomplete reason is kept as-is; machine does not rewrite it.
|
|
191
|
-
return { status: "incomplete", stage: officer, reason, submission: retainedSubmission(decision) };
|
|
192
|
-
}
|
|
193
|
-
return noExplicitReleaseIncomplete(officer, decision);
|
|
194
|
-
}
|
|
179
|
+
if (record === undefined) return noUsableReleaseFailure(officer, decision);
|
|
195
180
|
if (record.status === "bounce") {
|
|
196
181
|
return {
|
|
197
182
|
status: "bounce",
|
|
@@ -208,7 +193,7 @@ function projectOfficerDecision(
|
|
|
208
193
|
findings: asStringArray(record.findings),
|
|
209
194
|
};
|
|
210
195
|
}
|
|
211
|
-
return
|
|
196
|
+
return noUsableReleaseFailure(officer, decision);
|
|
212
197
|
}
|
|
213
198
|
|
|
214
199
|
export async function runGatekeeper(options: RunGatekeeperOptions): Promise<GatekeeperResult> {
|
|
@@ -221,7 +206,7 @@ export async function runGatekeeper(options: RunGatekeeperOptions): Promise<Gate
|
|
|
221
206
|
roleLabel: "Gatekeeper",
|
|
222
207
|
menxiaSeat: "gatekeeper",
|
|
223
208
|
systemPrompt: `${await loadSoul("gatekeeper")}\n\n${INVOCATION_OVERLAY}`,
|
|
224
|
-
prompt: "Read the admitted subject with ak_gatekeeper_subject, then dispatch it
|
|
209
|
+
prompt: "Read the admitted subject with ak_gatekeeper_subject, then dispatch it to one officer.",
|
|
225
210
|
tool: createGatekeeperOutputTool(),
|
|
226
211
|
dossierTool: subjectTool(options.subject),
|
|
227
212
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
@@ -259,7 +244,7 @@ export async function runGatekeeper(options: RunGatekeeperOptions): Promise<Gate
|
|
|
259
244
|
}
|
|
260
245
|
}
|
|
261
246
|
|
|
262
|
-
/** Project GatekeeperResult onto a submit path: transport→failInfrastructure; bounce/
|
|
247
|
+
/** Project GatekeeperResult onto a submit path: transport→failInfrastructure; bounce/no_receipt→typed throw; pass silent. */
|
|
263
248
|
export async function requireGatekeeperPass(options: {
|
|
264
249
|
readonly context: ExtensionContext;
|
|
265
250
|
readonly subject: GatekeeperSubject;
|
|
@@ -274,11 +259,16 @@ export async function requireGatekeeperPass(options: {
|
|
|
274
259
|
});
|
|
275
260
|
if (gatekeeper.status === "pass") return;
|
|
276
261
|
if (gatekeeper.status === "transport_failure") {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
262
|
+
// Typed stage/reason/submission ride failInfrastructure → durable tool_result (#475).
|
|
263
|
+
const error = new Error(`Gatekeeper transport failure at ${gatekeeper.stage}: ${gatekeeper.reason}`) as Error & {
|
|
264
|
+
stage: typeof gatekeeper.stage;
|
|
265
|
+
reason: string;
|
|
266
|
+
submission?: unknown;
|
|
267
|
+
};
|
|
268
|
+
error.stage = gatekeeper.stage;
|
|
269
|
+
error.reason = gatekeeper.reason;
|
|
270
|
+
if (gatekeeper.submission !== undefined) error.submission = gatekeeper.submission;
|
|
271
|
+
options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
|
|
282
272
|
}
|
|
283
273
|
// Envelope owns the execute→tool_result bridge; this module only projects + throws.
|
|
284
274
|
options.hostActions.bindGatekeeperNonPass(options.toolCallId, gatekeeper);
|
package/src/judge-auditor.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
|
|
|
10
10
|
import { loadAuditorSoul } from "./auditor-soul.ts";
|
|
11
11
|
import {
|
|
12
12
|
readJudgeAuditSubjects,
|
|
13
|
+
requireAuditMaterials,
|
|
13
14
|
resolveAuditDossier,
|
|
14
|
-
toAuditIncomplete,
|
|
15
15
|
} from "./dossier-resolution.ts";
|
|
16
16
|
|
|
17
17
|
export const JUDGE_AUDIT_TOOL_NAME = "ak_soul_audit_decision";
|
|
@@ -37,9 +37,9 @@ export function createPiJudgeAuditor(
|
|
|
37
37
|
): (options: JudgeAuditOptions) => Promise<ComplianceDecision> {
|
|
38
38
|
return async (options) => {
|
|
39
39
|
const dossier = resolveAuditDossier();
|
|
40
|
-
|
|
40
|
+
requireAuditMaterials(dossier);
|
|
41
41
|
const subjects = readJudgeAuditSubjects(options.context);
|
|
42
|
-
|
|
42
|
+
requireAuditMaterials(subjects);
|
|
43
43
|
|
|
44
44
|
return runComplianceAudit({
|
|
45
45
|
tool: auditDecisionTool,
|
package/src/judge-role.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* One shared typed terminal classifier owns durable completion for lifecycle,
|
|
16
16
|
* publicNavigatorSettlement, and every public CLI Receipt extractor:
|
|
17
17
|
* - accepted/human: isError exactly false and no infrastructure-failure fact
|
|
18
|
-
* - infrastructure: isError exactly true plus
|
|
18
|
+
* - infrastructure: isError exactly true plus base kind/source/reasonCode identity
|
|
19
19
|
* - retryable/missing/nonboolean/contradictory/malformed: nonterminal
|
|
20
20
|
*
|
|
21
21
|
* One truth table also owns marker↔terminal cardinality:
|
|
@@ -33,13 +33,25 @@ export const NAVIGATOR_INVOCATION_ENTRY = "ak-navigator-invocation" as const;
|
|
|
33
33
|
/** Typed durable infrastructure-failure fact on a packaged role output toolResult. */
|
|
34
34
|
export const NAVIGATOR_INFRASTRUCTURE_FAILURE_KIND = "role_infrastructure_failure" as const;
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/** Required base identity keys for infrastructure-failure recognition. */
|
|
37
37
|
const NAVIGATOR_INFRASTRUCTURE_FAILURE_KEYS = [
|
|
38
38
|
"kind",
|
|
39
39
|
"source",
|
|
40
40
|
"reasonCode",
|
|
41
41
|
] as const;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Known typed failure evidence keys projected onto durable infrastructure details (#475).
|
|
45
|
+
* Extraction whitelist only — classification does not reject unknown extras (ADR 0040).
|
|
46
|
+
*/
|
|
47
|
+
export const NAVIGATOR_INFRASTRUCTURE_FAILURE_EVIDENCE_KEYS = [
|
|
48
|
+
"observation",
|
|
49
|
+
"candidate",
|
|
50
|
+
"submission",
|
|
51
|
+
"stage",
|
|
52
|
+
"reason",
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
43
55
|
export type NavigatorInfrastructureFailureFact = {
|
|
44
56
|
kind: typeof NAVIGATOR_INFRASTRUCTURE_FAILURE_KIND;
|
|
45
57
|
source: "shared-role-lifecycle";
|
|
@@ -55,16 +67,13 @@ export function buildNavigatorInfrastructureFailureFact(): NavigatorInfrastructu
|
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
70
|
+
* Base infrastructure-failure identity on durable details.
|
|
71
|
+
* Only kind/source/reasonCode discriminate; extra fields are allowed and retained
|
|
72
|
+
* (ADR 0040 — discriminators select the branch, they do not ban extras).
|
|
60
73
|
*/
|
|
61
|
-
export function
|
|
62
|
-
value: unknown,
|
|
63
|
-
): value is NavigatorInfrastructureFailureFact {
|
|
74
|
+
export function hasNavigatorInfrastructureFailureBase(value: unknown): boolean {
|
|
64
75
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
65
76
|
const record = value as Record<string, unknown>;
|
|
66
|
-
const keys = Object.keys(record);
|
|
67
|
-
if (keys.length !== NAVIGATOR_INFRASTRUCTURE_FAILURE_KEYS.length) return false;
|
|
68
77
|
for (const key of NAVIGATOR_INFRASTRUCTURE_FAILURE_KEYS) {
|
|
69
78
|
if (!Object.hasOwn(record, key)) return false;
|
|
70
79
|
}
|
|
@@ -75,6 +84,18 @@ export function isNavigatorInfrastructureFailureFact(
|
|
|
75
84
|
);
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Exact closed infrastructure-failure fact (no evidence extensions).
|
|
89
|
+
* Classifier uses {@link hasNavigatorInfrastructureFailureBase} so enriched
|
|
90
|
+
* durable details still complete as infrastructure (#475).
|
|
91
|
+
*/
|
|
92
|
+
export function isNavigatorInfrastructureFailureFact(
|
|
93
|
+
value: unknown,
|
|
94
|
+
): value is NavigatorInfrastructureFailureFact {
|
|
95
|
+
if (!hasNavigatorInfrastructureFailureBase(value)) return false;
|
|
96
|
+
return Object.keys(value as object).length === NAVIGATOR_INFRASTRUCTURE_FAILURE_KEYS.length;
|
|
97
|
+
}
|
|
98
|
+
|
|
78
99
|
const PACKAGED_ROLE_OUTPUT_TOOLS: ReadonlyMap<string, string> = new Map(
|
|
79
100
|
PACKAGED_ROLE_REGISTRY.map((entry) => [entry.outputTool, entry.role]),
|
|
80
101
|
);
|
|
@@ -191,11 +212,11 @@ export function classifyPackagedRoleTerminalResult(
|
|
|
191
212
|
if (typeof message.toolName !== "string") return { kind: "nonterminal" };
|
|
192
213
|
if (!PACKAGED_ROLE_OUTPUT_TOOLS.has(message.toolName)) return { kind: "nonterminal" };
|
|
193
214
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
215
|
+
// Base identity is enough; durable details may carry typed failure evidence (#475).
|
|
216
|
+
const hasInfraBase = hasNavigatorInfrastructureFailureBase(message.details);
|
|
217
|
+
const infraFact = hasInfraBase ? buildNavigatorInfrastructureFailureFact() : undefined;
|
|
197
218
|
|
|
198
|
-
// Infrastructure completion: exact isError === true +
|
|
219
|
+
// Infrastructure completion: exact isError === true + infra base identity.
|
|
199
220
|
if (message.isError === true) {
|
|
200
221
|
if (infraFact === undefined) return { kind: "nonterminal" };
|
|
201
222
|
return { kind: "infrastructure", fact: infraFact };
|
package/src/notary-contracts.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public Notary (符宝郎) terminating receipt contracts.
|
|
3
|
-
* Lawful explicit releases: pass | bounce
|
|
4
|
-
*
|
|
3
|
+
* Lawful explicit releases: pass | bounce.
|
|
4
|
+
* No usable result is infrastructure failure via public settlement, not a judgment status (#475).
|
|
5
5
|
*/
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
|
|
@@ -31,14 +31,11 @@ export const NOTARY_FIXED_KICKOFF =
|
|
|
31
31
|
export const notaryOutputSchema = openToolObject(
|
|
32
32
|
Type.Object({
|
|
33
33
|
status: Type.Unknown({
|
|
34
|
-
description: "pass | bounce
|
|
34
|
+
description: "pass | bounce — guidance, not a schema gate.",
|
|
35
35
|
}),
|
|
36
36
|
findings: Type.Unknown({
|
|
37
37
|
description: "string[] findings retained with pass or bounce.",
|
|
38
38
|
}),
|
|
39
|
-
reason: Type.Unknown({
|
|
40
|
-
description: "Why the notary decision is incomplete.",
|
|
41
|
-
}),
|
|
42
39
|
}),
|
|
43
40
|
);
|
|
44
41
|
|
|
@@ -54,8 +51,7 @@ type NotaryOutputClean =
|
|
|
54
51
|
readonly status: "bounce";
|
|
55
52
|
readonly disposition: "rewrite";
|
|
56
53
|
readonly findings: readonly string[];
|
|
57
|
-
}
|
|
58
|
-
| { readonly status: "incomplete"; readonly reason: string };
|
|
54
|
+
};
|
|
59
55
|
|
|
60
56
|
/** Clean submission or seat-fallback tainted accepted receipt (ADR 0071). */
|
|
61
57
|
export type NotaryOutput =
|
|
@@ -73,7 +69,7 @@ function asStringArray(value: unknown): readonly string[] {
|
|
|
73
69
|
|
|
74
70
|
/**
|
|
75
71
|
* Project one explicit Notary release. Throws when there is no lawful explicit
|
|
76
|
-
* pass / bounce
|
|
72
|
+
* pass / bounce — callers map that to the existing non-zero failure channel.
|
|
77
73
|
*/
|
|
78
74
|
export function validateNotaryOutput(value: unknown): NotaryOutput {
|
|
79
75
|
if (!isRecord(value)) {
|
|
@@ -84,13 +80,6 @@ export function validateNotaryOutput(value: unknown): NotaryOutput {
|
|
|
84
80
|
throw new Error("Notary output has no recognized execution discriminator");
|
|
85
81
|
}
|
|
86
82
|
const status = seatFallbackBaseStatus(statusRaw);
|
|
87
|
-
if (status === "incomplete") {
|
|
88
|
-
const reason = value.reason;
|
|
89
|
-
if (typeof reason !== "string" || reason.trim() === "") {
|
|
90
|
-
throw new Error("Notary incomplete requires a non-empty reason");
|
|
91
|
-
}
|
|
92
|
-
return structuredClone(value) as NotaryOutput;
|
|
93
|
-
}
|
|
94
83
|
if (status === "bounce") {
|
|
95
84
|
const clone = structuredClone(value) as Record<string, unknown>;
|
|
96
85
|
if (clone.disposition === undefined) clone.disposition = "rewrite";
|
|
@@ -127,8 +116,5 @@ export function notaryDecisiveFacts(output: NotaryOutput): Record<string, unknow
|
|
|
127
116
|
if (status === "bounce") {
|
|
128
117
|
facts.disposition = "rewrite";
|
|
129
118
|
}
|
|
130
|
-
if (status === "incomplete") {
|
|
131
|
-
facts.reason = (output as { reason: string }).reason;
|
|
132
|
-
}
|
|
133
119
|
return facts;
|
|
134
120
|
}
|
package/src/notary-role.ts
CHANGED
|
@@ -86,7 +86,7 @@ export function createNotaryRoleRuntime(
|
|
|
86
86
|
name: NOTARY_OUTPUT_TOOL_NAME,
|
|
87
87
|
label: "Notary Output",
|
|
88
88
|
description:
|
|
89
|
-
"Submit one typed pass
|
|
89
|
+
"Submit one typed pass or bounce decision on quote fidelity and ticket alignment.",
|
|
90
90
|
promptSnippet: "Submit the Notary decision",
|
|
91
91
|
promptGuidelines: [
|
|
92
92
|
`Use ${NOTARY_OUTPUT_TOOL_NAME} as the sole final action.`,
|
|
@@ -109,7 +109,7 @@ export function createNotaryRoleRuntime(
|
|
|
109
109
|
output = validateNotaryOutput(parameters);
|
|
110
110
|
} catch (error) {
|
|
111
111
|
// Non-explicit release stays a rejected terminating call so public
|
|
112
|
-
// settlement can
|
|
112
|
+
// settlement can map it to the existing non-zero failure channel (#475).
|
|
113
113
|
throw error instanceof Error
|
|
114
114
|
? error
|
|
115
115
|
: new Error(String(error));
|