@akagilnc/pi-workflow-roles 0.1.2521 → 0.1.2653-next.71811ca

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 (109) hide show
  1. package/README.md +129 -39
  2. package/README.zh-CN.md +128 -39
  3. package/dist/activation-ledger-topology.js +1 -1
  4. package/dist/archivist-record-entry.js +47 -26
  5. package/dist/auditor-dossier-tool.js +3 -1
  6. package/dist/compliance-transport.js +17 -26
  7. package/dist/doctor-contracts.js +1 -2
  8. package/dist/dossier-resolution.js +4 -8
  9. package/dist/engine-detour-tool.js +33 -8
  10. package/dist/engine-detour.js +1 -0
  11. package/dist/evidence-child-executor.js +277 -424
  12. package/dist/host-contracts.js +22 -0
  13. package/dist/institutional-resolution.js +88 -0
  14. package/dist/merger-contracts.js +1 -2
  15. package/dist/navigator-attendance.js +16 -0
  16. package/dist/notary-contracts.js +2 -3
  17. package/dist/package-contracts/fixer-output.js +1 -2
  18. package/dist/packaged-role-registry.js +102 -9
  19. package/dist/pi/in-process-session.js +671 -0
  20. package/dist/public-cli/config.js +7 -5
  21. package/dist/public-cli/main.js +3767 -4113
  22. package/dist/reviewer-child-executor.js +6 -1
  23. package/dist/session-opening-materials.js +10 -38
  24. package/dist/sitian-appender.js +146 -0
  25. package/dist/sitian-contracts.js +21 -0
  26. package/dist/sitian-facade.js +15 -0
  27. package/dist/sitian-reader.js +52 -0
  28. package/extensions/role-runtime.ts +9 -7
  29. package/package.json +5 -5
  30. package/resources/engines/cursor.md +12 -26
  31. package/resources/engines/hermes.md +2 -23
  32. package/resources/engines/opus.md +1 -16
  33. package/resources/navigator-route-playbook.md +2 -2
  34. package/scripts/build-package.mjs +0 -1
  35. package/souls/quality-law.md +1 -1
  36. package/src/activation-ledger-topology.ts +1 -1
  37. package/src/archivist-record-entry.ts +57 -45
  38. package/src/auditor-dossier-tool.ts +4 -2
  39. package/src/canonical-skill-binding.ts +37 -27
  40. package/src/collector-role.ts +11 -20
  41. package/src/collector-tool-schemas.ts +1 -4
  42. package/src/compliance-transport.ts +19 -44
  43. package/src/doctor-auditor.ts +1 -5
  44. package/src/doctor-contracts.ts +1 -4
  45. package/src/doctor-role.ts +31 -8
  46. package/src/dossier-resolution.ts +8 -8
  47. package/src/engine-detour-tool.ts +48 -19
  48. package/src/engine-detour.ts +3 -0
  49. package/src/evidence-child-executor.ts +277 -485
  50. package/src/factory-board.ts +3 -3
  51. package/src/gatekeeper-role.ts +21 -10
  52. package/src/host-contracts.ts +386 -0
  53. package/src/in-process-session.ts +1 -115
  54. package/src/institutional-resolution.ts +135 -0
  55. package/src/judge-auditor.ts +1 -5
  56. package/src/judge-role.ts +37 -51
  57. package/src/merger-contracts.ts +1 -4
  58. package/src/merger-role.ts +7 -11
  59. package/src/navigator-attendance.ts +20 -1
  60. package/src/notary-contracts.ts +8 -11
  61. package/src/notary-role.ts +7 -23
  62. package/src/notary-source-run.ts +4 -3
  63. package/src/package-contracts/fixer-output.ts +1 -4
  64. package/src/package-resources/method-skill-binding.ts +10 -34
  65. package/src/packaged-role-registry.ts +122 -12
  66. package/src/pi/adapter.ts +192 -0
  67. package/src/pi/durable-principal.ts +98 -0
  68. package/src/pi/in-process-session.ts +819 -0
  69. package/src/pi/known-failure.ts +77 -0
  70. package/src/pi/pi-normalization.ts +284 -0
  71. package/src/pi/role-turn-host.ts +459 -0
  72. package/src/public-cli/auto-resume.ts +42 -44
  73. package/src/public-cli/cli.ts +171 -257
  74. package/src/public-cli/coder-run.ts +115 -433
  75. package/src/public-cli/collector-run.ts +62 -314
  76. package/src/public-cli/config.ts +7 -5
  77. package/src/public-cli/doctor-run.ts +41 -52
  78. package/src/public-cli/fixer-run.ts +122 -427
  79. package/src/public-cli/invocation.ts +287 -96
  80. package/src/public-cli/judge-run.ts +92 -422
  81. package/src/public-cli/merger-run.ts +167 -429
  82. package/src/public-cli/notary-run.ts +41 -47
  83. package/src/public-cli/option-definitions.ts +136 -3
  84. package/src/public-cli/post-admission.ts +497 -0
  85. package/src/public-cli/public-run-credentials.ts +4 -4
  86. package/src/public-cli/reviewer-dispatch-rejection.ts +106 -0
  87. package/src/public-cli/reviewer-run.ts +132 -449
  88. package/src/public-cli/run-lifecycle.ts +339 -327
  89. package/src/public-cli/settlement.ts +251 -134
  90. package/src/public-cli/terminal.ts +3 -9
  91. package/src/public-cli/turn-request.ts +87 -0
  92. package/src/reviewer-child-executor.ts +9 -1
  93. package/src/reviewer-role.ts +11 -20
  94. package/src/role-runtime.ts +129 -78
  95. package/src/session-opening-materials.ts +25 -39
  96. package/src/sitian-appender.ts +171 -0
  97. package/src/sitian-contracts.ts +106 -0
  98. package/src/sitian-facade.ts +18 -0
  99. package/src/sitian-reader.ts +62 -0
  100. package/src/worker-role.ts +42 -40
  101. package/src/worker-submission-gates.ts +33 -0
  102. package/dist/archivist-role-run-coordinates.js +0 -28
  103. package/dist/package-contracts/terminating-infrastructure.js +0 -90
  104. package/resources/engines/sonnet.md +0 -27
  105. package/resources/engines/zcode.md +0 -63
  106. package/src/archivist-role-run-coordinates.ts +0 -45
  107. package/src/package-contracts/terminating-infrastructure.ts +0 -132
  108. package/src/public-cli/explicit-internal.ts +0 -427
  109. package/src/public-cli/one-shot-dispatch.ts +0 -300
