@akagilnc/pi-workflow-roles 0.1.2383 → 0.1.2402

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 (57) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/audit-escalation.js +0 -13
  4. package/dist/compliance-transport.js +28 -5
  5. package/dist/doctor-contracts.js +2 -9
  6. package/dist/dossier-resolution.js +21 -2
  7. package/dist/engine-detour-tool.js +10 -35
  8. package/dist/evidence-child-executor.js +12 -4
  9. package/dist/merger-contracts.js +2 -8
  10. package/dist/navigator-attendance.js +1 -9
  11. package/dist/navigator-invocation-identity.js +16 -4
  12. package/dist/notary-contracts.js +7 -25
  13. package/dist/package-contracts/judge-output.js +1 -4
  14. package/dist/package-contracts/reviewer-output.js +1 -7
  15. package/dist/package-contracts/terminating-tools.js +6 -11
  16. package/dist/public-cli/main.js +125 -475
  17. package/package.json +1 -1
  18. package/scripts/build-package.mjs +1 -2
  19. package/src/analyst-gate-cycles-read.ts +5 -8
  20. package/src/analyst-metric-families/acceptance-success-rework.ts +3 -6
  21. package/src/audit-escalation.ts +0 -25
  22. package/src/collector-role.ts +1 -8
  23. package/src/compliance-transport.ts +38 -6
  24. package/src/doctor-auditor.ts +3 -3
  25. package/src/doctor-contracts.ts +3 -18
  26. package/src/doctor-role.ts +1 -5
  27. package/src/dossier-resolution.ts +25 -4
  28. package/src/engine-detour-tool.ts +16 -51
  29. package/src/evidence-child-executor.ts +12 -4
  30. package/src/factory-board.ts +2 -4
  31. package/src/gatekeeper-role.ts +33 -43
  32. package/src/judge-auditor.ts +3 -3
  33. package/src/judge-role.ts +1 -8
  34. package/src/merger-contracts.ts +3 -17
  35. package/src/merger-role.ts +1 -8
  36. package/src/navigator-attendance.ts +1 -14
  37. package/src/navigator-invocation-identity.ts +34 -13
  38. package/src/notary-contracts.ts +9 -43
  39. package/src/notary-role.ts +3 -5
  40. package/src/package-contracts/judge-output.ts +2 -17
  41. package/src/package-contracts/reviewer-output.ts +2 -21
  42. package/src/package-contracts/terminating-tools.ts +5 -14
  43. package/src/package-contracts/worker-output.ts +2 -10
  44. package/src/public-cli/doctor-run.ts +0 -2
  45. package/src/public-cli/fixer-run.ts +0 -15
  46. package/src/public-cli/judge-run.ts +0 -16
  47. package/src/public-cli/notary-run.ts +1 -1
  48. package/src/public-cli/one-shot-dispatch.ts +1 -1
  49. package/src/public-cli/reviewer-run.ts +0 -15
  50. package/src/public-cli/settlement.ts +110 -368
  51. package/src/public-cli/terminal.ts +3 -71
  52. package/src/reviewer-auditor.ts +3 -3
  53. package/src/reviewer-role.ts +2 -11
  54. package/src/role-runtime.ts +43 -37
  55. package/src/worker-role.ts +2 -14
  56. package/dist/engine-labor-fallback.js +0 -136
  57. package/src/engine-labor-fallback.ts +0 -270
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.2383",
3
+ "version": "0.1.2402",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -9,8 +9,7 @@ const entries = [
9
9
  "work-subject-identity",
10
10
  "navigator-invocation-identity",
11
11
  "navigator-attendance",
12
- // Navigator package-graph dependency (seat-fallback base status / ADR 0071).
13
- "engine-labor-fallback",
12
+ // Navigator package-graph dependencies used by attendance settlement.
14
13
  "activation-ledger-git",
15
14
  "activation-ledger-topology",
16
15
  "activation-reconciliation",
@@ -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. Lawful Gatekeeper
20
- * typed `incomplete` is a recognizable terminal (omit from pairing, leg stays
21
- * readable) — only unknown/non-contract dispatch status stays loud. True
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 / incomplete / …). */
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 terminals: dispatch | incomplete. Incomplete is a
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}`,
@@ -9,7 +9,6 @@
9
9
  * - success rate den = success-eligible accepted legs (no-receipt out; planned out)
10
10
  * - planned = plan-duty acceptance; never success numerator or denominator
11
11
  */
12
- import { seatFallbackBaseStatus } from "../engine-labor-fallback.ts";
13
12
  import type { AnalystReadableRunFacts, AnalystRunTerminalFace } from "../analyst-ledger.ts";
14
13
  import { medianNumber } from "../analyst-median.ts";
15
14
  import type { AnalystMetricFamilyModule } from "../analyst-metric-family.ts";
@@ -186,10 +185,8 @@ function mapTerminal(
186
185
  };
187
186
  }
188
187
 
189
- // Seat-fallback taint is visible on the label; acceptance/success use base semantics.
190
- const statusBase = seatFallbackBaseStatus(status);
191
188
  const acceptedSet = ACCEPTED_STATUS[role];
192
- if (acceptedSet === undefined || !acceptedSet.has(statusBase)) {
189
+ if (acceptedSet === undefined || !acceptedSet.has(status)) {
193
190
  return {
194
191
  terminalLabel: status,
195
192
  accepted: false,
@@ -199,9 +196,9 @@ function mapTerminal(
199
196
  };
200
197
  }
201
198
 
202
- const plannedDuty = WORKER_ROLES.has(role) && statusBase === "planned";
199
+ const plannedDuty = WORKER_ROLES.has(role) && status === "planned";
203
200
  const successSet = SUCCESS_STATUS[role] ?? new Set<string>();
204
- const success = !plannedDuty && successSet.has(statusBase);
201
+ const success = !plannedDuty && successSet.has(status);
205
202
  const successEligible = !plannedDuty;
206
203
 
207
204
  return {
@@ -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
  }
@@ -4,10 +4,6 @@ import type {
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
  import type { Static } from "typebox";
6
6
 
7
- import {
8
- readActivationEngineLaborFallbackField,
9
- withEngineLaborFallbackField,
10
- } from "./engine-labor-fallback.ts";
11
7
  import {
12
8
  COLLECTOR_FIXED_KICKOFF,
13
9
  emptyCollectorManifest,
@@ -422,10 +418,7 @@ export function createCollectorRoleRuntime(
422
418
  );
423
419
  activation.ledger.markOutputAccepted();
424
420
  activation.ledger.completeOperational(toolCallId);
425
- const acceptedDetails = withEngineLaborFallbackField(
426
- receipt,
427
- readActivationEngineLaborFallbackField(),
428
- );
421
+ const acceptedDetails = receipt;
429
422
  return {
430
423
  content: [{
431
424
  type: "text" as const,
@@ -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 | ComplianceAuditIncomplete;
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 must retain malformed candidates so they can settle as typed
29
- // audit-incomplete outcomes; status values are guidance, not a schema gate.
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_)) return { status: "audit-incomplete", observation: { kind: "non-object-arguments", type: arguments_ === null ? "null" : Array.isArray(arguments_) ? "array" : typeof arguments_ as ComplianceArgumentRootType }, candidate: arguments_, ...(usage === undefined ? {} : { usage }) };
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
- return { status: "audit-incomplete", observation: { kind: "object-status-unreadable", status: status === undefined ? "missing" : "unknown" }, candidate: arguments_, ...(usage === undefined ? {} : { usage }) };
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 = {
@@ -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
- if (dossier.status === "incomplete") return toAuditIncomplete(dossier.observation);
38
+ requireAuditMaterials(dossier);
39
39
  const subjects = readDoctorAuditSubjects(options.context);
40
- if (subjects.status === "incomplete") return toAuditIncomplete(subjects.observation);
40
+ requireAuditMaterials(subjects);
41
41
 
42
42
  return runComplianceAudit({
43
43
  tool,
@@ -1,10 +1,5 @@
1
1
  import { Type } from "typebox";
2
2
  import { canonicalJson } from "./canonical-json.ts";
3
- import {
4
- seatFallbackBaseStatus,
5
- seatFallbackStatusHasLawfulEvidence,
6
- type WithEngineLaborFallback,
7
- } from "./engine-labor-fallback.ts";
8
3
  import { openToolObjectFromUnion } from "./open-tool-schema.ts";
9
4
 
10
5
  export const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
@@ -42,13 +37,9 @@ export type DoctorFinding =
42
37
  export type DoctorSubmission =
43
38
  | { status: "completed"; case: DoctorCaseIdentity; findings: DoctorFinding[] }
44
39
  | { status: "refused"; reason: string; missingEvidence: Array<{ need: string; targetKeys: string[] }> };
45
- type DoctorOutputClean =
40
+ export type DoctorOutput =
46
41
  | { status: "completed"; case: DoctorCaseIdentity; findings: DoctorFinding[]; cost: DoctorCaseCost }
47
42
  | Extract<DoctorSubmission, { status: "refused" }>;
48
- /** Clean recorded shape or seat-fallback tainted accepted receipt (ADR 0071). */
49
- export type DoctorOutput =
50
- | DoctorOutputClean
51
- | WithEngineLaborFallback<DoctorOutputClean>;
52
43
  export type DoctorEvidenceEntry = { id: string; kind: "session" | "stderr"; byteLength: number; contentLength: number; sha256: string; content: string };
53
44
  export type DoctorCase = { version: 1; identity: DoctorCaseIdentity; evidence: DoctorEvidenceEntry[]; cost: DoctorCaseCost };
54
45
 
@@ -105,19 +96,13 @@ function isRecord(value: unknown): value is Record<string, unknown> { return typ
105
96
  function read(value: unknown, key: string): unknown { if (!isRecord(value)) return undefined; try { return value[key]; } catch { return undefined; } }
106
97
  export function validateDoctorSubmissionShape(value: unknown): DoctorSubmission {
107
98
  const status = read(value, "status");
108
- const base = typeof status === "string" ? seatFallbackBaseStatus(status) : status;
109
- if (base !== "completed" && base !== "refused") throw new DoctorSubmissionContractError("Doctor submission has no recognized execution status");
110
- // ADR 0071: tainted status requires latch-shaped engineLaborFallback evidence.
111
- if (typeof status === "string" && !seatFallbackStatusHasLawfulEvidence(status, value)) {
112
- throw new DoctorSubmissionContractError("Doctor submission has no recognized execution status");
113
- }
99
+ if (status !== "completed" && status !== "refused") throw new DoctorSubmissionContractError("Doctor submission has no recognized execution status");
114
100
  return value as DoctorSubmission;
115
101
  }
116
102
  export function validateRecordedDoctorOutput(value: unknown): DoctorOutput {
117
103
  const output = validateDoctorSubmissionShape(value);
118
104
  const status = read(output, "status");
119
- const base = typeof status === "string" ? seatFallbackBaseStatus(status) : status;
120
- if (base === "completed" && read(output, "cost") === undefined) throw new Error("Completed Doctor receipt has no runtime-owned cost testimony");
105
+ if (status === "completed" && read(output, "cost") === undefined) throw new Error("Completed Doctor receipt has no runtime-owned cost testimony");
121
106
  return output as DoctorOutput;
122
107
  }
123
108
 
@@ -2,10 +2,6 @@ import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-
2
2
  import { disposeComplianceDecision } from "./audit-escalation.ts";
3
3
  import { appendActiveSessionCustomEntry, type ComplianceDecision } from "./compliance-transport.ts";
4
4
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
5
- import {
6
- readActivationEngineLaborFallbackField,
7
- withEngineLaborFallbackField,
8
- } from "./engine-labor-fallback.ts";
9
5
  import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
10
6
 
11
7
  export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME };
@@ -16,7 +12,7 @@ export function createDoctorRoleRuntime(pi: ExtensionAPI, dependencies: DoctorRo
16
12
  let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
17
13
  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
14
  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, auditIncomplete: (result) => result }, acceptedDetails); } });
15
+ 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 = details; 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
16
  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
17
  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
18
  }
@@ -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
- export function toAuditIncomplete<TObservation extends DossierObservation>(
134
- observation: TObservation,
135
- ): { status: "audit-incomplete"; observation: TObservation; candidate: undefined } {
136
- return { status: "audit-incomplete", observation, candidate: undefined };
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
  }
@@ -2,9 +2,8 @@
2
2
  * Package-owned engine detour tool (#357 T2 / #378 / #380).
3
3
  * Registered by shared role-runtime when any role + engine activation signal is present.
4
4
  * Evidence-child legs install the same definition via customTools (no spawn in role modules).
5
- * #380: engine process failure soft-returns so the seat rejoins the main road; declaration
6
- * is recorded once via engine-labor-fallback (no fail-closed reject-leg).
7
- * Caller AbortSignal cancel propagates without fallback.
5
+ * Engine process failures stop through the host infrastructure-failure seam.
6
+ * Caller AbortSignal cancellation propagates unchanged.
8
7
  */
9
8
  import type {
10
9
  AgentToolResult,
@@ -22,11 +21,6 @@ import {
22
21
  isEngineDetourFailure,
23
22
  runEngineDetourOnce,
24
23
  } from "./engine-detour.ts";
25
- import {
26
- activationEngineLaborFallbackLatch,
27
- recordEngineLaborFallback,
28
- type EngineLaborFallbackField,
29
- } from "./engine-labor-fallback.ts";
30
24
 
31
25
  const engineDetourArgsSchema = Type.Object(
32
26
  {
@@ -56,27 +50,7 @@ export type EngineDetourToolRegistration = {
56
50
 
57
51
  type EngineDetourLatch = { used: boolean };
58
52
 
59
- function seatFallbackToolResult(
60
- field: EngineLaborFallbackField,
61
- failure: string,
62
- ): AgentToolResult<unknown> {
63
- return {
64
- content: [
65
- {
66
- type: "text" as const,
67
- text:
68
- `Engine detour failed: ${failure}. Perform the labor in this session (seat main road) and submit via the existing typed path. A mechanical fallback declaration will attach to the typed receipt.`,
69
- },
70
- ],
71
- details: {
72
- tool: ENGINE_DETOUR_TOOL_NAME,
73
- detourFailed: true,
74
- ...field,
75
- },
76
- };
77
- }
78
-
79
- /** Caller/upper-layer cancel must propagate; process failure is seat-fallback, not cancel. */
53
+ /** Caller/upper-layer cancellation must propagate unchanged. */
80
54
  function isCallerCancellation(
81
55
  error: unknown,
82
56
  signal: AbortSignal | undefined,
@@ -96,8 +70,8 @@ function isCallerCancellation(
96
70
  * Build one once-latch detour tool definition for a configured engine name.
97
71
  * `latch` is shared so parent registration can reset between activations.
98
72
  * `fail` owns host abort (parent) vs throw (evidence child) for tool misuse only.
99
- * Engine process failure (nonzero/empty/spawn) soft-returns seat fallback (#380).
100
- * Caller AbortSignal cancel propagates without writing fallback.
73
+ * Engine process failures (nonzero/empty/spawn) stop via `fail` with their cause.
74
+ * Caller AbortSignal cancellation propagates unchanged.
101
75
  */
102
76
  export function createEngineDetourToolDefinition(input: {
103
77
  engineName: string;
@@ -116,7 +90,7 @@ export function createEngineDetourToolDefinition(input: {
116
90
  `Use ${ENGINE_DETOUR_TOOL_NAME} exactly once for the configured engine (${engineName}). Optional packaged notes are guidance when present; a bare engine name alone is also a valid call path.`,
117
91
  "Pass argv for the host CLI of this engine name — first element is the executable name on PATH. Follow optional packaged notes when delivered; otherwise act from the engine name and the host CLI actual interface. Do not invent package flags.",
118
92
  "On success, use the returned stdout as labor content for the existing typed submission / report path.",
119
- "On engine failure the tool returns a soft failure: continue labor in this session and submit via the existing typed path. Do not treat engine failure as a reason to withhold the typed receipt.",
93
+ "An engine process failure stops this activation; do not continue labor in this session.",
120
94
  ],
121
95
  parameters: engineDetourArgsSchema,
122
96
  async execute(
@@ -145,17 +119,6 @@ export function createEngineDetourToolDefinition(input: {
145
119
  );
146
120
  }
147
121
 
148
- const softFail = (failure: string): AgentToolResult<unknown> => {
149
- // Activation-scoped latch is the sole shared recorder (parent seat + legs).
150
- const fallbackLatch =
151
- activationEngineLaborFallbackLatch() ?? { field: undefined };
152
- const field = recordEngineLaborFallback(fallbackLatch, {
153
- engine: engineName,
154
- failure,
155
- });
156
- return seatFallbackToolResult(field, failure);
157
- };
158
-
159
122
  let result: Awaited<ReturnType<typeof runEngineDetourOnce>>;
160
123
  try {
161
124
  result = await runEngineDetourOnce({
@@ -164,17 +127,19 @@ export function createEngineDetourToolDefinition(input: {
164
127
  ...(signal === undefined ? {} : { signal }),
165
128
  });
166
129
  } catch (error) {
167
- // Caller cancel: propagate. Spawn/engine failure: seat fallback.
168
- if (isCallerCancellation(error, signal)) {
169
- throw error;
170
- }
171
- const failure =
172
- error instanceof Error ? error.message : String(error);
173
- return softFail(failure.trim() === "" ? "engine detour spawn failed" : failure);
130
+ if (isCallerCancellation(error, signal)) throw error;
131
+ const cause = error instanceof Error
132
+ ? error
133
+ : new Error(String(error).trim() || "engine detour spawn failed");
134
+ input.fail(cause, toolCallId, ctx);
174
135
  }
175
136
 
176
137
  if (isEngineDetourFailure(result)) {
177
- return softFail(engineDetourFailureDiagnostic(result));
138
+ input.fail(
139
+ new Error(engineDetourFailureDiagnostic(result)),
140
+ toolCallId,
141
+ ctx,
142
+ );
178
143
  }
179
144
 
180
145
  return {
@@ -610,15 +610,17 @@ export async function executeEvidenceChild(
610
610
  engine: engineName,
611
611
  packageRoot: options.packageRoot,
612
612
  });
613
- // #378/#380: same detour tool as parent seat. Engine process failure soft-returns
614
- // seat main-road fallback (ADR 0069 detour-rejoins-main-road); tool misuse still throws.
613
+ // Evidence legs use the parent detour tool; retain any engine process cause
614
+ // so the enclosing child boundary, rather than a tool-error result, terminates.
615
+ let engineDetourFailure: Error | undefined;
615
616
  const engineDetourTool =
616
617
  engineName === undefined
617
618
  ? undefined
618
619
  : createEngineDetourToolDefinition({
619
620
  engineName,
620
621
  fail(error) {
621
- throw error instanceof Error ? error : new Error(String(error));
622
+ engineDetourFailure ??= error instanceof Error ? error : new Error(String(error));
623
+ throw engineDetourFailure;
622
624
  },
623
625
  });
624
626
  // No tools allowlist — Pi defaults + unrestricted evidence surface (ADR 0064).
@@ -654,8 +656,14 @@ export async function executeEvidenceChild(
654
656
  try {
655
657
  await session.prompt(delivered);
656
658
  } catch (error) {
659
+ if (engineDetourFailure !== undefined) {
660
+ throw classifiedError(engineDetourFailure, "child");
661
+ }
657
662
  throw classifiedError(error, "provider");
658
663
  }
664
+ if (engineDetourFailure !== undefined) {
665
+ throw classifiedError(engineDetourFailure, "child");
666
+ }
659
667
  if (signal?.aborted) throw new Error("Evidence child was cancelled");
660
668
  const lastAssistant = [...session.messages]
661
669
  .reverse()
@@ -927,7 +935,7 @@ export async function executeAuditorChild(
927
935
  decision = part.arguments;
928
936
  decisionCallId = part.id;
929
937
  // Pi can reject malformed root arguments before invoking execute;
930
- // that remains the existing typed audit-incomplete candidate path.
938
+ // that remains the existing unreadable-candidate failure path.
931
939
  if (part.arguments === undefined) decisionSubmitted = true;
932
940
  }
933
941
  }
@@ -20,7 +20,6 @@ import { randomUUID } from "node:crypto";
20
20
  import { lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
21
21
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
22
22
 
23
- import { seatFallbackBaseStatus } from "./engine-labor-fallback.ts";
24
23
  import {
25
24
  formatDurationZh,
26
25
  formatLocalDateTime,
@@ -177,8 +176,7 @@ export function decideTicketCurrentState(input: {
177
176
  const latest = sortRunsByStart(input.runs).at(-1)!;
178
177
  if (!latest.hasResult) return unacceptedBand(latest.mtimeMs, input.now.getTime());
179
178
  // Escalate is an awaiting overlay: same placement and sort band, distinct state.
180
- // Seat-fallback taint (`escalate-by-fallback`) keeps escalate base semantics.
181
- if (seatFallbackBaseStatus(latest.resultStatus) === "escalate") return "escalate-awaiting";
179
+ if ((latest.resultStatus) === "escalate") return "escalate-awaiting";
182
180
  return "accepted-awaiting";
183
181
  }
184
182
 
@@ -847,7 +845,7 @@ function buildBreadcrumbSteps(runs: readonly TicketTrajectoryRun[]): BreadcrumbS
847
845
  const isRejected = group.runs.some(
848
846
  (run) =>
849
847
  run.hasResult &&
850
- REJECTED_RESULT_STATUSES.has(seatFallbackBaseStatus(run.resultStatus)),
848
+ REJECTED_RESULT_STATUSES.has((run.resultStatus)),
851
849
  );
852
850
  steps.push({
853
851
  station: group.station,