@@ -6,15 +6,17 @@
6
6
  import { mkdtemp, rm } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
- import { createAssistantMessageEventStream, InMemoryCredentialStore, } from "@earendil-works/pi-ai";
10
- import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, prepareComplianceDispatch, } from "./compliance-transport.js";
9
+ import {} from "@earendil-works/pi-ai";
10
+ import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, } from "./compliance-transport.js";
11
+ import { auditorRunDirectory } from "./auditor-dossier-tool.js";
12
+ import { sitianReport } from "./sitian-facade.js";
11
13
  import { createEngineDetourToolDefinition } from "./engine-detour-tool.js";
12
14
  import { engineNameFromEnv } from "./engine-detour.js";
13
15
  import { appendEngineSessionMaterial, engineSessionMaterialFromOptions, } from "./package-resources/engine-material.js";
14
16
  import { readPackageMaterial } from "./session-opening-materials.js";
15
- import { formatModelSpec, loadPublicCliConfig, resolveGateOfficerModelSelection, } from "./public-cli/config.js";
17
+ import {} from "./public-cli/config.js";
18
+ import { readInstitutionalSeatSelection } from "./institutional-resolution.js";
16
19
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
17
- import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.js";
18
20
  import { hasUpstreamErrorTestimony, isNonSuccessHttpStatus, projectConfirmedRemotePayload, } from "./upstream-error-testimony.js";
19
21
  /**
20
22
  * Shared Standards/Spec evidence-child system materials — path roster only.
@@ -74,247 +76,33 @@ export async function withInProcessScratch(options, run) {
74
76
  }
75
77
  }
76
78
  /**
77
- * Build an inherited ModelRuntime + provider. Optional idle-only retry is the
78
- * ADR 0059 provider-stream seam (not package-owned-tool idle).
79
+ * Run every child cleanup, aggregating failures so one throwing cleanup never
80
+ * skips the rest (e.g. handle.close must still run when unsubscribe throws).
81
+ * If a primary failure is supplied and cleanup also failed, the two are combined
82
+ * into an AggregateError; otherwise only the failing branch is surfaced.
79
83
  */
80
- export async function createInheritedRuntime(options) {
81
- const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
82
- // Explicit override wins; never silent-fallback to parent when override is set (#453).
83
- const activeModel = options.model ?? options.context.model;
84
- if (activeModel === undefined)
85
- throw new Error(`${options.label} requires an active model`);
86
- const dispatch = await prepareComplianceDispatch(activeModel, options.context, options.label);
87
- const parentProvider = options.runCompletion === undefined
88
- ? options.context.modelRegistry.getProvider(activeModel.provider)
89
- : undefined;
90
- if (parentProvider === undefined && options.runCompletion === undefined) {
91
- throw new Error(`${options.label} provider not found: ${activeModel.provider}`);
92
- }
93
- const runtime = await ModelRuntime.create({
94
- credentials: new InMemoryCredentialStore(),
95
- modelsPath: null,
96
- });
97
- // Injected completions historically accepted the minimal model exposed by an
98
- // ExtensionContext. AgentSession crosses ModelRuntime first, so complete the
99
- // model metadata required by that runtime without changing provider identity.
100
- const inheritedModel = options.runCompletion === undefined
101
- ? dispatch.model
102
- : {
103
- ...dispatch.model,
104
- name: dispatch.model.name ?? dispatch.model.id,
105
- baseUrl: dispatch.model.baseUrl ?? "",
106
- reasoning: dispatch.model.reasoning ?? false,
107
- input: dispatch.model.input ?? ["text"],
108
- cost: dispatch.model.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
109
- contextWindow: dispatch.model.contextWindow ?? 1,
110
- maxTokens: dispatch.model.maxTokens ?? 1,
111
- };
112
- const state = {
113
- runtime,
114
- model: inheritedModel,
115
- dispatch,
116
- streamFailure: undefined,
117
- };
118
- const abortReason = (signal) => signal.reason ?? new Error(`${options.label} provider stream aborted`);
119
- async function waitForStream(promise, signal) {
120
- if (signal.aborted)
121
- throw abortReason(signal);
122
- let onAbort;
84
+ async function runChildCleanup(cleanups, primaryFailure, label) {
85
+ let cleanupFailure;
86
+ for (const cleanup of cleanups) {
123
87
  try {
124
- return await Promise.race([
125
- promise,
126
- new Promise((_resolve, reject) => {
127
- onAbort = () => reject(abortReason(signal));
128
- signal.addEventListener("abort", onAbort, { once: true });
129
- }),
130
- ]);
88
+ await cleanup();
131
89
  }
132
- finally {
133
- if (onAbort !== undefined)
134
- signal.removeEventListener("abort", onAbort);
90
+ catch (failure) {
91
+ cleanupFailure = cleanupFailure === undefined
92
+ ? failure
93
+ : new AggregateError([cleanupFailure, failure], `${label} cleanup failed`, {
94
+ cause: cleanupFailure,
95
+ });
135
96
  }
136
97
  }
137
- const createRetriedStream = (simple, model, context, request) => {
138
- const wrapped = createAssistantMessageEventStream();
139
- void (async () => {
140
- for (let attempt = 0;; attempt += 1) {
141
- const idle = createStreamIdleGuard(options.signal === undefined ? {} : { parentSignal: options.signal });
142
- // Typed HTTP status observed via onResponse — never inferred from prose.
143
- let observedHttpStatus;
144
- try {
145
- const requestSignal = request?.signal;
146
- const streamSignal = requestSignal === undefined
147
- ? idle.signal
148
- : AbortSignal.any([idle.signal, requestSignal]);
149
- const priorOnResponse = request?.onResponse;
150
- const inheritedRequest = {
151
- ...(request ?? {}),
152
- ...(dispatch.auth.env === undefined ? {} : { env: dispatch.auth.env }),
153
- signal: streamSignal,
154
- onResponse: async (response, model) => {
155
- if (typeof response?.status === "number")
156
- observedHttpStatus = response.status;
157
- await priorOnResponse?.(response, model);
158
- },
159
- };
160
- if (options.runCompletion !== undefined) {
161
- await new Promise((resolve) => setImmediate(resolve));
162
- if (streamSignal.aborted)
163
- throw abortReason(streamSignal);
164
- const completed = await waitForStream(options.runCompletion(model, options.injectedSystemPrompt === undefined
165
- ? context
166
- : { ...context, systemPrompt: options.injectedSystemPrompt }, inheritedRequest), streamSignal);
167
- const response = attachObservedHttpStatus({
168
- ...completed,
169
- api: model.api,
170
- provider: model.provider,
171
- model: model.id,
172
- }, observedHttpStatus);
173
- if (response.stopReason === "error" || response.stopReason === "aborted") {
174
- wrapped.push({ type: "error", reason: response.stopReason, error: response });
175
- }
176
- else {
177
- wrapped.end(response);
178
- }
179
- return;
180
- }
181
- const source = simple
182
- ? parentProvider.streamSimple(model, context, inheritedRequest)
183
- : parentProvider.stream(model, context, inheritedRequest);
184
- let sawEvent = false;
185
- const iterator = source[Symbol.asyncIterator]();
186
- while (true) {
187
- const next = await waitForStream(iterator.next(), idle.signal);
188
- if (next.done)
189
- break;
190
- sawEvent = true;
191
- idle.poke();
192
- wrapped.push(enrichStreamEvent(next.value, observedHttpStatus));
193
- }
194
- const response = attachObservedHttpStatus(await waitForStream(source.result(), idle.signal), observedHttpStatus);
195
- if (!sawEvent)
196
- wrapped.end(response);
197
- return;
198
- }
199
- catch (error) {
200
- if (request?.signal?.aborted) {
201
- const response = {
202
- role: "assistant",
203
- content: [],
204
- api: model.api,
205
- provider: model.provider,
206
- model: model.id,
207
- usage: emptyUsage(),
208
- stopReason: "aborted",
209
- errorMessage: "Auditor session aborted",
210
- timestamp: Date.now(),
211
- };
212
- wrapped.push({ type: "error", reason: "aborted", error: response });
213
- wrapped.end(response);
214
- return;
215
- }
216
- const failure = isStreamIdleTimeoutError(idle.signal.reason) ? idle.signal.reason : error;
217
- if (options.idleRetry === true
218
- && isStreamIdleTimeoutError(failure)
219
- && attempt < DEFAULT_COMPLIANCE_IDLE_MAX_RETRIES
220
- && options.signal?.aborted !== true) {
221
- continue;
222
- }
223
- state.streamFailure = failure;
224
- const projected = projectStructuredRemote(failure);
225
- // Prefer structured fields on the thrown failure; fall back to onResponse observation.
226
- const httpStatus = projected.httpStatus ?? numericHttpStatus(observedHttpStatus);
227
- const response = {
228
- role: "assistant",
229
- content: [],
230
- api: model.api,
231
- provider: model.provider,
232
- model: model.id,
233
- usage: emptyUsage(),
234
- stopReason: "error",
235
- // Preserve the held failure message bytes — do not rewrite.
236
- errorMessage: failure instanceof Error ? failure.message : String(failure),
237
- timestamp: Date.now(),
238
- ...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
239
- ...(httpStatus === undefined ? {} : { status: httpStatus, statusCode: httpStatus }),
240
- ...(projected.body === undefined ? {} : { body: projected.body }),
241
- ...(projected.code === undefined ? {} : { code: projected.code }),
242
- ...(projected.errno === undefined ? {} : { errno: projected.errno }),
243
- };
244
- wrapped.push({ type: "error", reason: "error", error: response });
245
- wrapped.end(response);
246
- return;
247
- }
248
- finally {
249
- idle.dispose();
250
- }
251
- }
252
- })();
253
- return wrapped;
254
- };
255
- // Provider id must match activeModel.provider so ModelRuntime auth lookup
256
- // finds this registration when a gate override changes provider (#453).
257
- const provider = options.idleRetry === true || options.runCompletion !== undefined
258
- ? {
259
- id: activeModel.provider,
260
- name: parentProvider?.name ?? options.label,
261
- auth: {
262
- apiKey: {
263
- name: `Inherited ${options.label} authentication`,
264
- async resolve() {
265
- const { env, ...auth } = dispatch.auth;
266
- return {
267
- auth: {
268
- ...auth,
269
- ...(dispatch.model.baseUrl === undefined ? {} : { baseUrl: dispatch.model.baseUrl }),
270
- },
271
- ...(env === undefined ? {} : { env }),
272
- };
273
- },
274
- },
275
- },
276
- getModels() { return [inheritedModel]; },
277
- stream(model, context, request) {
278
- return createRetriedStream(false, model, context, request);
279
- },
280
- streamSimple(model, context, request) {
281
- return createRetriedStream(true, model, context, request);
282
- },
283
- }
284
- : {
285
- id: parentProvider.id,
286
- name: parentProvider.name,
287
- ...(parentProvider.baseUrl === undefined ? {} : { baseUrl: parentProvider.baseUrl }),
288
- ...(parentProvider.headers === undefined ? {} : { headers: parentProvider.headers }),
289
- auth: {
290
- apiKey: {
291
- name: `Inherited ${options.label} authentication`,
292
- async resolve() {
293
- return {
294
- auth: {
295
- ...(dispatch.auth.apiKey === undefined ? {} : { apiKey: dispatch.auth.apiKey }),
296
- ...(dispatch.auth.headers === undefined ? {} : { headers: dispatch.auth.headers }),
297
- ...(dispatch.model.baseUrl === undefined ? {} : { baseUrl: dispatch.model.baseUrl }),
298
- },
299
- ...(dispatch.auth.env === undefined ? {} : { env: dispatch.auth.env }),
300
- };
301
- },
302
- },
303
- },
304
- getModels() { return [inheritedModel]; },
305
- stream(model, childContext, streamOptions) {
306
- return parentProvider.stream(model, childContext, streamOptions);
307
- },
308
- streamSimple(model, childContext, streamOptions) {
309
- return parentProvider.streamSimple(model, childContext, streamOptions);
310
- },
311
- };
312
- runtime.registerNativeProvider(provider);
313
- // registerNativeProvider fires a background refresh; await it so child
314
- // AgentSession.prompt sees configured auth without racing void refresh
315
- // (mock timers / slow CI otherwise stall before the compliance stream).
316
- await runtime.refresh({ allowNetwork: false });
317
- return state;
98
+ if (cleanupFailure === undefined)
99
+ return;
100
+ if (primaryFailure !== undefined) {
101
+ throw new AggregateError([primaryFailure, cleanupFailure], `${label} execution and cleanup failed`, { cause: primaryFailure });
102
+ }
103
+ throw new AggregateError([cleanupFailure], `${label} cleanup failed`, {
104
+ cause: cleanupFailure,
105
+ });
318
106
  }
319
107
  function numericHttpStatus(value) {
320
108
  return isNonSuccessHttpStatus(value) ? value : undefined;
@@ -428,6 +216,23 @@ function classifiedError(error, evidenceChildFailure) {
428
216
  : evidenceChildFailure;
429
217
  return Object.assign(wrapped, { evidenceChildFailure: classification });
430
218
  }
219
+ /** Extract the first text diagnostic from a flattened toolResult error `details`. */
220
+ function extractToolResultText(details) {
221
+ if (typeof details !== "object" || details === null)
222
+ return undefined;
223
+ const record = details;
224
+ const content = record.content;
225
+ if (Array.isArray(content)) {
226
+ for (const part of content) {
227
+ if (typeof part === "object" && part !== null) {
228
+ const text = part.text;
229
+ if (typeof text === "string" && text.trim() !== "")
230
+ return text;
231
+ }
232
+ }
233
+ }
234
+ return undefined;
235
+ }
431
236
  function emptyUsage() {
432
237
  return {
433
238
  input: 0,
@@ -452,24 +257,19 @@ function addUsage(total, next) {
452
257
  }
453
258
  export async function executeEvidenceChild(workspace, prompt, context, options = {}) {
454
259
  const signal = options.signal;
260
+ const runDirectory = options.runDirectory ?? auditorRunDirectory(context);
261
+ if (runDirectory === undefined) {
262
+ throw new Error("Evidence child requires a run directory carrying the institutional resolution page");
263
+ }
264
+ const selection = await readInstitutionalSeatSelection(runDirectory, "evidenceChild");
455
265
  return withInProcessScratch({
456
266
  prefix: "ak-evidence-child-",
457
267
  ...(options.credentialScratchParent === undefined
458
268
  ? {}
459
269
  : { parentDirectory: options.credentialScratchParent }),
460
270
  }, async (childConfigDir) => {
461
- const { openInProcessAgentSession } = await import("./in-process-session.js");
271
+ const { openPiInstitutionalSession } = await import("./pi/in-process-session.js");
462
272
  const { createRecordSession } = await import("./archivist-record-entry.js");
463
- let inherited;
464
- try {
465
- inherited = await createInheritedRuntime({
466
- context,
467
- label: "Evidence child",
468
- });
469
- }
470
- catch (error) {
471
- throw classifiedError(error, "provider");
472
- }
473
273
  // #378: when labor engine is configured, legs get the same detour tool + material
474
274
  // dual-path as the parent seat (ADR 0069 detour-rejoins-main-road).
475
275
  const engineName = engineNameFromEnv();
@@ -495,30 +295,38 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
495
295
  },
496
296
  });
497
297
  // No tools allowlist — Pi defaults + unrestricted evidence surface (ADR 0064).
498
- // Single createAgentSession owner: in-process-session.ts.
499
- const { session, dispose } = await openInProcessAgentSession({
500
- cwd: workspace,
501
- agentDir: childConfigDir,
502
- model: inherited.model,
503
- thinkingLevel: context.thinkingLevel ?? "off",
504
- modelRuntime: inherited.runtime,
505
- systemPrompt: await buildEvidenceChildSystemPrompt(engineMaterial),
506
- ...(engineDetourTool === undefined
507
- ? {}
508
- : { customTools: [engineDetourTool] }),
509
- sessionManager: createRecordSession({
298
+ // Single open seam owner: pi/in-process-session.ts.
299
+ let opened;
300
+ try {
301
+ opened = await openPiInstitutionalSession({
510
302
  cwd: workspace,
511
- kind: "evidence-children",
512
- ...(context.sessionManager === undefined ? {} : { parent: context.sessionManager }),
513
- }),
514
- });
303
+ agentDir: childConfigDir,
304
+ selection,
305
+ systemPrompt: await buildEvidenceChildSystemPrompt(engineMaterial),
306
+ ...(engineDetourTool === undefined
307
+ ? {}
308
+ : { customTools: [engineDetourTool] }),
309
+ sessionManager: createRecordSession({
310
+ cwd: workspace,
311
+ kind: "evidence-children",
312
+ ...(context.sessionManager === undefined ? {} : { parent: context.sessionManager }),
313
+ }),
314
+ ...(signal === undefined ? {} : { signal }),
315
+ label: "Evidence child",
316
+ });
317
+ }
318
+ catch (error) {
319
+ throw classifiedError(error, "provider");
320
+ }
321
+ const { handle } = opened;
515
322
  const usage = emptyUsage();
516
- const unsubscribe = session.subscribe((event) => {
517
- if (event.type === "message_end" && event.message.role === "assistant") {
518
- addUsage(usage, event.message.usage);
323
+ const unsubscribe = handle.subscribe((event) => {
324
+ if (event.type === "message_end" && event.role === "assistant") {
325
+ if (event.usage)
326
+ addUsage(usage, event.usage);
519
327
  }
520
328
  });
521
- const abortChild = () => { void session.abort(); };
329
+ const abortChild = () => { handle.abort(); };
522
330
  if (signal?.aborted)
523
331
  abortChild();
524
332
  else
@@ -526,8 +334,9 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
526
334
  let primaryFailure;
527
335
  try {
528
336
  const delivered = prompt;
337
+ let turnResult;
529
338
  try {
530
- await session.prompt(delivered);
339
+ turnResult = await handle.prompt(delivered);
531
340
  }
532
341
  catch (error) {
533
342
  if (engineDetourFailure !== undefined) {
@@ -540,22 +349,23 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
540
349
  }
541
350
  if (signal?.aborted)
542
351
  throw new Error("Evidence child was cancelled");
543
- const lastAssistant = [...session.messages]
544
- .reverse()
545
- .find((message) => message.role === "assistant");
352
+ const lastAssistant = turnResult.messages !== undefined
353
+ ? [...turnResult.messages].reverse().find((message) => message?.role === "assistant")
354
+ : undefined;
546
355
  // error|aborted assistant stops share the upstream-testimony rule: provider only
547
356
  // with direct HTTP/SDK testimony, otherwise existing unknown. child is reserved
548
357
  // for real local child/report failures (no assistant / blank report / cleanup).
549
- if (lastAssistant?.role === "assistant"
550
- && (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")) {
551
- throw classifiedError(new Error(lastAssistant.errorMessage ?? "", { cause: lastAssistant }), projectStructuredRemote(lastAssistant).hasTestimony ? "provider" : "unknown");
358
+ if (turnResult.stopReason === "error" || turnResult.stopReason === "aborted"
359
+ || (lastAssistant?.role === "assistant" && (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted"))) {
360
+ const errMsg = turnResult.errorMessage ?? lastAssistant?.errorMessage ?? "";
361
+ throw classifiedError(new Error(errMsg, { cause: lastAssistant }), lastAssistant && projectStructuredRemote(lastAssistant).hasTestimony ? "provider" : "unknown");
552
362
  }
553
- if (lastAssistant?.role !== "assistant") {
363
+ if (lastAssistant !== undefined && lastAssistant.role !== "assistant") {
554
364
  throw classifiedError(new Error("Evidence child child terminated without a report", {
555
- cause: lastAssistant ?? session.messages,
365
+ cause: lastAssistant ?? turnResult.messages,
556
366
  }), "child");
557
367
  }
558
- const report = session.getLastAssistantText() ?? "";
368
+ const report = turnResult.text;
559
369
  if (report.trim().length === 0) {
560
370
  throw new Error("Evidence child returned a blank child report");
561
371
  }
@@ -567,74 +377,30 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
567
377
  }
568
378
  finally {
569
379
  signal?.removeEventListener("abort", abortChild);
570
- let cleanupFailure;
571
- for (const cleanup of [() => unsubscribe(), () => dispose()]) {
572
- try {
573
- cleanup();
574
- }
575
- catch (failure) {
576
- cleanupFailure = cleanupFailure === undefined
577
- ? failure
578
- : new AggregateError([cleanupFailure, failure], "Reviewer child cleanup failed", {
579
- cause: cleanupFailure,
580
- });
581
- }
582
- }
583
- if (cleanupFailure !== undefined) {
584
- if (primaryFailure !== undefined) {
585
- throw new AggregateError([primaryFailure, cleanupFailure], "Reviewer child execution and cleanup failed", { cause: primaryFailure });
586
- }
587
- throw new AggregateError([cleanupFailure], "Reviewer child cleanup failed", {
588
- cause: cleanupFailure,
589
- });
590
- }
380
+ await runChildCleanup([() => unsubscribe(), () => handle.close()], primaryFailure, "Reviewer child");
591
381
  }
592
382
  });
593
383
  }
594
- /**
595
- * Resolve province child model from public-cli persistent config (#453).
596
- * Own override > gatekeeper override > unset (inherit parent). Availability
597
- * failures throw — createInheritedRuntime must not silent-fallback to parent.
598
- */
599
- async function resolveGateSeatModelOptions(context, seat, roleLabel) {
600
- const selection = resolveGateOfficerModelSelection(await loadPublicCliConfig(), seat);
601
- if (selection === undefined)
602
- return {};
603
- const find = context.modelRegistry.find?.bind(context.modelRegistry);
604
- if (typeof find !== "function") {
605
- throw new Error(`${roleLabel} model registry cannot resolve models`);
606
- }
607
- const model = find(selection.provider, selection.model);
608
- if (model === undefined) {
609
- throw new Error(`${roleLabel} model is unavailable: ${formatModelSpec(selection)}`);
610
- }
611
- return {
612
- model,
613
- ...(selection.thinking === undefined ? {} : { thinkingLevel: selection.thinking }),
614
- };
384
+ /** Resolution-page seat key for an auditor invocation: province gate seats map
385
+ * to their own page seats; doctor/judge compliance audits use the auditor seat. */
386
+ function auditorSeatKey(gateSeat) {
387
+ return gateSeat ?? "auditor";
615
388
  }
616
389
  /**
617
- * Auditor lifecycle via the shared in-process helper.
390
+ * Auditor lifecycle via the shared institutional sub-session adapter.
618
391
  * Adapter keeps role label / soul / decision tool / result projection only.
619
392
  * No tools allowlist (ADR 0064). Provider-stream idle-only retry (ADR 0059).
620
393
  * Durable child session via ADR 0065 archivist entry.
621
394
  */
622
395
  export async function executeAuditorChild(options) {
623
396
  const { createRecordSession } = await import("./archivist-record-entry.js");
397
+ const runDirectory = options.runDirectory ?? auditorRunDirectory(options.context);
398
+ if (runDirectory === undefined) {
399
+ throw new Error(`${options.roleLabel} requires a run directory carrying the institutional resolution page`);
400
+ }
401
+ const seat = auditorSeatKey(options.gateSeat);
402
+ const selection = await readInstitutionalSeatSelection(runDirectory, seat);
624
403
  return withInProcessScratch({ prefix: "ak-auditor-role-" }, async (scratch) => {
625
- const gate = options.gateSeat === undefined
626
- ? {}
627
- : await resolveGateSeatModelOptions(options.context, options.gateSeat, options.roleLabel);
628
- const inherited = await createInheritedRuntime({
629
- context: options.context,
630
- label: options.roleLabel,
631
- idleRetry: true,
632
- ...(gate.model === undefined ? {} : { model: gate.model }),
633
- ...(options.runCompletion === undefined
634
- ? {}
635
- : { runCompletion: options.runCompletion, injectedSystemPrompt: options.systemPrompt }),
636
- ...(options.signal === undefined ? {} : { signal: options.signal }),
637
- });
638
404
  const cwd = options.context.cwd ?? process.cwd();
639
405
  let decision;
640
406
  let noReceiptLifecycle;
@@ -656,12 +422,15 @@ export async function executeAuditorChild(options) {
656
422
  try {
657
423
  const result = await options.tool.execute(...args);
658
424
  delivery.recordAccepted();
659
- decision = args[1];
425
+ const rawDecision = args[1];
426
+ const isMissingArgs = rawDecision === undefined
427
+ || (typeof rawDecision === "object" && rawDecision !== null && !Array.isArray(rawDecision) && Object.keys(rawDecision).length === 0);
428
+ decision = isMissingArgs ? undefined : rawDecision;
660
429
  decisionCallId = args[0];
661
430
  decisionToolFailure = undefined;
662
431
  decisionToolFailures.delete(args[0]);
663
432
  decisionSubmitted = true;
664
- return result;
433
+ return { ...result, terminate: true };
665
434
  }
666
435
  catch (error) {
667
436
  decisionToolFailure = error;
@@ -679,18 +448,35 @@ export async function executeAuditorChild(options) {
679
448
  kind: "auditor-roles",
680
449
  ...(parentSessionManager === undefined ? {} : { parent: parentSessionManager }),
681
450
  });
682
- // Shared session open — no tools allowlist (ADR 0064).
683
- const { openInProcessAgentSession: openSession } = await import("./in-process-session.js");
684
- const { session, dispose } = await openSession({
451
+ // Shared session open — no tools allowlist (ADR 0064). Auth resolved
452
+ // child-locally from the explicit seat selection; adapter owns runtime/provider.
453
+ const { openPiInstitutionalSession } = await import("./pi/in-process-session.js");
454
+ const evidenceToolFailures = new Map();
455
+ const wrappedDossierTool = {
456
+ ...options.dossierTool,
457
+ label: options.roleLabel,
458
+ async execute(...args) {
459
+ try {
460
+ return await options.dossierTool.execute(...args);
461
+ }
462
+ catch (error) {
463
+ evidenceToolFailures.set(args[0], error);
464
+ throw error;
465
+ }
466
+ },
467
+ };
468
+ const opened = await openPiInstitutionalSession({
685
469
  cwd,
686
470
  agentDir: scratch,
687
- model: inherited.model,
688
- thinkingLevel: gate.thinkingLevel ?? options.context.thinkingLevel ?? "off",
689
- modelRuntime: inherited.runtime,
471
+ selection,
690
472
  systemPrompt: options.systemPrompt,
691
- customTools: [{ ...options.dossierTool, label: options.roleLabel }, tool],
473
+ customTools: [wrappedDossierTool, tool],
692
474
  sessionManager: auditorSessionManager,
475
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
476
+ idleRetry: true,
477
+ label: options.roleLabel,
693
478
  });
479
+ const { handle } = opened;
694
480
  const binding = {
695
481
  version: 1,
696
482
  parent: {
@@ -704,6 +490,17 @@ export async function executeAuditorChild(options) {
704
490
  // Durable binding is a prerequisite: never observe the provider when its
705
491
  // response could not later be tied to the current parent attempt.
706
492
  auditorSessionManager.appendCustomEntry(AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, binding);
493
+ try {
494
+ sitianReport({
495
+ level: "event",
496
+ kind: "auditor",
497
+ cwd,
498
+ sessionParent: parentSessionFile,
499
+ payload: { type: AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, ...binding },
500
+ source: "evidence-child-executor",
501
+ });
502
+ }
503
+ catch { }
707
504
  let turns = 0;
708
505
  const sessionUsage = emptyUsage();
709
506
  let boundaryResponse;
@@ -712,33 +509,13 @@ export async function executeAuditorChild(options) {
712
509
  let rejectedDecisionResponse;
713
510
  let promptNeighboringFailure;
714
511
  let promptDecisionFailures = [];
715
- const registeredToolNames = new Set(session.getAllTools().map((entry) => entry.name));
716
- const evidenceToolFailures = new Map();
717
- for (const name of registeredToolNames) {
718
- if (name === tool.name)
719
- continue;
720
- const definition = session.getToolDefinition(name);
721
- if (definition === undefined)
722
- continue;
723
- const execute = definition.execute.bind(definition);
724
- definition.execute = async (...args) => {
725
- try {
726
- return await execute(...args);
727
- }
728
- catch (error) {
729
- evidenceToolFailures.set(args[0], error);
730
- throw error;
731
- }
732
- };
733
- }
734
512
  const findToolFailure = (response) => {
735
- const callIds = response.content.flatMap((part) => part.type === "toolCall" && part.name !== tool.name && registeredToolNames.has(part.name) ? [part.id] : []);
513
+ const callIds = response.content.flatMap((part) => part.type === "toolCall" && part.name !== tool.name ? [part.id] : []);
736
514
  for (const callId of callIds) {
737
515
  if (evidenceToolFailures.has(callId))
738
516
  return evidenceToolFailures.get(callId);
739
517
  }
740
- const callIdSet = new Set(callIds);
741
- return [...session.messages].reverse().find((message) => message.role === "toolResult" && callIdSet.has(message.toolCallId) && message.isError);
518
+ return undefined;
742
519
  };
743
520
  const drainRejectedDecisionFailures = (response) => {
744
521
  for (const part of response.content) {
@@ -749,36 +526,83 @@ export async function executeAuditorChild(options) {
749
526
  decisionToolFailures.delete(part.id);
750
527
  }
751
528
  };
752
- const unsubscribe = session.subscribe((event) => {
753
- if (event.type === "message_end" && event.message.role === "assistant" && boundaryResponse === undefined) {
529
+ const retainedAssistants = [];
530
+ const unsubscribe = handle.subscribe((event) => {
531
+ // Capture evidence-tool (non-decision) failures from the handle's real
532
+ // tool_result events. The old runCompletion path wrapped every child tool's
533
+ // execute to observe failures; through the real handle the adapter forwards
534
+ // tool_execution_end isError. Record the errored tool call id so an adjacent
535
+ // evidence failure can outrank a settled/correctable decision feedback.
536
+ if (event.type === "tool_result" && event.isError === true && event.toolName !== tool.name) {
537
+ // Through the real handle the child flattens a throwing evidence tool
538
+ // into a toolResult error whose content text carries the diagnostic
539
+ // (e.g. "ENOENT: no such file or directory..."). Recover that text so
540
+ // the adjacent evidence failure outranks a settled decision feedback
541
+ // with its original diagnostic, mirroring the pre-migration wrapped
542
+ // execute capture (which held the raw error object).
543
+ const detailText = extractToolResultText(event.details);
544
+ // A native unknown-tool receipt ("Tool <name> not found") is not an
545
+ // evidence-tool failure: the child session has no such tool registered,
546
+ // so its errored toolResult must not short-circuit the auditor (it keeps
547
+ // prompting to the turn limit, mirroring the pre-migration
548
+ // registeredToolNames exclusion in findToolFailure).
549
+ if (detailText !== undefined && /^Tool\s+.+ not found$/.test(detailText.trim()))
550
+ return;
551
+ const failure = detailText === undefined
552
+ ? new Error(event.toolName ?? "evidence tool failed")
553
+ : new Error(detailText);
554
+ // Recover the errno code from the flattened diagnostic so an evidence
555
+ // failure's identity (e.g. code "ENOENT") is preserved across the real
556
+ // HTTP round-trip, mirroring the pre-migration raw error object.
557
+ const errno = /^([A-Z_]+):/.exec(detailText ?? "");
558
+ if (errno !== null && errno[1] !== undefined)
559
+ failure.code = errno[1];
560
+ evidenceToolFailures.set(event.toolCallId, failure);
561
+ }
562
+ if (event.type === "message_end" && event.role === "assistant" && boundaryResponse === undefined) {
754
563
  turns += 1;
755
- addUsage(sessionUsage, event.message.usage);
756
- retainedResponse = event.message;
757
- try {
758
- options.retainResponse?.(event.message);
759
- }
760
- catch (error) {
761
- retentionFailure = error;
762
- }
763
- // A tool call in assistant output is only an observation. Preserve its
764
- // candidate for typed malformed-decision settlement, but the wrapped
765
- // execute path above is the sole owner of accepted-receipt state; a
766
- // rejected execution must remain retryable in this same session.
767
- for (const part of event.message.content) {
768
- if (part.type === "toolCall" && part.name === tool.name) {
769
- rejectedDecisionResponse = event.message;
770
- if (decision === undefined) {
771
- decision = part.arguments;
772
- decisionCallId = part.id;
773
- // Pi can reject malformed root arguments before invoking execute;
774
- // that remains the existing unreadable-candidate failure path.
775
- if (part.arguments === undefined)
776
- decisionSubmitted = true;
564
+ if (event.usage)
565
+ addUsage(sessionUsage, event.usage);
566
+ const msg = event.message;
567
+ retainedResponse = msg;
568
+ if (msg) {
569
+ retainedAssistants.push(msg);
570
+ try {
571
+ options.retainResponse?.(msg);
572
+ }
573
+ catch (error) {
574
+ retentionFailure = error;
575
+ }
576
+ // A tool call in assistant output is only an observation. Preserve its
577
+ // candidate for typed malformed-decision settlement, but the wrapped
578
+ // execute path above is the sole owner of accepted-receipt state; a
579
+ // rejected execution must remain retryable in this same session.
580
+ for (const part of msg.content) {
581
+ if (part.type === "toolCall" && part.name === tool.name) {
582
+ rejectedDecisionResponse = msg;
583
+ if (decision === undefined) {
584
+ decision = (part.arguments === undefined
585
+ || (typeof part.arguments === "object" && part.arguments !== null
586
+ && !Array.isArray(part.arguments) && Object.keys(part.arguments).length === 0))
587
+ ? undefined
588
+ : part.arguments;
589
+ decisionCallId = part.id;
590
+ // Pi can reject malformed root arguments before invoking execute;
591
+ // that remains the existing unreadable-candidate failure path.
592
+ // A missing root argument reaches the real provider adapter as an
593
+ // empty object after serialization — treat it as missing too so a
594
+ // one-shot typed missing-args settlement does not solicit another turn.
595
+ if (part.arguments === undefined
596
+ || (typeof part.arguments === "object" && part.arguments !== null
597
+ && !Array.isArray(part.arguments) && Object.keys(part.arguments).length === 0)) {
598
+ decisionSubmitted = true;
599
+ }
600
+ }
777
601
  }
778
602
  }
603
+ if (turns >= AUDITOR_TURN_LIMIT || msg.stopReason === "error")
604
+ boundaryResponse = msg;
779
605
  }
780
- if (turns >= AUDITOR_TURN_LIMIT)
781
- boundaryResponse = event.message;
782
606
  }
783
607
  if (event.type === "turn_end") {
784
608
  if (rejectedDecisionResponse !== undefined) {
@@ -788,15 +612,16 @@ export async function executeAuditorChild(options) {
788
612
  if (decisionSubmitted || promptNeighboringFailure !== undefined
789
613
  || (boundaryResponse !== undefined && rejectedDecisionResponse === undefined)
790
614
  || retentionFailure !== undefined) {
791
- void session.abort();
615
+ handle.abort();
792
616
  }
793
617
  }
794
618
  });
795
- const abort = () => { void session.abort(); };
619
+ const abort = () => { handle.abort(); };
796
620
  if (options.signal?.aborted)
797
621
  abort();
798
622
  else
799
623
  options.signal?.addEventListener("abort", abort, { once: true });
624
+ let auditorFailure;
800
625
  try {
801
626
  try {
802
627
  const promptAllowingRejectedDecision = async (prompt) => {
@@ -806,7 +631,7 @@ export async function executeAuditorChild(options) {
806
631
  promptDecisionFailures = [];
807
632
  let promptFailure;
808
633
  try {
809
- await session.prompt(prompt);
634
+ await handle.prompt(prompt);
810
635
  }
811
636
  catch (error) {
812
637
  promptFailure = error;
@@ -831,6 +656,10 @@ export async function executeAuditorChild(options) {
831
656
  }
832
657
  if (decisionToolFailure !== undefined)
833
658
  return;
659
+ if (retentionFailure !== undefined)
660
+ return;
661
+ if (opened.streamFailure !== undefined)
662
+ throw opened.streamFailure;
834
663
  if (promptFailure !== undefined)
835
664
  throw promptFailure;
836
665
  };
@@ -842,8 +671,8 @@ export async function executeAuditorChild(options) {
842
671
  promptDecisionFailures = [];
843
672
  };
844
673
  await promptAllowingRejectedDecision(options.prompt);
845
- while (!decisionSubmitted && (boundaryResponse === undefined || decisionToolFailure !== undefined)
846
- && inherited.streamFailure === undefined && delivery.nextAction() === "request-delivery") {
674
+ while (!decisionSubmitted && retentionFailure === undefined && (boundaryResponse === undefined || decisionToolFailure !== undefined)
675
+ && opened.streamFailure === undefined && delivery.nextAction() === "request-delivery") {
847
676
  if (decisionToolFailure !== undefined) {
848
677
  const failures = promptDecisionFailures.length === 0
849
678
  ? [decisionToolFailure]
@@ -865,7 +694,7 @@ export async function executeAuditorChild(options) {
865
694
  await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
866
695
  }
867
696
  }
868
- if (!decisionSubmitted && inherited.streamFailure === undefined
697
+ if (!decisionSubmitted && retentionFailure === undefined && opened.streamFailure === undefined
869
698
  && delivery.nextAction() === "no-receipt") {
870
699
  const runPointer = options.context.sessionManager.getSessionFile() ?? options.context.cwd ?? process.cwd();
871
700
  const attemptPointer = binding.parent.attemptEntryId ?? binding.parent.sessionId ?? `current:${runPointer}`;
@@ -875,6 +704,17 @@ export async function executeAuditorChild(options) {
875
704
  // charged this prompt to the exhausted shared budget.
876
705
  decisionToolFailure = undefined;
877
706
  auditorSessionManager.appendCustomEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
707
+ try {
708
+ sitianReport({
709
+ level: "event",
710
+ kind: "auditor",
711
+ cwd,
712
+ sessionParent: parentSessionFile,
713
+ payload: { type: NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, ...facts },
714
+ source: "evidence-child-executor",
715
+ });
716
+ }
717
+ catch { }
878
718
  // Provenance is granted only after the lifecycle owner persisted the
879
719
  // current child record; accepted model arguments can never set it.
880
720
  noReceiptLifecycle = facts;
@@ -883,45 +723,44 @@ export async function executeAuditorChild(options) {
883
723
  catch (error) {
884
724
  if (options.signal?.aborted)
885
725
  throw options.signal.reason;
886
- if (inherited.streamFailure !== undefined)
887
- throw inherited.streamFailure;
888
- throw error;
726
+ if (retentionFailure === undefined && opened.streamFailure !== undefined)
727
+ throw opened.streamFailure;
728
+ if (retentionFailure === undefined)
729
+ throw error;
889
730
  }
890
731
  if (options.signal?.aborted)
891
732
  throw options.signal.reason;
892
- if (inherited.streamFailure !== undefined)
893
- throw inherited.streamFailure;
733
+ if (retentionFailure === undefined && opened.streamFailure !== undefined)
734
+ throw opened.streamFailure;
894
735
  if (!decisionSubmitted && decisionToolFailure !== undefined)
895
736
  throw decisionToolFailure;
896
737
  const relevantResponse = !decisionSubmitted
897
738
  ? boundaryResponse
898
- : [...session.messages].reverse().find((message) => message.role === "assistant" && message.content.some((part) => part.type === "toolCall" && part.name === tool.name));
739
+ : (retainedResponse && retainedResponse.role === "assistant" && retainedResponse.content.some((part) => part.type === "toolCall" && part.name === tool.name)
740
+ ? retainedResponse
741
+ : undefined);
899
742
  if (relevantResponse !== undefined) {
900
743
  const toolFailure = findToolFailure(relevantResponse);
901
744
  if (toolFailure !== undefined)
902
745
  throw toolFailure;
903
746
  }
904
- if (retentionFailure !== undefined && retainedResponse?.stopReason !== "error")
905
- throw retentionFailure;
906
- if (boundaryResponse !== undefined && !decisionSubmitted && noReceiptLifecycle === undefined) {
747
+ const assistants = [...retainedAssistants].reverse();
748
+ const response = !decisionSubmitted
749
+ ? assistants[0]
750
+ : assistants.find((message) => message.content.some((part) => part.type === "toolCall" && part.name === tool.name));
751
+ if (boundaryResponse !== undefined && boundaryResponse.stopReason !== "error" && !decisionSubmitted && noReceiptLifecycle === undefined) {
907
752
  const toolNames = boundaryResponse.content.flatMap((part) => part.type === "toolCall" ? [part.name] : []);
908
753
  throw new AuditorTurnLimitError(AUDITOR_TURN_LIMIT, turns, {
909
754
  stopReason: boundaryResponse.stopReason,
910
755
  toolNames,
911
756
  });
912
757
  }
913
- const assistants = [...session.messages]
914
- .reverse()
915
- .filter((message) => message.role === "assistant");
916
- const response = !decisionSubmitted
917
- ? assistants[0]
918
- : assistants.find((message) => message.content.some((part) => part.type === "toolCall" && part.name === tool.name));
919
758
  if (response !== undefined) {
920
759
  try {
760
+ if (retentionFailure !== undefined)
761
+ throw retentionFailure;
921
762
  if (retainedResponse === undefined)
922
763
  options.retainResponse?.(response);
923
- else if (retentionFailure !== undefined)
924
- throw retentionFailure;
925
764
  }
926
765
  catch (retentionFailure) {
927
766
  if (response.stopReason !== "error")
@@ -971,7 +810,7 @@ export async function executeAuditorChild(options) {
971
810
  }),
972
811
  },
973
812
  };
974
- auditorSessionManager.appendCustomEntry(AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, {
813
+ const failureData = {
975
814
  version: 1,
976
815
  parent: binding.parent,
977
816
  failure: {
@@ -980,15 +819,26 @@ export async function executeAuditorChild(options) {
980
819
  ...(failure.message === "" ? {} : { diagnostic: failure.message }),
981
820
  details: failure.details,
982
821
  },
983
- });
822
+ };
823
+ auditorSessionManager.appendCustomEntry(AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, failureData);
824
+ try {
825
+ sitianReport({
826
+ level: "event",
827
+ kind: "auditor",
828
+ cwd,
829
+ sessionParent: parentSessionFile,
830
+ payload: { type: AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, ...failureData },
831
+ source: "evidence-child-executor",
832
+ });
833
+ }
834
+ catch { }
984
835
  throw failure;
985
836
  }
986
837
  }
987
838
  if (response === undefined
988
839
  || response.stopReason === "error"
989
- || response.stopReason === "aborted"
990
- || (!decisionSubmitted && decision === undefined)) {
991
- throw new Error(`${options.roleLabel} exited without a readable decision receipt`);
840
+ || (!decisionSubmitted && (response.stopReason === "aborted" || decision === undefined))) {
841
+ throw new Error(response?.errorMessage ?? `${options.roleLabel} exited without a readable decision receipt`);
992
842
  }
993
843
  return {
994
844
  decision,
@@ -996,10 +846,13 @@ export async function executeAuditorChild(options) {
996
846
  ...(noReceiptLifecycle === undefined ? {} : { noReceiptLifecycle }),
997
847
  };
998
848
  }
849
+ catch (error) {
850
+ auditorFailure = error;
851
+ throw error;
852
+ }
999
853
  finally {
1000
854
  options.signal?.removeEventListener("abort", abort);
1001
- unsubscribe();
1002
- dispose();
855
+ await runChildCleanup([() => unsubscribe(), () => handle.close()], auditorFailure, options.roleLabel);
1003
856
  }
1004
857
  });
1005
858
  }